微信公众平台的ACCESS_TOKEN和模板消息

最近项目用到了微信公众平台的模板消息,发现实现过程并不是一帆风顺的,所以这里做一下笔记。

阅读微信公众平台技术文档 相关章节之后,了解到要实现模板消息,与实际开发相关的有以下几点: (具体还是查看平台技术文档)

1.获取ACCESS_TOKEN
关于access_token,有几点需要说明:
1.access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token.
2.目前微信公众平台提供了获取access_token的接口,接口同时会返回access_token的有效期,目前为7200s;
3.重复调用会导致上次获取的access_token失效,但是为了保证客户端的平滑过渡,微信公众平台会保证老的access_token会有5分钟的存活期。
详见 获取access_token

2.获取模板列表
得到access_token之后获取模板列表就相当简单了,直接rest接口调用即可得到模板列表。
注意,模板消息接口中有提示模板参数的格式:{{xxx.DATA}},千万注意这里括号之间是不能有空格的!文档中的Demo中带了空格,误导人了....

http请求方式:GET
https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=ACCESS_TOKEN

3.发送模板消息
发送模板消息也比较简单,选择一个模板发送给指定的用户(open_id)

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
请求信息都在body中

4. 测试
平台提供了测试号,接口测试号申请
开发需要申请测试号,获得appID和appsecret,然后配置测试模板等.

代码实现

代码实现部分其实主要关注的是access_token的获取逻辑.
考虑到access_token的特性,以及我们获取access_token的逻辑所在中心是分布式部署的,所以我这边获取access_token的逻辑如下所述:
1.优先从redis中获取;
2.redis中不存在,则控制一个线程X去调微信接口查询access_token并存入redis中;
3.其他线程,如果老的access_token可用则直接使用老的access_token;如果的老的access_token不可用,则等待线程X获取access_token之后的通知即可。

具体可以看下面代码,注释写的很详细了...
代码放到 github 上了...不对的地方还请指正。

/**
 *  获取微信的AccessToken
 *
 *  微信提供了一个rest接口,根据appid和secret 更新并返回 AccessToken;
 *  微信的这个AccessToken有几点需要注意:
 *  1.每次调用该接口,会返回新的AccessToken,老的AccessToken会有5分钟的存活期
 *  2.微信端该接口返回的AccessToken有效期目前为7200s
 * Created by xh on 2019/4/25.
 */
@Slf4j
public class WeChatAccessTokenUtil {

    private static RestTemplate restTemplate;
    private static WeChatProperties weChatProperties;
    private static RedissonClient redissonClient;

    private volatile static String accessToken;
    private volatile static boolean callFlag = true;
    private static CountDownLatch latch = new CountDownLatch(1);

    private static boolean initFlag = false;

    private static final String LOCK_KEY = "lock-AccessToken";
    private static final String ACCESSTOKEN = "ACCESSTOKEN";
    private static final String ACCESSTOKEN_LASTUPDATE = "ACCESSTOKEN_LASTUPDATE";

    static {
        restTemplate = SpringContext.getBean(RestTemplate.class);
        weChatProperties = SpringContext.getBean(WeChatProperties.class);
        redissonClient = SpringContext.getBean(RedissonClient.class);
    }

