微信公众平台的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<String> 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:获取微信模板消息接口

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

user@CentOS7.3[/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":""}]user@CentOS7.3[/xxx/xxx]$
测试账号配置的模板消息.png
测试2:发送具体的模板消息

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

user@CentOS7.3[/xxx/xxx]$curl http://10.45.18.85:8080/luoluocaihong/wechat/send -X POST -H 'Content-Type:application/json'
781640493582254081user@CentOS7.3[/xxx/xxx]$
微信测试公众号给我推送的消息.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容