最近項(xiàng)目用到了微信公眾平臺(tái)的模板消息痕届,發(fā)現(xiàn)實(shí)現(xiàn)過程并不是一帆風(fēng)順的级乍,所以這里做一下筆記。
閱讀微信公眾平臺(tái)技術(shù)文檔 相關(guān)章節(jié)之后竣贪,了解到要實(shí)現(xiàn)模板消息军洼,與實(shí)際開發(fā)相關(guān)的有以下幾點(diǎn): (具體還是查看平臺(tái)技術(shù)文檔)
1.獲取ACCESS_TOKEN
關(guān)于access_token巩螃,有幾點(diǎn)需要說明:
1.access_token是公眾號(hào)的全局唯一接口調(diào)用憑據(jù),公眾號(hào)調(diào)用各接口時(shí)都需使用access_token.
2.目前微信公眾平臺(tái)提供了獲取access_token的接口,接口同時(shí)會(huì)返回access_token的有效期,目前為7200s;
3.重復(fù)調(diào)用會(huì)導(dǎo)致上次獲取的access_token失效,但是為了保證客戶端的平滑過渡,微信公眾平臺(tái)會(huì)保證老的access_token會(huì)有5分鐘的存活期匕争。
詳見 獲取access_token
2.獲取模板列表
得到access_token之后獲取模板列表就相當(dāng)簡(jiǎn)單了避乏,直接rest接口調(diào)用即可得到模板列表。
注意,模板消息接口中有提示模板參數(shù)的格式:{{xxx.DATA}}
汗捡,千萬(wàn)注意這里括號(hào)之間是不能有空格的淑际!文檔中的Demo中帶了空格,誤導(dǎo)人了....
http請(qǐng)求方式:GET
https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=ACCESS_TOKEN
3.發(fā)送模板消息
發(fā)送模板消息也比較簡(jiǎn)單,選擇一個(gè)模板發(fā)送給指定的用戶(open_id)
http請(qǐng)求方式: POST
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
請(qǐng)求信息都在body中
4. 測(cè)試
平臺(tái)提供了測(cè)試號(hào)扇住,接口測(cè)試號(hào)申請(qǐng)
開發(fā)需要申請(qǐng)測(cè)試號(hào),獲得appID和appsecret,然后配置測(cè)試模板等.
代碼實(shí)現(xiàn)
代碼實(shí)現(xiàn)部分其實(shí)主要關(guān)注的是access_token的獲取邏輯.
考慮到access_token的特性春缕,以及我們獲取access_token的邏輯所在中心是分布式部署的,所以我這邊獲取access_token的邏輯如下所述:
1.優(yōu)先從redis中獲取艘蹋;
2.redis中不存在,則控制一個(gè)線程X去調(diào)微信接口查詢access_token并存入redis中锄贼;
3.其他線程,如果老的access_token可用則直接使用老的access_token;如果的老的access_token不可用,則等待線程X獲取access_token之后的通知即可女阀。
具體可以看下面代碼宅荤,注釋寫的很詳細(xì)了...
代碼放到 github 上了...不對(duì)的地方還請(qǐng)指正。
/**
* 獲取微信的AccessToken
*
* 微信提供了一個(gè)rest接口浸策,根據(jù)appid和secret 更新并返回 AccessToken冯键;
* 微信的這個(gè)AccessToken有幾點(diǎn)需要注意:
* 1.每次調(diào)用該接口,會(huì)返回新的AccessToken庸汗,老的AccessToken會(huì)有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");
//優(yōu)先從redis中獲取
RBucket<String> accessTokenCache = redissonClient.getBucket(ACCESSTOKEN);
//redis中存在惫确,返回redis中的ACCESS_TOKEN;
// 同時(shí)如果accessToken未初始化蚯舱,則將redis中的ACCESS_TOKEN值寫入共享變量accessToken 這個(gè)不需要考慮并發(fā)問題改化,重復(fù)設(shè)置也沒事
if (accessTokenCache != null && !StringUtils.isEmpty(accessTokenCache.get())) {
if (!initFlag) {
accessToken = accessTokenCache.get();
initFlag = true;
}
return accessTokenCache.get();
}
//redis中不存在,那么就需要讓一個(gè)線程A去調(diào)用微信接口查詢accessToken并刷入redis;
//其他線程使用老的accessToken(即共享變量accessToken),如果存在的話; 如果老的accessToken不存在則等待線程A的通知枉昏;
//老的accessToken有5分鐘的存活期陈肛,所以這里使用一個(gè)緩存key并設(shè)置失效時(shí)間來(lái)控制老的accessToken是否可用,具體方式是:
//在將accessToken刷入redis時(shí),同時(shí)刷入另一個(gè)key:ACCESSTOKEN_LASTUPDATE,并控制失效時(shí)間比accessToken多五分鐘兄裂,當(dāng)緩存失效時(shí)句旱,我們判斷緩存ACCESSTOKEN_LASTUPDATE是否存在,如果不存在則表示老的accessToken失效不可用了懦窘,這時(shí)候清空共享變量accessToken.
else {
Lock lock = redissonClient.getLock(LOCK_KEY);
//所有線程循環(huán)嘗試獲取分布式鎖,只有一個(gè)線程X 會(huì)獲得鎖前翎,獲得鎖的線程X 首先設(shè)置計(jì)數(shù)器latch為1,然后判斷是否存在緩存ACCESSTOKEN_LASTUPDATE畅涂,不存在表示老的accessToken已經(jīng)過了5分鐘的存活期港华,那么就清空共享變量accessToken;
//然后線程X 設(shè)置共享變量callFlag = false午衰,那么其他線程會(huì)退出while循環(huán)立宜;
//對(duì)于線程X冒萄,因?yàn)橐紤]分布式的場(chǎng)景,所以首選再次去redis中查詢accessToken橙数,查詢到則更新共享變量accessToken尊流;查詢不到則調(diào)rest接口獲取accessToken;
//對(duì)于其他退出循環(huán)的線程,如果共享變量accessToken有值灯帮,表示還在存活期內(nèi)崖技,則使用老的accessToken返回給業(yè)務(wù)使用;如果accessToken為空钟哥,則需要等待線程X 的通知迎献;
boolean innerFlag = true; //線程私有的變量, 獲得鎖的線程通過修改這個(gè)標(biāo)志退出循環(huán)
//callFlag 線程共享的變量,用于當(dāng)一個(gè)線程獲取鎖時(shí)腻贰,通知其他線程跳出循環(huán)
while (innerFlag && callFlag) {
if (lock.tryLock()) { //默認(rèn)30000ms
try {
latch = new CountDownLatch(1);
//判斷老的accessToken是否可用
if (redissonClient.getBucket(ACCESSTOKEN_LASTUPDATE).get() == null) {
accessToken = null;
}
callFlag = false;
//獲取鎖之后吁恍,首先查詢r(jià)edis ,如果redis中存在則不再需要調(diào)用微信接口了 這里是考慮分布式的場(chǎng)景
accessTokenCache = redissonClient.getBucket(ACCESSTOKEN);
if (accessTokenCache != null && !StringUtils.isEmpty(accessTokenCache.get())) {
accessToken = accessTokenCache.get();
}
else {
//調(diào)用微信的接口查詢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 {
//防止因?yàn)榫W(wǎng)絡(luò)等問題導(dǎo)致失敗播演,無(wú)法通知其他線程 所以這里放在finally塊里
//共享變量accessToken已經(jīng)設(shè)置新值為可用的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 {
//調(diào)rest接口查詢AccessToken,這里就不展示了
}
}
測(cè)試
關(guān)于代碼中提供的2個(gè)rest接口,這里也做了測(cè)試:
測(cè)試1:獲取微信模板消息接口
可以看到返回了我在測(cè)試賬號(hào)中配置的模板消息
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":"物流狀態(tài):{{state.DATA}}\\n\\n發(fā)貨時(shí)間: {{deliverTime.DATA}}","example":""},{"templateId":"0RywEuCbkh9tMlaZyaCxyYE2uIrjxMlZYAaF4cODLEs","title":"Test","primaryIndustry":"","deputyIndustry":"","content":"{{result.DATA}}\\n\\n領(lǐng)獎(jiǎng)金額:{{withdrawMoney.DATA}}\\n領(lǐng)獎(jiǎng) 時(shí)間: {{withdrawTime.DATA}}\\n銀行信息:{{cardInfo.DATA}}\\n到賬時(shí)間: {{arrivedTime.DATA}}\\n{{remark.DATA}}","example":""},{"templateId":"NfcHMyxMr3hPTRmDFa8cCRtkKYkPoAOFGd5SmO3d-RA","title":"Hello","primaryIndustry":"","deputyIndustry":"","content":"您好写烤,{{name.DATA}}","example":""}]user@CentOS7.3[/xxx/xxx]$
測(cè)試2:發(fā)送具體的模板消息
Demo中我是直接寫死了發(fā)送的消息格式的,實(shí)際項(xiàng)目中是解析存入表中的
然后可以看到微信測(cè)試公眾號(hào)也將消息推送給我了
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]$