feat(wx-login): 更新微信登录配置和优化access_token管理

- 修改application.yml中的激活环境从glt2到ysb2
- 在CreditMpCustomerMapper.xml中增加to_user字段的关键词搜索功能
- 删除WxLoginController中重复的TimeUnit导入
- 实现access_token失效时的自动刷新重试机制
- 使用微信稳定的stable_token接口替代原有token接口
- 添加强制刷新access_token的功能支持
- 优化缓存TTL计算逻辑,确保token在过期前及时刷新
- 改进微信API错误处理和异常信息提示
This commit is contained in:
2026-03-18 10:43:10 +08:00
parent 5775ad862d
commit ced6178271
3 changed files with 98 additions and 53 deletions

View File

@@ -42,7 +42,6 @@ import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.PlatformConstants.MP_WEIXIN;
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
@@ -246,27 +245,30 @@ public class WxLoginController extends BaseController {
* @param userParam 需要传微信凭证code
*/
private String getPhoneByCode(UserParam userParam) {
// 获取手机号码
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getAccessToken();
HashMap<String, Object> paramMap = new HashMap<>();
if (StrUtil.isBlank(userParam.getCode())) {
throw new BusinessException("code不能为空");
}
paramMap.put("code", userParam.getCode());
// 执行post请求
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(post);
if (json.get("errcode").equals(0)) {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
// 微信用户的手机号码
final String phoneNumber = phoneInfo.getString("phoneNumber");
// 验证手机号码
// if (userParam.getNotVerifyPhone() == null && !Validator.isMobile(phoneNumber)) {
// String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString());
// redisTemplate.delete(key);
// throw new BusinessException("手机号码格式不正确");
// }
return phoneNumber;
// access_token 失效/过期时自动刷新并重试一次
for (int attempt = 0; attempt < 2; attempt++) {
String accessToken = (attempt == 0) ? getAccessToken(false) : getAccessToken(true);
String apiUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
String post = HttpUtil.post(apiUrl, JSON.toJSONString(paramMap));
JSONObject json = JSON.parseObject(post);
Integer errcode = json.getInteger("errcode");
if (errcode != null && errcode.equals(0)) {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
return phoneInfo.getString("phoneNumber");
}
if (errcode != null && (errcode == 40001 || errcode == 42001 || errcode == 40014)) {
continue;
}
return null;
}
return null;
}
@@ -285,6 +287,10 @@ public class WxLoginController extends BaseController {
* <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html">...</a>
*/
public String getAccessToken() {
return getAccessToken(false);
}
private String getAccessToken(boolean forceRefresh) {
Integer tenantId = getTenantId();
String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId.toString());
@@ -294,9 +300,13 @@ public class WxLoginController extends BaseController {
throw new BusinessException("请先配置小程序");
}
if (forceRefresh) {
redisTemplate.delete(key);
}
// 从缓存获取access_token
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
if (!forceRefresh && value != null) {
// 解析access_token
JSONObject response = JSON.parseObject(value);
String accessToken = response.getString("access_token");
@@ -305,23 +315,38 @@ public class WxLoginController extends BaseController {
}
}
// 微信获取凭证接口
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token";
// 组装url参数
String url = apiUrl.concat("?grant_type=client_credential")
.concat("&appid=").concat(setting.getString("appId"))
.concat("&secret=").concat(setting.getString("appSecret"));
String appId = setting.getString("appId");
String appSecret = setting.getString("appSecret");
// 执行get请求
String result = HttpUtil.get(url);
// 解析access_token
// 微信稳定版获取凭证接口避免并发刷新导致旧token失效
String apiUrl = "https://api.weixin.qq.com/cgi-bin/stable_token";
JSONObject reqBody = new JSONObject();
reqBody.put("grant_type", "client_credential");
reqBody.put("appid", appId);
reqBody.put("secret", appSecret);
reqBody.put("force_refresh", forceRefresh);
String result = HttpRequest.post(apiUrl)
.header("Content-Type", "application/json")
.body(reqBody.toJSONString())
.execute()
.body();
JSONObject response = JSON.parseObject(result);
if (response.getString("access_token") != null) {
// 存入缓存
redisTemplate.opsForValue().set(key, result, 7000L, TimeUnit.SECONDS);
return response.getString("access_token");
Integer errcode = response.getInteger("errcode");
if (errcode != null && errcode != 0) {
throw new BusinessException("获取access_token失败: " + response.getString("errmsg") + " (errcode: " + errcode + ")");
}
throw new BusinessException("小程序配置不正确");
String accessToken = response.getString("access_token");
Integer expiresIn = response.getInteger("expires_in");
if (accessToken != null) {
long ttlSeconds = Math.max((expiresIn != null ? expiresIn : 7200) - 300L, 60L);
redisTemplate.opsForValue().set(key, result, ttlSeconds, TimeUnit.SECONDS);
return accessToken;
}
throw new BusinessException("获取access_token失败: " + result);
}
@Operation(summary = "获取微信openId并更新")
@@ -576,23 +601,29 @@ public class WxLoginController extends BaseController {
throw new IOException("小程序配置不完整,缺少 appId 或 appSecret");
}
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/token")
.newBuilder()
.addQueryParameter("grant_type", "client_credential")
.addQueryParameter("appid", appId)
.addQueryParameter("secret", appSecret)
.build();
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/stable_token").newBuilder().build();
var root = om.createObjectNode();
root.put("grant_type", "client_credential");
root.put("appid", appId);
root.put("secret", appSecret);
root.put("force_refresh", false);
Request req = new Request.Builder().url(url).get().build();
okhttp3.RequestBody reqBody = okhttp3.RequestBody.create(
root.toString(), MediaType.parse("application/json; charset=utf-8"));
Request req = new Request.Builder().url(url).post(reqBody).build();
try (Response resp = http.newCall(req).execute()) {
String body = resp.body().string();
JsonNode json = om.readTree(body);
if (json.has("errcode") && json.get("errcode").asInt() != 0) {
throw new IOException("Get access_token failed: " + body);
}
if (json.has("access_token")) {
String token = json.get("access_token").asText();
long expiresIn = json.get("expires_in").asInt(7200);
// 缓存完整的JSON响应与其他方法保持一致
redisUtil.set(key, body, expiresIn, TimeUnit.SECONDS);
long ttlSeconds = Math.max(expiresIn - 300L, 60L);
redisUtil.set(key, body, ttlSeconds, TimeUnit.SECONDS);
tokenExpireEpoch = now + expiresIn;
System.out.println("获取新的access_token成功(Local)租户ID: " + tenantId);
return token;
@@ -789,10 +820,21 @@ public class WxLoginController extends BaseController {
String appId = wxConfig.getString("appId");
String appSecret = wxConfig.getString("appSecret");
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret;
String apiUrl = "https://api.weixin.qq.com/cgi-bin/stable_token";
System.out.println("调用微信API获取token - 租户ID: " + tenantId + ", AppID: " + (appId != null ? appId.substring(0, Math.min(8, appId.length())) + "..." : "null"));
System.out.println("微信API请求URL: " + apiUrl.replaceAll("secret=[^&]*", "secret=***"));
String result = HttpUtil.get(apiUrl);
System.out.println("微信API请求URL: " + apiUrl);
JSONObject reqBody = new JSONObject();
reqBody.put("grant_type", "client_credential");
reqBody.put("appid", appId);
reqBody.put("secret", appSecret);
reqBody.put("force_refresh", false);
String result = HttpRequest.post(apiUrl)
.header("Content-Type", "application/json")
.body(reqBody.toJSONString())
.execute()
.body();
System.out.println("微信API响应: " + result);
JSONObject json = JSON.parseObject(result);
@@ -800,14 +842,15 @@ public class WxLoginController extends BaseController {
if (json.containsKey("errcode")) {
Integer errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg);
if (errcode == 40125) {
throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret");
} else if (errcode == 40013) {
throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID");
} else {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
if (errcode != null && errcode != 0) {
System.err.println("微信API错误 - errcode: " + errcode + ", errmsg: " + errmsg);
if (errcode == 40125) {
throw new RuntimeException("微信AppSecret配置错误请检查并更新正确的AppSecret");
} else if (errcode == 40013) {
throw new RuntimeException("微信AppID配置错误请检查并更新正确的AppID");
} else {
throw new RuntimeException("微信API调用失败: " + errmsg + " (errcode: " + errcode + ")");
}
}
}
@@ -816,7 +859,8 @@ public class WxLoginController extends BaseController {
Integer expiresIn = json.getInteger("expires_in");
// 缓存access_token存储完整JSON响应与getAccessToken方法保持一致
redisUtil.set(key, result, (long) (expiresIn - 300), TimeUnit.SECONDS);
long ttlSeconds = Math.max((expiresIn != null ? expiresIn : 7200) - 300L, 60L);
redisUtil.set(key, result, ttlSeconds, TimeUnit.SECONDS);
System.out.println("获取新的access_token成功租户ID: " + tenantId);
return accessToken;

View File

@@ -73,6 +73,7 @@
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
OR a.to_user LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>

View File

@@ -4,7 +4,7 @@ server:
# 多环境配置
spring:
profiles:
active: glt2
active: ysb2
application:
name: server