application.yml
wx:
appid: yourAppId
secret: yourAppSecret
从小程序 → 开发管理/开发设置 里去拿
用途:1. 获取 access_token(调用微信后台接口的“通行证”)
后端会用 appid + secret 去请求 cgi-bin/token,拿到 access_token,然后再带着 token 调“接口
用 js_code 换 openid(登录态)
小程序端 wx.login 给你一个 js_code,后端用 appid + secret + js_code 去调用 jscode2session 换回 openid
对应配置类
@Data
@Component
@ConfigurationProperties(prefix = "wx")
public class WxProps {
private String appid;
private String secret;
}
@ConfigurationProperties(prefix = "wx"):告诉 Spring,把配置文件里以 wx. 开头的配置项,按字段名映射到这个类里面去
@Service
@RequiredArgsConstructor
public class WxAccessTokenManager {
private final WxProps props; // 配置类 拿AppId和AppSecret
private final RestTemplate rt = new RestTemplate();// 用来发HTTP请求到微信接口
private volatile long expireAtMs = 0; // 缓存住 token 的过期时间点(毫秒时间戳)
private volatile String token; // 缓存access_token
private final Object lock = new Object(); // 并发锁。多线程同时进来时,保证只有一个线程真的去请求微信获取新 token,其它线程等它拿完再用,避免同时拉多个新 token
public String getToken() {
long now = System.currentTimeMillis();
if (token != null && now < expireAtMs - 300_000) { // 提前5分钟刷新
return token;
}
synchronized (lock) {
// double-check 可能存在你在等待锁的时候,别的线程拿到锁,刷新了 token,然后你又拿到锁,发现 token 没有过期,直接返回了
now = System.currentTimeMillis();
if (token != null && now < expireAtMs - 300_000) return token;
// 拿Token Map大概长这个样
// {
// "access_token": "XXXXXXXX",
// "expires_in": XXXXX
// }
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
+ "&appid=" + props.getAppid()
+ "&secret=" + props.getSecret();
@SuppressWarnings("unchecked")
Map<String, Object> resp = (Map<String, Object>) rt.getForObject(url, Map.class);
// 校验 + 更新缓存
if (resp == null || resp.get("access_token") == null) {
throw new RuntimeException("获取access_token失败: " + resp);
}
token = String.valueOf(resp.get("access_token"));
// 拿到的 token 过期时间,单位是秒,这里转为毫秒
Object expiresObj = resp.getOrDefault("expires_in", 7200);
long expiresInSec = (expiresObj instanceof Number)
? ((Number) expiresObj).longValue()
: Long.parseLong(String.valueOf(expiresObj));
expireAtMs = System.currentTimeMillis() + expiresInSec * 1000L;
return token;
}
}
}
@Service
@RequiredArgsConstructor
public class WxMiniApi {
private final WxProps props;
private final WxAccessTokenManager tokenManager;
private final RestTemplate rt = new RestTemplate();
// 小程序端 wx.login() 会给一个 js_code 后端拿到这个 code 后调用微信的 jscode2session 接口
public Code2SessionResp code2Session(String jsCode) {
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + props.getAppid()
+ "&secret=" + props.getSecret()
+ "&js_code=" + jsCode
+ "&grant_type=authorization_code";
// 调微信接口
Map<?, ?> resp = rt.getForObject(url, Map.class);
// 判断结果里有没有 openid,没有就抛错
if (resp == null) throw new RuntimeException("code2Session失败: null");
if (resp.get("openid") == null) {
throw new RuntimeException("code2Session失败: " + resp);
}
// 把 Map 里的字段装进 Code2SessionResp 这个 DTO 返回
Code2SessionResp out = new Code2SessionResp();
out.setOpenid(String.valueOf(resp.get("openid")));
out.setSessionKey(String.valueOf(resp.get("session_key")));
out.setUnionid(resp.get("unionid") == null ? null : String.valueOf(resp.get("unionid")));
return out;
}
// 发送订阅消息
public Map<?, ?> sendSubscribe(SendSubscribeReq req) {
// 拿Token
String accessToken = tokenManager.getToken();
// 拼发送接口 URL
String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken;
// 微信要求的 JSON 结构
Map<String, Object> body = Map.of(
"touser", req.getTouser(),
"template_id", req.getTemplateId(),
"page", req.getPage(),
"lang", "zh_CN",
"miniprogram_state", "formal",
"data", req.getData()
);
// 把微信返回的 Map 直接返回给调用方
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<Map> resp = rt.exchange(url, HttpMethod.POST, new HttpEntity<>(body, headers), Map.class);
return resp.getBody();
}
@Data
public static class Code2SessionResp {
private String openid; // 用户在你这个小程序下的唯一标识
private String sessionKey; // 会话密钥(你如果要解密手机号/用户信息会用到)
private String unionid; // 可选(需要 unionid 能力时才会返回)
}
@Data
public static class SendSubscribeReq {
private String touser; // openid
private String templateId; // 模板id
private String page; // 点击消息跳转的小程序页,可空
private Map<String, Object> data; // 形如:{ thing1: {value:"xxx"}, time2:{value:"2025-12-05 10:00"} }
}
}
对应的Controller
@RestController
@RequestMapping("/wx")
@RequiredArgsConstructor
public class WxSubscribeController {
private final WxMiniApi wxMiniApi;
@PostMapping("/code2session")
public WxMiniApi.Code2SessionResp code2Session(@RequestBody CodeReq req) {
return wxMiniApi.code2Session(req.getJsCode());
}
@PostMapping("/subscribe/send")
public Map<?, ?> send(@RequestBody WxMiniApi.SendSubscribeReq req) {
return wxMiniApi.sendSubscribe(req);
}
@Data
public static class CodeReq {
private String jsCode;
}
}
浙公网安备 33010602011771号