很多开发者,在初次使用微信公众平台的模板消息功能之际,皆会遇到意想不到的“坑”,于那看似简单的流程当中 。
模板消息的价值与挑战
公众号借助模板消息可向用户发送诸如订单状态、预约提醒等之类的服务通知,这提升了服务效率以及用户体验,可是,从获取凭据一直到最终发送,每一个环节都存在需要留意的技术细节,稍有差错就会致使消息发送失败或者接口调用出现异常,从而给项目开发增添额外的时间成本。
深入理解Access_Token
拥有调用全部微信高级接口功能的钥匙是Access_Token,其有效期时长规定为7200秒。它是通过AppID以及AppSecret进行换取的。存在一个关键限制情况,即重复获取就会即刻致使旧Token失效,不过微信会针对旧Token留存大约5分钟的缓冲期。这就代表着在分布式系统当中,必须要设计出一套集中且高效的Token管理机制,以此来防止多个服务实例同时刷新进而造成Token频繁失效 。
获取与配置消息模板
开发者于公众号后台之上的模板消息库里头,能够选用那行业模板或者去申请那新模板。获取模板列表的接口调用虽说简单,然而务必严格依照参数格式。模板变量格式是{{xxx.DATA}},括号以内不能存在空格。早期时期微信官方文档那儿的示例代码曾含有空格,此情况误导了好多开发者 ,在进行实际编码时要格外仔细核对。
发送消息的具体步骤
采用POST请求来发送那所谓的模板消息,在请求体里要带着负责接收方面的OpenID、还有模板ID以及具体呈现的数据内容。数据内容呢,它是个属于JSON的对象,并且它要和模板当中事先定义好的变量逐一分别对应起来哟。除此以外呀,能进行跳转,至于跳转的是指定好的页面或者小程序呢。发送达成成功的状态之后呀,那些用户就会在微信对话列表当中收到这一条对应的服务通知啦。
充分利用测试号进行开发
/**
* 获取微信的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,这里就不展示了
}
}
微信给出了公众平台测试账号系统,开发者不用去申请正式公众号就能体验全部接口。于测试账号管理页面那儿,能够配置自身的模板消息,还能获到专用的AppID以及AppSecret。全部开发调试工作都应当在测试环境里完成,等流程彻底跑通之后再迁移至正式环境,这是规避风险的最佳实践。
设计稳健的Token管理方案
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]$
从Token的有效性以及分布式部署方面加以考量,一种较为常见的方案是运用Redis这类缓存中间件。程序读取Token之时会优先从缓存这里获取;要是失效了的话,借助互斥锁机制让单个实例前往微信服务器去更新,并且存入缓存那里设定过期时间。其他并发的请求能够短暂使用旧的Token或者等待新的Token生成。这对服务的稳定以及高可用起到了保障作用。
面对微信模板消息功能的实现,你碰到的最难搞的问题是什么呢,其是怎样被你成功攻克的呀?欢迎于评论地方分享出你的经验哟,要是认为这篇内容有帮助的话,请进行点赞或者分享给更多的开发者好啦。
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]$