JFinalWeixin使用技巧-为提升微信用户体验,服务号订阅通知功能上线:)

服务号模板消息能力的设计初衷,旨在帮助开发者实现及时通知,但存在一些问题,如:
1. 部分开发者在用户无预期的情况下,发送与用户无关的信息,对用户造成了骚扰。
2. 模板消息是用户触发后的通知消息,不支持营销类消息,不能满足部分业务需求。
为提升微信用户体验,我们开始灰度测试服务号订阅通知功能。

https://developers.weixin.qq.com/community/minihome/doc/000a4e1df800d82acb9b7fb5e5b001
虽然很不愿意用这个订阅功能,但是没办法。。。腾讯的地盘听他的。。。
生活还得过得去,码云SDK现在补上了, 但是没有合并到主分支,因为对腾讯还抱有一丝希望。。。
代码在订阅通知分支:https://gitee.com/jfinal/jfinal-weixin/tree/dev-SubscribeNotices/
有需要的朋友可以先进行:服务号订阅通知灰度测试
具体用法检出项目后:安装在本地就可以了:)
image.png

安装到本地成功后,可以在本地库中看见这些jar
image.png

项目中引用:

<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 

image.png

注意域名签名的代码,HTTPS和HTTP是有区别的,如果不一样也不会成功。

以及可以在微信公众号后台的素材中编辑文章中插入 订阅通知 :

image.png

效果图:
image.png


注意观察控制台输出的 用户openID 用于下面模拟请求挂参发送消息

域名/api/subscribeSend?openID=


发送成功后的效果图:
image.png

这个和小程序的订阅通知是差不多的(模板管理都是一个地址。。。),多出来h5和回调通知配置。

好了分享到这里结束,如果大家业务上对模板消息有依赖,需要尽快换上这个功能,不知道腾讯下一步想干啥。。。

image.png

有参考价值的话点个赞吧~ 星期五的快乐消失了。。。

评论区

JFinal

2021-03-12 21:39

这里有个大问题:
难道服务号模板消息这个功能会被干掉?

此外,本分享中生成 xml 、json 可以用 enjoy,比 String 拼接要方便多了

杜福忠

2021-03-12 23:51

@JFinal 是的大概率会像小程序一样干掉模板消息,目前他们计划是只对政务医疗开放。微信社区一片哀嚎谩骂。。。灰度测试截止4月30号。希望腾讯会好好重新计划一下吧。。。但是大概率可能不会改变,因为腾讯一向说改就改。。。我们有大量的模板消息业务,基本完犊子,已经在对接短信和邮件了。剩余部分业务可以用订阅通知替换

704442497

2021-03-18 09:42

模板消息多好的东西,说砍就砍,都用短信通知每年要花多少钱几千块