服务号模板消息能力的设计初衷,旨在帮助开发者实现及时通知,但存在一些问题,如: 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 拼接要方便多了