使用模板消息發送消息給多個用戶
一、需求背景
基於微信的通知渠道,微信小程序為開發者提供了可以高效觸達用戶的模板消息能力,在用戶本人與小程序頁面有交互行為后觸發,通過微信聊天列表中的服務通知可快捷進入查看消息,點擊查看詳情還能跳轉到下發消息的小程序的指定頁面。
微信小程序允許下發模板消息的條件分為兩類:支付或者提交表單。通過提交表單來下發模板消息的限制為“允許開發者向用戶在7天內推送有限條數的模板消息(1次提交表單可下發1條,多次提交下條數獨立,相互不影響)”。
然而,用戶1次觸發7天內推送1條通知是明顯不夠用的。比如,簽到功能利用模板消息的推送來提醒用戶每天簽到,只能在用戶前一天簽到的情況下,獲取一次推送模板消息的機會,然后用於第二天向該用戶發送簽到提醒。但是很多情況下,用戶在某一天忘記簽到,系統便失去了提醒用戶的權限,導致和用戶斷開了聯系;再比如,系統想主動告知用戶即將做某活動,然而由於微信小程序被動觸發通知的限制,系統將無法主動推送消息。
二、如何突破模板消息的推送限制?
為了突破模板消息的推送限制,實現7天內任性推送,只需收集到足夠的推送碼,即每次提交表單時獲取到的formId。一個formId代表着開發者有向當前用戶推送模板消息的一次權限。
三、實現
1.收集推送碼。獲取多個表單id並保存,便於一次性發送給服務器
// 收集推送碼
Page({
formSubmit: funcition(e) {//form表單按鈕點擊調用該方法
let formId = e.detail.formId; //獲取表單formId
this.collectFormIds(formId); //保存推送碼
let type = e.detail.target.dataset.type; // 根據type執行點擊事件
},
collectFormIds: function(formId) { //保存推送碼
let formIds = app.globalData.globalFormIds; // 獲取全局推送碼數組
if (!formIds)
formIds = [];
let data = {
formId: formId,
expire: new Data().getTime() + 60480000 // 7天后的過期時間戳
}
formIds.push(data);
app.globalData.globalFormIds = formIds;
},
})
2.上傳推送碼。即將推送碼發送給服務器然后保存起來(需要上傳的數據:formId和openId)
// 上報推送碼
Page({
onLoad: funcition(e) {
this.uploadFormIds(); //上傳推送碼
},
collectFormIds: function(formId) {
var formIds = app.globalData.globalFormIds; // 獲取全局推送碼
if (formIds.length) {
formIds = JSON.stringify(formIds); // 轉換成JSON字符串
app.globalData.gloabalFomIds = ''; // 清空當前全局推送碼
}
wx.request({ // 發送到服務器
url: 'http://xxx',
method: 'POST',
data: {
openId: 'openId',//openId為小程序的用戶唯一標識,需自行獲取
formIds: formIds//表單id
},
success: function(res) {
}
});
},
})
3.服務端-存儲推送碼(高頻IO,采用Redis來存儲推送碼。將推送碼保存到數據庫實現持久化存儲,由於formId有七天有效期,所以需要定時清理無效推送碼)
/**
* 收集用戶推送碼
*
* @param openId 用戶的openid
* @param formIds 用戶的formId
*/
public void collect(String openId, List<FormTemplateVO> formTemplates) {
redisTemplate.opsForList().rightPushAll(openId, formIds);
}
4.發送模板消息相關接口
相關接口詳細信息請查看官方文檔:https://developers.weixin.qq.com/miniprogram/dev/api-backend/
需要使用的接口:
1>模板消息發送api:POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
要想使用該接口發送模板消息,還需要獲取
ACCESS_TOKEN 即接口調用憑證
2>
接口調用憑證
api:
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
獲取接口調用憑證需要appid和secret
appid:小程序唯一標識secret:小程序唯一憑證密鑰
appid和secret獲取方式請查看:https://jingyan.baidu.com/article/8cdccae9221703315513cd6e.html

