使用模板消息发送消息给多个用户
一、需求背景
基于微信的通知渠道,微信小程序为开发者提供了可以高效触达用户的模板消息能力,在用户本人与小程序页面有交互行为后触发,通过微信聊天列表中的服务通知可快捷进入查看消息,点击查看详情还能跳转到下发消息的小程序的指定页面。
微信小程序允许下发模板消息的条件分为两类:支付或者提交表单。通过提交表单来下发模板消息的限制为“允许开发者向用户在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
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
/**
* 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
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
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