LuckyOx

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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;
}

}

posted on 2025-12-05 15:58  lucky_ox  阅读(0)  评论(0)    收藏  举报