From 8ea6a7dd1fcdbd4e22c564ed63a5a8023510161a Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 14 十月 2026 08:56:45 +0800
Subject: [PATCH] feat: 新增实名认证、企业认证、我的邀请与头像上传功能

---
 hugray-server/ruoyi-auth/src/main/java/com/ruoyi/auth/service/WeChatMiniappService.java |  140 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 140 insertions(+), 0 deletions(-)

diff --git a/hugray-server/ruoyi-auth/src/main/java/com/ruoyi/auth/service/WeChatMiniappService.java b/hugray-server/ruoyi-auth/src/main/java/com/ruoyi/auth/service/WeChatMiniappService.java
index 6e84452..43de14e 100644
--- a/hugray-server/ruoyi-auth/src/main/java/com/ruoyi/auth/service/WeChatMiniappService.java
+++ b/hugray-server/ruoyi-auth/src/main/java/com/ruoyi/auth/service/WeChatMiniappService.java
@@ -4,7 +4,9 @@
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.Base64;
 import java.util.concurrent.TimeUnit;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -47,6 +49,21 @@
 
     /** 换取失败统一提示 */
     private static final String PHONE_FAIL_MESSAGE = "微信手机号获取失败,请重试";
+
+    /** 获取不限量小程序码路径 */
+    private static final String QRCODE_PATH = "/wxa/getwxacodeunlimit";
+
+    /** 小程序码扫码直达页面 */
+    private static final String QRCODE_PAGE = "pages/login/register";
+
+    /** 小程序码图片宽度(px) */
+    private static final int QRCODE_WIDTH = 430;
+
+    /** 小程序码缓存时长(秒),30 天,避免重复消耗微信 getUnlimited 日配额 */
+    private static final long QRCODE_CACHE_SECONDS = 30L * 24 * 60 * 60;
+
+    /** 小程序码生成失败统一提示 */
+    private static final String QRCODE_FAIL_MESSAGE = "邀请码二维码生成失败,请重试";
 
     private final HttpClient httpClient = HttpClient.newBuilder()
             .connectTimeout(Duration.ofSeconds(5))
@@ -91,6 +108,97 @@
             throw new ServiceException(PHONE_FAIL_MESSAGE);
         }
         return phoneNumber;
+    }
+
+    /**
+     * 生成不限量小程序码(wxacode.getUnlimited)
+     *
+     * 以邀请码作为 scene,扫码直达注册页并由前端自动回填邀请码。
+     * 结果按 scene 缓存 30 天,命中缓存直接返回,避免重复消耗微信接口日配额。
+     *
+     * @param scene 场景值,即邀请码(8 位 A-Z0-9)
+     * @return PNG 图片的 base64 DataURI(data:image/png;base64,...)
+     */
+    public String getUnlimitedQrcode(String scene)
+    {
+        // STEP 1: 命中缓存直接返回
+        String cacheKey = CacheConstants.WECHAT_QRCODE_KEY + scene;
+        String cached = redisService.getCacheObject(cacheKey);
+        if (StringUtils.isNotEmpty(cached))
+        {
+            return cached;
+        }
+
+        // STEP 2: 校验配置是否完整
+        String appid = weChatMiniappProperties.getAppid();
+        String secret = weChatMiniappProperties.getSecret();
+        if (StringUtils.isEmpty(appid) || StringUtils.isEmpty(secret))
+        {
+            log.error("[WECHAT] miniapp appid/secret 未配置");
+            throw new ServiceException(QRCODE_FAIL_MESSAGE);
+        }
+
+        // STEP 3: 获取全局 access_token
+        String accessToken = getAccessToken(appid, secret);
+
+        // STEP 4: 调用微信生成小程序码(scene 为邀请码,已由入参正则限定为 A-Z0-9,可安全拼接)
+        String url = WECHAT_API_HOST + QRCODE_PATH + "?access_token=" + accessToken;
+        String requestBody = "{\"scene\":\"" + scene + "\",\"page\":\"" + QRCODE_PAGE
+                + "\",\"check_path\":false,\"env_version\":\"release\",\"width\":" + QRCODE_WIDTH + "}";
+        HttpResponse<byte[]> response = doPostForBytes(url, requestBody);
+
+        // STEP 5: 按响应类型解析(图片转 base64,JSON 则说明失败)
+        String dataUri = parseQrcodeResponse(response, scene);
+
+        // STEP 6: 缓存生成结果并返回
+        redisService.setCacheObject(cacheKey, dataUri, QRCODE_CACHE_SECONDS, TimeUnit.SECONDS);
+        return dataUri;
+    }
+
+    /**
+     * 解析小程序码响应
+     *
+     * 微信成功返回 PNG 字节流(Content-Type: image/*),失败返回 JSON(含 errcode/errmsg)。
+     *
+     * @param response 微信响应
+     * @param scene 邀请码(仅用于日志)
+     * @return PNG 的 base64 DataURI
+     */
+    private String parseQrcodeResponse(HttpResponse<byte[]> response, String scene)
+    {
+        String contentType = response.headers().firstValue("Content-Type").orElse("");
+        byte[] body = response.body();
+
+        // 图片响应:PNG 二进制转 base64 DataURI
+        if (contentType.startsWith("image/"))
+        {
+            if (body == null || body.length == 0)
+            {
+                log.error("[WECHAT] getUnlimitedQrcode empty image, scene={}", scene);
+                throw new ServiceException(QRCODE_FAIL_MESSAGE);
+            }
+            return "data:image/png;base64," + Base64.getEncoder().encodeToString(body);
+        }
+
+        // 非图片响应:微信以 JSON 返回错误码,解析并记日志
+        Integer errcode = null;
+        String errmsg = null;
+        try
+        {
+            String text = body == null ? null : new String(body, StandardCharsets.UTF_8);
+            JSONObject json = JSON.parseObject(text);
+            if (StringUtils.isNotNull(json))
+            {
+                errcode = json.getInteger("errcode");
+                errmsg = json.getString("errmsg");
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("[WECHAT] getUnlimitedQrcode parse error, scene={}", scene, e);
+        }
+        log.error("[WECHAT] getUnlimitedQrcode failed, scene={}, errcode={}, errmsg={}", scene, errcode, errmsg);
+        throw new ServiceException(QRCODE_FAIL_MESSAGE);
     }
 
     /**
@@ -177,4 +285,36 @@
             throw new ServiceException(PHONE_FAIL_MESSAGE);
         }
     }
+
+    /**
+     * 发送 HTTP POST 请求并按字节数组接收响应(用于小程序码等二进制接口)
+     *
+     * @param url 请求地址
+     * @param body 请求体 JSON,可为空
+     * @return 原始响应(含状态头与字节体)
+     */
+    private HttpResponse<byte[]> doPostForBytes(String url, String body)
+    {
+        try
+        {
+            HttpRequest.Builder builder = HttpRequest.newBuilder()
+                    .uri(URI.create(url))
+                    .timeout(Duration.ofSeconds(WECHAT_REQUEST_TIMEOUT_SECONDS))
+                    .header("Content-Type", "application/json");
+            if (StringUtils.isEmpty(body))
+            {
+                builder.POST(HttpRequest.BodyPublishers.noBody());
+            }
+            else
+            {
+                builder.POST(HttpRequest.BodyPublishers.ofString(body));
+            }
+            return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofByteArray());
+        }
+        catch (Exception e)
+        {
+            log.error("[WECHAT] request failed, url={}", url, e);
+            throw new ServiceException(QRCODE_FAIL_MESSAGE);
+        }
+    }
 }

--
Gitblit v1.7.1