服务号模板消息能力的设计初衷,旨在帮助开发者实现及时通知,但存在一些问题,如: 1. 部分开发者在用户无预期的情况下,发送与用户无关的信息,对用户造成了骚扰。 2. 模板消息是用户触发后的通知消息,不支持营销类消息,不能满足部分业务需求。 为提升微信用户体验,我们开始灰度测试服务号订阅通知功能。
https://developers.weixin.qq.com/community/minihome/doc/000a4e1df800d82acb9b7fb5e5b001
虽然很不愿意用这个订阅功能,但是没办法。。。腾讯的地盘听他的。。。
生活还得过得去,码云SDK现在补上了, 但是没有合并到主分支,因为对腾讯还抱有一丝希望。。。
代码在订阅通知分支:https://gitee.com/jfinal/jfinal-weixin/tree/dev-SubscribeNotices/
有需要的朋友可以先进行:服务号订阅通知灰度测试
具体用法检出项目后:安装在本地就可以了:)
安装到本地成功后,可以在本地库中看见这些jar
项目中引用:
<dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal-weixin</artifactId> <version>3.1</version> </dependency>
PS 我或许应该把SDK的版本号命名为 3.1-SubscribeNotices,如果有需要的人可以先改jfinal weixin的pom.xml再进行安装本地
接口测试类:
package com.jfinal.weixin.sdk.api; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.jfinal.weixin.sdk.msg.InMsgParser; import com.jfinal.weixin.sdk.msg.in.InMsg; import org.junit.Test; import java.util.Arrays; import java.util.List; /** * 测试 订阅通知 API * com.jfinal.weixin.sdk.api.SubscribeNoticesApiTest */ public class SubscribeNoticesApiTest { public SubscribeNoticesApiTest(){ AccessTokenApiTest.init(); } @Test public void getCategory(){ //需要非测试号,先在微信公众号后台设置 行业类目 才能获取到数据 //获取公众号类目 ApiResult result = SubscribeNoticesApi.getCategory(); System.out.println(result); JSONArray data = JSON.parseObject(result.getJson()).getJSONArray("data"); for (int i = 0; i < data.size(); i++) { JSONObject d = data.getJSONObject(i); System.out.print("\n|-" + d.getString("name") + "\t:"); //获取类目下的公共模板 getPubTemplateTitles(d.getString("id")); } } private void getPubTemplateTitles(String ids){ //获取类目下的公共模板 ApiResult result = SubscribeNoticesApi.getPubTemplateTitles(ids, 0, 10); System.out.print(result); JSONArray data = JSON.parseObject(result.getJson()).getJSONArray("data"); for (int i = 0; i < data.size(); i++) { JSONObject d = data.getJSONObject(i); System.out.print("\n|--" + d.getString("title") + "\t:"); //获取类目下的公共模板 getPubTemplateKeywords(d.getString("tid")); } } private void getPubTemplateKeywords(String tid) { //获取公共模板下的关键词列表 ApiResult result = SubscribeNoticesApi.getPubTemplateKeywords(tid); System.out.println(result); } @Test public void addTemplate(){ //{"categoryId":"413","tid":492,"title":"预约通知","type":2} String tid = "492"; //获取公共模板下的关键词列表 // getPubTemplateKeywords(tid); //姓名、电话、预约时间、企业名称、预约状态、备注 List<Integer> kidList = Arrays.asList(1,3,5,20,6); String sceneDesc = "用户预约结果状态通知提醒"; //从公共模板库中选用模板,到私有模板库中 ApiResult result = SubscribeNoticesApi.addTemplate(tid, kidList, sceneDesc); System.out.println(result); } @Test public void getTemplate(){ //获取私有模板列表 ApiResult result = SubscribeNoticesApi.getTemplate(); System.out.println(result); } @Test public void delTemplate(){ //priTmplId 来自方法 getTemplate() String priTmplId = "xztiP-um1dD2uuV09EJZaXQLCox6mSxzG4Y-uRhVkg8"; ApiResult result = SubscribeNoticesApi.delTemplate(priTmplId); System.out.println(result); } @Test public void send(){ //TemplateData.New().build() String jsonStr = "{\n" + " \"touser\": \"o1aqAwX1Jwc4zS7q1UK4ZYFxxpNw\",\n" + " \"template_id\": \"xztiP-um1dD2uuV09EJZaXQLCox6mSxzG4Y-uRhVkg8\",\n" + " \"data\": {\n" + " \"date1\": {\n" + " \"value\": \"2019年8月8日18 15:30\"\n" + " },\n" + " \"thing2\": {\n" + " \"value\": \"计算机课\"\n" + " },\n" + " \"name3\": {\n" + " \"value\": \"杜同学\"\n" + " },\n" + " \"thing4\": {\n" + " \"value\": \"北京市青龙寺四号小龙禅厅\"\n" + " }\n" + " }\n" + "}"; ApiResult result = SubscribeNoticesApi.send(jsonStr); System.out.println(result); } @Test public void xmlInSubscribeMsgPopupEvent(){ //模拟订阅通知回调事件 String xml = "<xml>\n" + " <ToUserName><![CDATA[gh_123456789abc]]></ToUserName>\n" + " <FromUserName><![CDATA[otFpruAK8D-E6EfStSYonYSBZ8_4]]></FromUserName>\n" + " <CreateTime>1610969440</CreateTime>\n" + " <MsgType><![CDATA[event]]></MsgType>\n" + " <Event><![CDATA[subscribe_msg_popup_event]]></Event>\n" + " <SubscribeMsgPopupEvent>\n" + " <List>\n" + " <TemplateId><![CDATA[VRR0UEO9VJOLs0MHlU0OilqX6MVFDwH3_3gz3Oc0NIc]]></TemplateId>\n" + " <SubscribeStatusString><![CDATA[accept]]></SubscribeStatusString>\n" + " <PopupScene>2</PopupScene>\n" + " </List>\n" + " <List>\n" + " <TemplateId><![CDATA[9nLIlbOQZC5Y89AZteFEux3WCXRRRG5Wfzkpssu4bLI]]></TemplateId>\n" + " <SubscribeStatusString><![CDATA[reject]]></SubscribeStatusString>\n" + " <PopupScene>2</PopupScene>\n" + " </List>\n" + " </SubscribeMsgPopupEvent>\n" + "</xml>"; InMsg msg = InMsgParser.parse(xml); System.out.println(msg); } }
腾讯回调通知事件的例子代码:
WeixinMsgController.java中增加方法:
/** * 用户操作订阅通知弹窗(取消|允许) || 处理接收到的订阅通知是否送达成功通知事件 * @param inSubscribeNoticesEvent */ @Override protected void processInSubscribeNoticesEvent(InSubscribeNoticesEvent inSubscribeNoticesEvent) { log.debug("测试方法:processInSubscribeNoticesEvent(); inSubscribeNoticesEvent=" + inSubscribeNoticesEvent); if(inSubscribeNoticesEvent.isEventByMsgSent()){ log.debug("发送订阅通知后的异步结果"); } else { log.debug("用户操作订阅通知弹窗事件"); } log.debug("订阅用户的OpenID=" + inSubscribeNoticesEvent.getFromUserName()); renderNull(); }
事件对象中包含一个list集合子类private List<Item> list; 里面参数有:(方便信息入库)
/** * 订阅通知的参数 */ public static class Item implements Serializable { // -------------------------------------------- // 订阅和取消订阅 //模板 id private String templateId; //用户点击行为(同意、取消发送通知) accept=用户点击“同意”; reject=用户点击“取消” private String subscribeStatusString; //场景 1=弹窗来自H5页面;2=弹窗来自图文消息;null=用户拒收通知; private String popupScene; // -------------------------------------------- // 发送订阅通知 推送的结果; // 场景:调用 bizsend 接口发送通知; // *失败仅包含因异步推送导致的系统失败 //推送消息的id private String msgID; //推送结果状态码(0表示成功) private String errorCode; //推送结果状态码文字含义 private String errorStatus;
H5的订阅例子代码:
WeixinApiController.java中增加方法:
/** * 获取 订阅通知 H5页面 * /api/subscribe */ public void subscribe() { JsTicket ticket = JsTicketApi.getTicket(JsTicketApi.JsApiType.jsapi); String nonceStr = RandomKit.genNonceStr(); ParaMap paraMap = ParaMap.create(); paraMap.put("noncestr", nonceStr); paraMap.put("jsapi_ticket", ticket.getTicket()); paraMap.put("timestamp", ticket.getExpiredTime().toString()); //注意修改域名,不然签名错误 paraMap.put("url", "https://你的域名/api/subscribe"); String str = PaymentKit.packageSign(paraMap.getData(), false); String signature = HashKit.sha1(str); set("d", Kv.by("appId", PropKit.get("appId"))// .set("timestamp", ticket.getExpiredTime())// .set("signature", signature)// .set("nonceStr", nonceStr)// //看自己申请的模板ID是多少 参考代码:com.jfinal.weixin.sdk.api.SubscribeNoticesApiTest.java .set("templateId", "xztiP-um1dD2uuV09EJZaXQLCox6mSxzG4Y-uRhVkg8")// ); renderTemplate("/weixin/subscribe.html"); } /** * 发送订阅消息 * /api/subscribeSend?openID=o1aqAwX1Jwc4zS7q1UK4ZYFxxpNw */ public void subscribeSend(){ String openID = get("openID", getAttr("openID")); renderJson(sendFn(openID)); } private ApiResult sendFn(String openID) { String jsonStr = "{\n" + " \"touser\": \"" + openID + "\",\n" + " \"template_id\": \"xztiP-um1dD2uuV09EJZaXQLCox6mSxzG4Y-uRhVkg8\",\n" + " \"data\": {\n" + " \"date1\": {\n" + " \"value\": \"2019年8月8日 15:30\"\n" + " },\n" + " \"thing2\": {\n" + " \"value\": \"计算机课\"\n" + " },\n" + " \"name3\": {\n" + " \"value\": \"杜同学\"\n" + " },\n" + " \"thing4\": {\n" + " \"value\": \"西安市青龙寺四号小龙禅厅\"\n" + " }\n" + " }\n" + "}"; ApiResult result = SubscribeNoticesApi.send(jsonStr); System.out.println(result); return result; } /** * 全部粉丝 发送订阅消息 * /api/subscribeSendAll */ public void subscribeSendAll(){ ApiResult ar = UserApi.getFollows(); Map map = ar.getMap("data"); List list = (List) map.get("openid"); Kv kv = Kv.create(); for (Object openID : list) { System.out.println(openID); kv.set(openID, sendFn(openID.toString())); } renderJson(kv); }
页面:/weixin/subscribe.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>wx-open-subscribe</title> <!-- 手机模式界面显示 --> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 禁止页面缓存 --> <meta http-equiv=Cache-Control content=no-cache /> </head> <body> <div style="text-align: center;"> <h1>订阅通知</h1> <wx-open-subscribe template="#(d.templateId)" id="subscribe-btn"> <template slot="style"> <style> .subscribe-btn { color: #fff; background-color: #07c160; } </style> </template> <template> <button class="subscribe-btn"> 一次性模版消息订阅 </button> </template> </wx-open-subscribe> </div> <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <script src="/assets/js/jquery-1.4.4.min.js"></script> <script> //开发手册: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html#11 wx.ready(function(){ console.log('ready'); // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 var btn = $('#subscribe-btn') btn.addEventListener('success', function (e) { console.log('success', e.detail); }); btn.addEventListener('error',function (e) { console.log('fail', e.detail); }); console.log('btn=' + btn); }); wx.error(function(res){ console.log('error', res); // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); // wx.checkJsApi({ // jsApiList: ['chooseImage'], // 需要检测的JS接口列表,所有JS接口列表见附录2, // success: function(res) { // console.log('checkJsApi', res); // // 以键值对的形式返回,可用的api值true,不可用为false // // 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"} // } // }); wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '#(d.appId)', // 必填,公众号的唯一标识 timestamp: '#(d.timestamp)', // 必填,生成签名的时间戳 nonceStr: '#(d.nonceStr)', // 必填,生成签名的随机串 signature: '#(d.signature)',// 必填,签名 jsApiList: [], // 必填,需要使用的JS接口列表 openTagList: ['wx-open-subscribe']// 订阅通知必填,需要使用的开放标签列表 }); </script> </body> </html>
需要仔细阅读开发手册,配置蛮多的,,我在这个位置坑了好一会儿。。。
好了,启动项目,关注公众号后 在手机上演示效果:
请求测试地址 :域名/api/subscribe
注意域名签名的代码,HTTPS和HTTP是有区别的,如果不一样也不会成功。
以及可以在微信公众号后台的素材中编辑文章中插入 订阅通知 :
效果图:
注意观察控制台输出的 用户openID 用于下面模拟请求挂参发送消息
域名/api/subscribeSend?openID=
发送成功后的效果图:
这个和小程序的订阅通知是差不多的(模板管理都是一个地址。。。),多出来h5和回调通知配置。
好了分享到这里结束,如果大家业务上对模板消息有依赖,需要尽快换上这个功能,不知道腾讯下一步想干啥。。。
有参考价值的话点个赞吧~ 星期五的快乐消失了。。。
难道服务号模板消息这个功能会被干掉?
此外,本分享中生成 xml 、json 可以用 enjoy,比 String 拼接要方便多了