    /**
     *  获取AccessToken
     * @return String
     */
    public static String getAccessToken() throws Exception {
        log.info("WeChatAccessTokenUtil.getAccessToken start");
        //优先从redis中获取
        RBucket accessTokenCache = redissonClient.getBucket(ACCESSTOKEN);
        //redis中存在,返回redis中的ACCESS_TOKEN;
        // 同时如果accessToken未初始化,则将redis中的ACCESS_TOKEN值写入共享变量accessToken  这个不需要考虑并发问题,重复设置也没事
        if (accessTokenCache != null && !StringUtils.isEmpty(accessTokenCache.get())) {
            if (!initFlag) {
                accessToken = accessTokenCache.get();
                initFlag = true;
            }
            return accessTokenCache.get();
        }
        //redis中不存在,那么就需要让一个线程A去调用微信接口查询accessToken并刷入redis;
        //其他线程使用老的accessToken(即共享变量accessToken),如果存在的话;  如果老的accessToken不存在则等待线程A的通知;
        //老的accessToken有5分钟的存活期,所以这里使用一个缓存key并设置失效时间来控制老的accessToken是否可用,具体方式是:
        //在将accessToken刷入redis时,同时刷入另一个key:ACCESSTOKEN_LASTUPDATE,并控制失效时间比accessToken多五分钟,当缓存失效时,我们判断缓存ACCESSTOKEN_LASTUPDATE是否存在,如果不存在则表示老的accessToken失效不可用了,这时候清空共享变量accessToken.
        else {
            Lock lock = redissonClient.getLock(LOCK_KEY);

            //所有线程循环尝试获取分布式锁,只有一个线程X 会获得锁,获得锁的线程X 首先设置计数器latch为1,然后判断是否存在缓存ACCESSTOKEN_LASTUPDATE,不存在表示老的accessToken已经过了5分钟的存活期,那么就清空共享变量accessToken;
            //然后线程X 设置共享变量callFlag = false,那么其他线程会退出while循环;
            //对于线程X,因为要考虑分布式的场景,所以首选再次去redis中查询accessToken,查询到则更新共享变量accessToken;查询不到则调rest接口获取accessToken;
            //对于其他退出循环的线程,如果共享变量accessToken有值,表示还在存活期内,则使用老的accessToken返回给业务使用;如果accessToken为空,则需要等待线程X 的通知;
            boolean innerFlag = true;  //线程私有的变量, 获得锁的线程通过修改这个标志退出循环
            //callFlag 线程共享的变量,用于当一个线程获取锁时,通知其他线程跳出循环
            while (innerFlag && callFlag) {
                if (lock.tryLock()) {  //默认30000ms
                    try {
                        latch = new CountDownLatch(1);

                        //判断老的accessToken是否可用
                        if (redissonClient.getBucket(ACCESSTOKEN_LASTUPDATE).get() == null) {
                            accessToken = null;
                        }
                        callFlag = false;

                        //获取锁之后,首先查询redis ,如果redis中存在则不再需要调用微信接口了  这里是考虑分布式的场景
                        accessTokenCache = redissonClient.getBucket(ACCESSTOKEN);
                        if (accessTokenCache != null && !StringUtils.isEmpty(accessTokenCache.get())) {
                            accessToken = accessTokenCache.get();
                        }
                        else {
                            //调用微信的接口查询ACCESS_TOKEN
                            WeChatAccessTokenResp accessTokenResp =  getAccessTokenFromWechat();
                            accessToken = accessTokenResp.getAccessToken();
                            Long expire = accessTokenResp.getExpiresIn();
                            if (expire > 200) {
                                expire -= 200;
                            }

                            //批量更新缓存
                            RBatch batch = redissonClient.createBatch();
                            batch.getBucket(ACCESSTOKEN).setAsync(accessToken, expire, TimeUnit.SECONDS);
                            batch.getBucket(ACCESSTOKEN_LASTUPDATE).setAsync(System.currentTimeMillis(), expire + 300, TimeUnit.SECONDS);
                            batch.execute();
                        }
                    }
                    finally {
                        //防止因为网络等问题导致失败,无法通知其他线程 所以这里放在finally块里
                        //共享变量accessToken已经设置新值为可用的accessToken,通知其他线程
                        latch.countDown();
                        innerFlag = false;
                        //还原
                        callFlag = true;
                        lock.unlock();
                    }
                }
            }

            if (StringUtils.isEmpty(accessToken)) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        log.info("WeChatAccessTokenUtil.getAccessToken end");
        return accessToken;
    }


    public static WeChatAccessTokenResp getAccessTokenFromWechat() throws Exception {
    //调rest接口查询AccessToken,这里就不展示了
    }
}

测试

关于代码中提供的2个rest接口,这里也做了测试:

测试1:获取微信模板消息接口

可以看到返回了我在测试账号中配置的模板消息

[email protected][/xxx/xxx]$curl http://10.45.18.85:8080/luoluocaihong/wechat/template -X GET -H 'Content-Type:application/json'
[{"templateId":"NTGqIwifErpioNS1m5bX6M1DtdQAusj0q4bZMFBmRw8","title":"物流模板","primaryIndustry":"","deputyIndustry":"","content":"物流状态:{{state.DATA}}\\n\\n发货时间: {{deliverTime.DATA}}","example":""},{"templateId":"0RywEuCbkh9tMlaZyaCxyYE2uIrjxMlZYAaF4cODLEs","title":"Test","primaryIndustry":"","deputyIndustry":"","content":"{{result.DATA}}\\n\\n领奖金额:{{withdrawMoney.DATA}}\\n领奖 时间: {{withdrawTime.DATA}}\\n银行信息:{{cardInfo.DATA}}\\n到账时间: {{arrivedTime.DATA}}\\n{{remark.DATA}}","example":""},{"templateId":"NfcHMyxMr3hPTRmDFa8cCRtkKYkPoAOFGd5SmO3d-RA","title":"Hello","primaryIndustry":"","deputyIndustry":"","content":"您好,{{name.DATA}}","example":""}][email protected][/xxx/xxx]$
测试账号配置的模板消息.png
测试2:发送具体的模板消息

Demo中我是直接写死了发送的消息格式的,实际项目中是解析存入表中的
然后可以看到微信测试公众号也将消息推送给我了

[email protected][/xxx/xxx]$curl http://10.45.18.85:8080/luoluocaihong/wechat/send -X POST -H 'Content-Type:application/json'
[email protected][/xxx/xxx]$
微信测试公众号给我推送的消息.png

你可能感兴趣的:(微信公众平台的ACCESS_TOKEN和模板消息)