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