refactor: 重构微信Token管理逻辑
This commit is contained in:
@@ -28,4 +28,9 @@ public class WxCorpConfig {
|
|||||||
* 审批回调URL(可选)
|
* 审批回调URL(可选)
|
||||||
*/
|
*/
|
||||||
private String callbackUrl;
|
private String callbackUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批申请人用户ID(提交审批的企微用户)
|
||||||
|
*/
|
||||||
|
private String creatorUserid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package com.example.mini_program.service;
|
package com.example.mini_program.service;
|
||||||
|
|
||||||
import com.example.mini_program.config.WxMiniAppConfig;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@@ -17,23 +15,17 @@ import java.nio.charset.StandardCharsets;
|
|||||||
public class WxService {
|
public class WxService {
|
||||||
|
|
||||||
private static final String WXACODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
|
private static final String WXACODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
|
||||||
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
||||||
private static final int TOKEN_EXPIRE_BUFFER = 300; // access_token提前过期时间(秒)
|
|
||||||
|
|
||||||
private final WxMiniAppConfig wxMiniAppConfig;
|
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
@Getter
|
||||||
@Value("${wx.miniapp.env:release}")
|
@Value("${wx.miniapp.env:release}")
|
||||||
private String defaultEnvVersion;
|
private String defaultEnvVersion;
|
||||||
|
|
||||||
private String cachedAccessToken;
|
public WxService(WxTokenService wxTokenService) {
|
||||||
private long tokenExpireTime;
|
|
||||||
|
|
||||||
public WxService(WxMiniAppConfig wxMiniAppConfig, RestTemplate restTemplate) {
|
|
||||||
this.wxMiniAppConfig = wxMiniAppConfig;
|
|
||||||
this.restTemplate = restTemplate;
|
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.wxTokenService = wxTokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,32 +65,11 @@ public class WxService {
|
|||||||
return java.util.Base64.getEncoder().encodeToString(response);
|
return java.util.Base64.getEncoder().encodeToString(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDefaultEnvVersion() {
|
|
||||||
return defaultEnvVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取微信access_token(带缓存)
|
* 获取微信access_token(使用统一的TokenService)
|
||||||
*/
|
*/
|
||||||
private String getAccessToken() throws Exception {
|
private String getAccessToken() throws Exception {
|
||||||
if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
|
return wxTokenService.getAccessToken();
|
||||||
return cachedAccessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = String.format(TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret());
|
|
||||||
log.info("刷新access_token");
|
|
||||||
|
|
||||||
String response = restTemplate.getForObject(url, String.class);
|
|
||||||
JsonNode json = objectMapper.readTree(response);
|
|
||||||
|
|
||||||
String accessToken = json.get("access_token").asText();
|
|
||||||
int expiresIn = json.get("expires_in").asInt();
|
|
||||||
|
|
||||||
cachedAccessToken = accessToken;
|
|
||||||
tokenExpireTime = System.currentTimeMillis() + (expiresIn - TOKEN_EXPIRE_BUFFER) * 1000L;
|
|
||||||
|
|
||||||
log.info("access_token已更新,有效期: {} 秒", expiresIn);
|
|
||||||
return accessToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.example.mini_program.service;
|
package com.example.mini_program.service;
|
||||||
|
|
||||||
import com.example.mini_program.config.WxMiniAppConfig;
|
|
||||||
import com.example.mini_program.util.HttpUtil;
|
import com.example.mini_program.util.HttpUtil;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -27,33 +26,15 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WxSubscribeMessageService {
|
public class WxSubscribeMessageService {
|
||||||
|
|
||||||
private static final String GET_TOKEN_URL =
|
|
||||||
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
||||||
private static final String SEND_MSG_URL =
|
private static final String SEND_MSG_URL =
|
||||||
"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
|
"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
|
||||||
|
|
||||||
private final WxMiniAppConfig wxMiniAppConfig;
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final WxTokenService wxTokenService;
|
||||||
|
|
||||||
@Value("${wx.miniapp.subscribe-template-id:}")
|
@Value("${wx.miniapp.subscribe-template-id:}")
|
||||||
private String templateId;
|
private String templateId;
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取小程序 access_token
|
|
||||||
*/
|
|
||||||
public String getAccessToken() {
|
|
||||||
String url = String.format(GET_TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret());
|
|
||||||
try {
|
|
||||||
Map<String, Object> resp = objectMapper.readValue(HttpUtil.get(url), Map.class);
|
|
||||||
if (resp.containsKey("access_token")) {
|
|
||||||
return (String) resp.get("access_token");
|
|
||||||
}
|
|
||||||
throw new RuntimeException("获取access_token失败: " + resp);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("获取access_token异常: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送订阅消息
|
* 发送订阅消息
|
||||||
* 所有字段不允许为空字符串(微信返回47003),name类型不接受纯数字
|
* 所有字段不允许为空字符串(微信返回47003),name类型不接受纯数字
|
||||||
@@ -64,7 +45,7 @@ public class WxSubscribeMessageService {
|
|||||||
log.warn("未配置 subscribe-template-id,跳过订阅消息推送");
|
log.warn("未配置 subscribe-template-id,跳过订阅消息推送");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String accessToken = getAccessToken();
|
String accessToken = wxTokenService.getAccessToken();
|
||||||
String url = String.format(SEND_MSG_URL, accessToken);
|
String url = String.format(SEND_MSG_URL, accessToken);
|
||||||
|
|
||||||
// name1 是 name 类型,纯数字会被微信拒绝,需加前缀兜底
|
// name1 是 name 类型,纯数字会被微信拒绝,需加前缀兜底
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.example.mini_program.service;
|
||||||
|
|
||||||
|
import com.example.mini_program.config.WxMiniAppConfig;
|
||||||
|
import com.example.mini_program.util.HttpUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 access_token 统一管理服务
|
||||||
|
* 支持标准接口和稳定版接口
|
||||||
|
* 提供缓存机制,避免重复获取
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxTokenService {
|
||||||
|
|
||||||
|
private static final String STANDARD_TOKEN_URL =
|
||||||
|
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
||||||
|
private static final String STABLE_TOKEN_URL =
|
||||||
|
"https://api.weixin.qq.com/cgi-bin/stable_token";
|
||||||
|
|
||||||
|
private final WxMiniAppConfig wxMiniAppConfig;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${wx.miniapp.token-type:standard}")
|
||||||
|
private String tokenType; // standard 或 stable
|
||||||
|
|
||||||
|
@Value("${wx.miniapp.token-expire-buffer:300}")
|
||||||
|
private int tokenExpireBuffer; // 提前过期缓冲时间(秒)
|
||||||
|
|
||||||
|
// 缓存:key为appid,value为token信息和过期时间
|
||||||
|
private static class TokenCache {
|
||||||
|
String token;
|
||||||
|
long expireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, TokenCache> tokenCacheMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信小程序 access_token
|
||||||
|
* @return access_token
|
||||||
|
*/
|
||||||
|
public String getAccessToken() {
|
||||||
|
String appid = wxMiniAppConfig.getAppid();
|
||||||
|
String secret = wxMiniAppConfig.getSecret();
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
TokenCache cache = tokenCacheMap.get(appid);
|
||||||
|
if (cache != null && System.currentTimeMillis() < cache.expireTime) {
|
||||||
|
log.debug("使用缓存的 access_token");
|
||||||
|
return cache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新token
|
||||||
|
String accessToken;
|
||||||
|
int expiresIn;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ("stable".equalsIgnoreCase(tokenType)) {
|
||||||
|
log.info("使用稳定版接口获取 access_token");
|
||||||
|
Map<String, String> requestBody = Map.of(
|
||||||
|
"grant_type", "client_credential",
|
||||||
|
"appid", appid,
|
||||||
|
"secret", secret
|
||||||
|
);
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||||
|
String response = HttpUtil.postJson(STABLE_TOKEN_URL, jsonBody);
|
||||||
|
Map<String, Object> resp = objectMapper.readValue(response, Map.class);
|
||||||
|
|
||||||
|
if (resp.containsKey("access_token")) {
|
||||||
|
accessToken = (String) resp.get("access_token");
|
||||||
|
expiresIn = (Integer) resp.get("expires_in");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("稳定版接口获取token失败: " + resp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("使用标准接口获取 access_token");
|
||||||
|
String url = String.format(STANDARD_TOKEN_URL, appid, secret);
|
||||||
|
String response = HttpUtil.get(url);
|
||||||
|
Map<String, Object> resp = objectMapper.readValue(response, Map.class);
|
||||||
|
|
||||||
|
if (resp.containsKey("access_token")) {
|
||||||
|
accessToken = (String) resp.get("access_token");
|
||||||
|
expiresIn = (Integer) resp.get("expires_in");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("标准接口获取token失败: " + resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
TokenCache newCache = new TokenCache();
|
||||||
|
newCache.token = accessToken;
|
||||||
|
newCache.expireTime = System.currentTimeMillis() + (expiresIn - tokenExpireBuffer) * 1000L;
|
||||||
|
tokenCacheMap.put(appid, newCache);
|
||||||
|
|
||||||
|
log.info("access_token 已更新,有效期: {} 秒,缓存过期时间: {} 秒后",
|
||||||
|
expiresIn, expiresIn - tokenExpireBuffer);
|
||||||
|
return accessToken;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取 access_token 异常", e);
|
||||||
|
throw new RuntimeException("获取 access_token 失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存,强制刷新token
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
tokenCacheMap.clear();
|
||||||
|
log.info("access_token 缓存已清除");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前缓存状态
|
||||||
|
*/
|
||||||
|
public boolean hasValidCache() {
|
||||||
|
String appid = wxMiniAppConfig.getAppid();
|
||||||
|
TokenCache cache = tokenCacheMap.get(appid);
|
||||||
|
return cache != null && System.currentTimeMillis() < cache.expireTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ wx:
|
|||||||
secret: e82fa407fad13a9df35503f2d176e5a4
|
secret: e82fa407fad13a9df35503f2d176e5a4
|
||||||
subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
|
subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
|
||||||
env: develop # 环境版本: release(正式版), trial(体验版), develop(开发版)
|
env: develop # 环境版本: release(正式版), trial(体验版), develop(开发版)
|
||||||
|
token-type: stable # token类型: standard(标准版), stable(稳定版)
|
||||||
|
token-expire-buffer: 300 # token提前过期缓冲时间(秒)
|
||||||
corp: # 企业ID
|
corp: # 企业ID
|
||||||
corpid: ww257614cff8a1b61b
|
corpid: ww257614cff8a1b61b
|
||||||
# 应用Secret
|
# 应用Secret
|
||||||
|
|||||||
Reference in New Issue
Block a user