最近项目用到了微信公众平台的模板消息,发现实现过程并不是一帆风顺的,所以这里做一下笔记。
阅读微信公众平台技术文档 相关章节之后,了解到要实现模板消息,与实际开发相关的有以下几点: (具体还是查看平台技术文档)
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]$
测试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]$