5.推送模板消息。發送消息給用戶
templateId消息模板id:自行登錄https://mp.weixin.qq.com/獲取
以下兩個方法只是部分代碼,調用sendTemplateMessage方法即可發送消息
/**
* 發送模板消息
*
* @param accessToken 接口調用憑證
* @param touser 接收者(用戶)的 openid
* @param templateId 所需下發的模板消息的id
* @param page 點擊模板卡片后的跳轉頁面,僅限本小程序內的頁面。支持帶參數,(示例index?foo=bar)。該字段不填則模板無跳轉
* @param formId 表單提交場景下,為 submit 事件帶上的 formId;支付場景下,為本次支付的 prepay_id
* @param data 模板內容,不填則下發空模板。具體格式請參考示例。
* @param emphasisKeyword 模板需要放大的關鍵詞,不填則默認無放大
*/
public WxResSendTemplateMessage sendTemplateMessage(String accessToken, String touser, String templateId, String page, String formId, Map<String, Object> data, String emphasisKeyword) {
/**
* data示例:
*
* "data": {
* "keyword1": { "value": title, "color": "#173177" },
* "keyword2": { "value": gettime() }
* }
* emphasisKeyword示例:
* "emphasis_keyword": "keyword1.DATA"
*
*/
/** 結果 */
String result = "";
/** 獲取輸出流 */
OutputStreamWriter os = null;
/** 獲取輸入流 */
BufferedReader in = null;
/** 請求地址 */
String urlPath = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=" + accessToken;
/** 需要傳遞的數據 */
Map<String, Object> messageData = new HashMap<>();
messageData.put("touser", touser);//接收者(用戶)的 openid
messageData.put("template_id", templateId);//所需下發的模板消息的id
messageData.put("page", page);//點擊模板卡片后的跳轉頁面,僅限本小程序內的頁面。支持帶參數,(示例index?foo=bar)。該字段不填則模板無跳轉
messageData.put("form_id", formId);//表單提交場景下,為 submit 事件帶上的 formId;支付場景下,為本次支付的 prepay_id
messageData.put("data", data);//模板內容,不填則下發空模板。具體格式請參考示例。
messageData.put("emphasis_keyword", emphasisKeyword);//模板需要放大的關鍵詞,不填則默認無放大
try {
/** 獲得url對象 */
URL url = new URL(urlPath);
/** 打開連接 */
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
/** 設置通用的請求屬性 */
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
/** 設置請求方式 */
conn.setRequestMethod(Http.HttpMethod.POST.name());
/** 設置允許讀寫出數據,默認可以讀不可寫 */
conn.setDoOutput(true);
conn.setDoInput(true);
/** 獲取輸出流 */
os = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
/** 發送請求參數 */
os.write(JsonMapper.toJsonString(messageData));
/** 輸出緩存流 */
os.flush();
/** 獲取輸入流 */
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
/** 接收行數據 */
String line;
/** 讀取數據 */
while((line = in.readLine()) != null) {
result += line;
}
} catch(Exception e) {
e.printStackTrace();
} finally {/** 釋放輸入輸出流並關閉 */
try {
if(os != null) {
os.close();
}
if(in != null) {
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
logger.error(">>>模板消息發送>>>result={}>>>", result);
return (WxResSendTemplateMessage) JsonMapper.fromJsonString(result, WxResSendTemplateMessage.class);
}
/**
* 獲取小程序全局唯一后台接口調用憑據(access_token)
*
* @param appId 小程序唯一憑證
* @param secret 小程序唯一憑證密鑰
* @return
* @throws BusinessException
*/
public WxResAccessToken getAccessToken(String appId, String secret) throws BusinessException {
String requestUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + secret;
String res = HttpUtils.get(requestUrl, HttpContentType.CONTENT_TYPE_FORM_DATA, null, null);
if (StringUtils.isBlank(res)) {
throw BusinessException.create("獲取信息失敗!");
}
try {
res = new String(res.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Map<String, Object> map = JsonUtils.unPackMap(res, String.class, Object.class);
if (map == null) {
throw BusinessException.create("獲取信息失敗!");
}
if (map.containsKey("errcode")) {
throw BusinessException.create(String.valueOf(map.get("errmsg")));
}
WxResAccessToken out = new WxResAccessToken();
/** 獲取到的憑證 */
out.setAccessToken(String.valueOf(map.get("access_token")));
/** 憑證有效時間,單位:秒。目前是7200秒之內的值 */
if(map.get("expires_in") != null) {
out.setExpiresIn(BaseUtils.getLong(String.valueOf(map.get("expires_in"))));
}else {
out.setExpiresIn(0L);
}
/** 錯誤碼 */
if(map.get("errcode") != null) {
out.setErrcode(BaseUtils.getLong(String.valueOf(map.get("errcode"))));
}else {
out.setErrcode(0L);
}
/** 錯誤信息 */
out.setErrmsg(String.valueOf(map.get("errmsg")));
return out;
}
注意:
openid、secret 、appid必須成套
模板消息內容的json字符串需嚴格按官方給出的格式
參考:
https://www.jianshu.com/p/e9641aabb051
https://blog.csdn.net/rolan1993/article/details/79398362
https://blog.csdn.net/xcrow/article/details/37731719