diff --git a/src/main/java/com/example/mini_program/config/WxCorpConfig.java b/src/main/java/com/example/mini_program/config/WxCorpConfig.java index 056a6d5..51bf921 100644 --- a/src/main/java/com/example/mini_program/config/WxCorpConfig.java +++ b/src/main/java/com/example/mini_program/config/WxCorpConfig.java @@ -28,4 +28,9 @@ public class WxCorpConfig { * 审批回调URL(可选) */ private String callbackUrl; + + /** + * 审批申请人用户ID(提交审批的企微用户) + */ + private String creatorUserid; } diff --git a/src/main/java/com/example/mini_program/service/WxService.java b/src/main/java/com/example/mini_program/service/WxService.java index 711d1fc..5a94a76 100644 --- a/src/main/java/com/example/mini_program/service/WxService.java +++ b/src/main/java/com/example/mini_program/service/WxService.java @@ -1,14 +1,12 @@ package com.example.mini_program.service; -import com.example.mini_program.config.WxMiniAppConfig; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; import java.nio.charset.StandardCharsets; @@ -17,23 +15,17 @@ import java.nio.charset.StandardCharsets; public class WxService { 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 WxTokenService wxTokenService; + @Getter @Value("${wx.miniapp.env:release}") private String defaultEnvVersion; - private String cachedAccessToken; - private long tokenExpireTime; - - public WxService(WxMiniAppConfig wxMiniAppConfig, RestTemplate restTemplate) { - this.wxMiniAppConfig = wxMiniAppConfig; - this.restTemplate = restTemplate; + public WxService(WxTokenService wxTokenService) { this.objectMapper = new ObjectMapper(); + this.wxTokenService = wxTokenService; } /** @@ -73,32 +65,11 @@ public class WxService { return java.util.Base64.getEncoder().encodeToString(response); } - public String getDefaultEnvVersion() { - return defaultEnvVersion; - } - /** - * 获取微信access_token(带缓存) + * 获取微信access_token(使用统一的TokenService) */ private String getAccessToken() throws Exception { - if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) { - 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; + return wxTokenService.getAccessToken(); } /** diff --git a/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java b/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java index af83e3e..418eb7d 100644 --- a/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java +++ b/src/main/java/com/example/mini_program/service/WxSubscribeMessageService.java @@ -1,6 +1,5 @@ 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; @@ -27,33 +26,15 @@ import java.util.Map; @RequiredArgsConstructor 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 = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s"; - private final WxMiniAppConfig wxMiniAppConfig; private final ObjectMapper objectMapper; + private final WxTokenService wxTokenService; @Value("${wx.miniapp.subscribe-template-id:}") private String templateId; - /** - * 获取小程序 access_token - */ - public String getAccessToken() { - String url = String.format(GET_TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret()); - try { - Map 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类型不接受纯数字 @@ -64,7 +45,7 @@ public class WxSubscribeMessageService { log.warn("未配置 subscribe-template-id,跳过订阅消息推送"); return; } - String accessToken = getAccessToken(); + String accessToken = wxTokenService.getAccessToken(); String url = String.format(SEND_MSG_URL, accessToken); // name1 是 name 类型,纯数字会被微信拒绝,需加前缀兜底 diff --git a/src/main/java/com/example/mini_program/service/WxTokenService.java b/src/main/java/com/example/mini_program/service/WxTokenService.java new file mode 100644 index 0000000..754f8cf --- /dev/null +++ b/src/main/java/com/example/mini_program/service/WxTokenService.java @@ -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 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 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 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 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; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 114d74e..0f98f7c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,6 +17,8 @@ wx: secret: e82fa407fad13a9df35503f2d176e5a4 subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ env: develop # 环境版本: release(正式版), trial(体验版), develop(开发版) + token-type: stable # token类型: standard(标准版), stable(稳定版) + token-expire-buffer: 300 # token提前过期缓冲时间(秒) corp: # 企业ID corpid: ww257614cff8a1b61b # 应用Secret