洛塔服务号回复010获取代码。
功能说明公众号订阅通知这个功能,微信本来打算替代掉模板消息和一次性订阅的,最后也没替代掉,成为单独的一个功能。 个人感觉和一次性订阅是没有太大区别的,只不过增加了一个长期订阅,但这个不是一般账号能申请下来的,所以整体来说使用也没有太大区别。
准备工作- 公众号后台设置ip白名单:推送给用户消息需要 位置:设置与开发–>基本配置,右侧IP白名单
- 启动服务器配置:接收订阅事件等推送消息需要 位置:设置与开发–>基本配置,右侧服务器配置 开启服务器配置需要将对应的url代码部署上,Java可以使用
/** * 完整项目源码可关注公众号"lootaayun"(洛塔),回复010获取 */ @GetMapping("wx10") public void wxGet(HttpServletRequest request, PrintWriter pw) { // 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息 String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); // 时间戳 String nonce = request.getParameter("nonce"); // 随机数 String echostr = request.getParameter("echostr"); // 随机字符串 // 将token、timestamp、nonce三个参数进行字典序排序 Listlist = new ArrayList(); list.add("lootaa"); // 公众号后台设置的token list.add(timestamp); list.add(nonce); Collections.sort(list); // 将三个参数字符串拼接成一个字符串进行sha1加密 String tokenStr = ""; for (int i = 0; i < list.size(); i++) { tokenStr += list.get(i); } String signatureStr = DigestUtils.sha1Hex(tokenStr); // 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 if (signature.equals(signatureStr)) { pw.write(echostr); // 原样返回echostr参数内容 } else { pw.write(""); } }手动添加模板
后台订阅通知部分可以选择对应的模板,是否选择参数以及参数顺序均可变更。有时候会有小坑,比如下图这个模板示例,如果原样写测试代码会失败,原因是thing的长度不能超过20个字符,但是demo样例里面显然已经超过了。
按照官方文档的说明,订阅有两种方式,一种是公众号图文消息直接插入订阅通知组件,一种是使用开放标签能力,在自己的网页中添加。开放标签能力这个需要熟悉前端开发,这个我不擅长,后面会单独写一篇关于开放标签的简单实现。图文消息插入的订阅通知组件在编辑页面的最上方位置。
事件推送和之前写的公众号接收事件推送的一致,只不过对应的Event值不同。再安全模式下,接收的代码为
@PostMapping("wx10") public void wxPost(HttpServletRequest request, HttpServletResponse response, PrintWriter pw) throws Exception { String token = "lootaa"; String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK"; String appid = "wx276049d6a7551dca"; WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String msgSignature = request.getParameter("msg_signature"); Document doc = getDocument(request); String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML()); System.out.println("解密后明文: " + result2); JSONObject resultJson = documentToJSONObject(result2); String messageType = resultJson.getString("MsgType"); //这个值如果是event表示是事件推送;如果是其他字符,参照Test004 if(Objects.equals("event", messageType)) { String event = resultJson.getString("Event"); //这个是事件的具体类型 if(Objects.equals(event, "subscribe_msg_popup_event")) { // 订阅消息 System.out.println("用户订阅后进入这里,后面会完善这一行代码"); pw.write(nonce); } else if(Objects.equals(event, "subscribe_msg_popup_event")) { // 发送订阅通知 System.out.println("发送订阅通知的事件"); pw.write(nonce); } else if(Objects.equals(event, "subscribe_msg_change_event")) { // 用户管理订阅通知 System.out.println("如果用户之前订阅了,然后管理里面点击了拒绝,会到这里。根据具体业务调整推送逻辑"); pw.write(nonce); } } }发送订阅通知
发送订阅通知的时候,需要几个参数
- touser:接收者的用户id,这个在上面的事件推送中能获取到,保存下来就知道给谁推消息了
- template_id:开通订阅通知后,后台添加自己的模板,列表和详情中均有这个参数
- page:跳转进入的网页地址,如果有小程序,这个就无效
- miniprogram:小程序的参数,对应的appid必须是已关联的才可以
- data:模板里面的数据内容
为了方便测试,我将发送订阅通知直接放到了收到事件推送里面,也就是用户订阅–>收到事件推送–>直接将通知发送出去,只是为了测试,真实场景不会这样。 发送部分的代码为
// SubscribeMsgPopupEvent 列表中可以单独管理各个模板推送,这里直接演示收到之后将消息发送出去 // 真实场景肯定是先保存,然后在合适的时机执行下面推送的代码 String openid = resultJson.getString("FromUserName"); // 先获取access_token,这部分正式环境需要配置定时获取,每天2000次调用限制 String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + SECRET; String result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result); String accessToken = JSON.parseObject(result).getString("access_token"); // 获取公众号的自动回复规则 url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend?access_token=" + accessToken; JSONObject param = new JSONObject(); param.put("touser", openid); //接收用户的openid param.put("template_id", "dnemOr1oZ7XLQApxzBaZFtwYJxfdYzvbVS5hjZyW4KI"); //可以为事件推送的模板id,这里测试直接使用后台配置好的 param.put("page", "https://blog.csdn.net/m0_58095675"); JSONObject miniprogram = new JSONObject(); miniprogram.put("appid", "wxa3b096d8546b270d"); miniprogram.put("pagepath", "pages/station/station"); param.put("miniprogram", miniprogram); JSONObject data = new JSONObject(); JSONObject thing1 = new JSONObject(); thing1.put("value", "20个以内字符。"); data.put("thing1", thing1); JSONObject thing2 = new JSONObject(); thing2.put("value", "超过了会报错47003。"); data.put("thing2", thing2); param.put("data", data); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST).requestBody(param.toString()) .timeout(60000).execute().body(); System.out.println(result);
发送订阅通知中请求参数data里面,各个参数内容都是有限制的,比如上面代码里面,thing就只能20个字符以内(即便给的示例超过20个字符也不行)。符号表示除中文、英文、数字外的常见符号,不能带有换行等控制字符。 时间格式支持HH:MM:SS或者HH:MM。 日期包含年月日,为 y 年m月 d 日,y年 m 月、m月 d 日格式,或者用‘-’、‘/’、‘.’符号连接,如2018-01-01,2018/01/01,2018.01.01,2018-01,01-01。各个参数限制如下:
参数类别 参数说明 参数值限制 说明 thing.DATA 事物 20个以内字符 可汉字、数字、字母或符号组合 number.DATA 数字 32位以内数字 只能数字,可带小数 letter.DATA 字母 32位以内字母 只能字母 symbol.DATA 符号 5位以内符号 只能符号 character_string.DATA 字符串 32位以内数字、字母或符号 可数字、字母或符号组合 time.DATA 时间 24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接 例如:15:01,或:2019年10月1日 15:01 date.DATA 日期 年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接 例如:2019年10月1日,或:2019年10月1日 15:01 amount.DATA 金额 1个币种符号+10位以内纯数字,可带小数,结尾可带“元” 可带小数 phone_number.DATA 电话 17位以内,数字、符号 电话号码,例:+86-0766-66888866 car_number.DATA 车牌 8位以内,第一位与最后一位可为汉字,其余为字母或数字 车牌号码:粤A8Z888挂 name.DATA 姓名 10个以内纯汉字或20个以内纯字母或符号 中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内 phrase.DATA 汉字 5个以内汉字 5个以内纯汉字,例如:配送中 类目和模板相关接口正常情况下是不会需要对接这些接口的,但是为了第三方调用,微信还是给开通了。按照正常测试顺序(官方文档里面的顺序得调整看,难受),测试流程如下。
获取公众号类目先拿到所有类目,使用这个类目才能请求公共模板
// 先获取access_token,这部分正式环境需要配置定时获取,每天2000次调用限制 String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + SECRET; String result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result); String accessToken = JSON.parseObject(result).getString("access_token"); // 获取公众号类目 url = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory?access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"data":[{"id":612,"name":"信息查询"},{"id":413,"name":"软件服务提供商"},{"id":1041,"name":"其他医学健康服务"},{"id":1085,"name":"货物运输"}],"errmsg":"ok","errcode":0} System.out.println(result);获取类目下的公共模板
请求参数需要类目id,也就是获取公共类目接口得到的id,多个id可以使用英文逗号分隔。本例id使用上面得到的四个612,413,1041,1085。
url = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles?ids=612,413,1041,1085&start=0&limit=30&access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"count":2335,"data":[{"categoryId":"1041","tid":370,"title":"基因检测结果提醒","type":2},{"categoryId":"612","tid":414,"title":"开奖结果通知","type":2},{"categoryId":"612","tid":420,"title":"线路调整通知","type":2},{"categoryId":"413","tid":492,"title":"预约通知","type":2},{"categoryId":"612","tid":513,"title":"签到提醒","type":2},{"categoryId":"612","tid":522,"title":"每日推壁纸更新通知","type":2},{"categoryId":"612","tid":525,"title":"新作品推荐提醒","type":2},{"categoryId":"612","tid":526,"title":"停电通知","type":2},{"categoryId":"612","tid":563,"title":"图书到期提醒","type":2},{"categoryId":"612","tid":565,"title":"停电通知","type":2},{"categoryId":"612","tid":576,"title":"实时地震通知","type":2},{"categoryId":"612","tid":624,"title":"活动预约提醒","type":2},{"categoryId":"612","tid":638,"title":"摇号结果通知","type":2},{"categoryId":"612","tid":648,"title":"代码更新提醒","type":2},{"categoryId":"612","tid":660,"title":"留言审核通知","type":2},{"categoryId":"612","tid":720,"title":"故障告警通知","type":2},{"categoryId":"612","tid":725,"title":"题库更新提醒","type":2},{"categoryId":"612","tid":730,"title":"客户分配提醒","type":2},{"categoryId":"612","tid":738,"title":"开通会员成功通知","type":2},{"categoryId":"612","tid":739,"title":"会员到期提醒","type":2},{"categoryId":"612","tid":786,"title":"审核结果通知","type":2},{"categoryId":"612","tid":789,"title":"样本状态变更提醒","type":2},{"categoryId":"612","tid":800,"title":"指标配置结果通知","type":2},{"categoryId":"612","tid":802,"title":"任务接收通知","type":2},{"categoryId":"612","tid":843,"title":"名言语录推荐通知","type":2},{"categoryId":"612","tid":867,"title":"监控告警通知","type":2},{"categoryId":"612","tid":895,"title":"审核通过通知","type":2},{"categoryId":"612","tid":1071,"title":"发货确认通知","type":2},{"categoryId":"612","tid":1072,"title":"订单签收通知","type":2},{"categoryId":"612","tid":1077,"title":"行程提醒","type":2}],"errmsg":"ok","errcode":0} System.out.println(result);获取模板中的关键词
上面已经获取到了对应的模板,返回结果中有tid,可以作为本接口的请求参数。返回对应模板的关键词,后面选用模板的时候,可以选择任意关键词,同时顺序也可以调整。 获取模板关键词代码:
url = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords?tid=370&access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"data":[{"kid":1,"name":"温馨提示","example":"您好,您的检测已出结果。","rule":"thing","enumValueList":[]},{"kid":2,"name":"检测项目","example":"基因检测","rule":"thing","enumValueList":[]},{"kid":3,"name":"检测类别","example":"精准医疗","rule":"thing","enumValueList":[]},{"kid":4,"name":"检测时间","example":"2017年02月22日","rule":"date","enumValueList":[]},{"kid":5,"name":"备注","example":"点击“详情”查看详细报告","rule":"thing","enumValueList":[]},{"kid":6,"name":"样本编号","example":"2004001071","rule":"number","enumValueList":[]}],"errmsg":"ok","errcode":0} System.out.println(result);选用模板
从公共模板库中选用模板,到私有模板库中。发送消息的时候,就是从私有模板中选择的。 这个地方有个小坑,使用Jsoup请求,必须添加header中的content-type,值为application/json,否则请求会报错。其他地方的接口即便是用post,也不需要这个header,搞不懂都是公众号的接口为什么不能统一。
// 选用模板:从公共模板库中选用模板,到私有模板库中 url = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate?access_token=" + accessToken; JSONObject param = new JSONObject(); param.put("tid", "370"); // 公共模板的tid JSONArray kidList = new JSONArray(); kidList.add(1); kidList.add(4); kidList.add(2); param.put("kidList", kidList); // 模板关键词列表,顺序可以调整 param.put("sceneDesc", "给用户看的场景描述"); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST) .header("content-type", "application/json") //其他接口不需要,只有本篇的几个接口需要,不加content-type就报错 .requestBody(param.toString()).execute().body(); // {"priTmplId":"vyIiro57RIuW2b0XlZYFa4NOVOsMYIe1dnCwNWZAF1M","errmsg":"ok","errcode":0} System.out.println(result);删除模板
就用上面已经选用的模板做删除测试。 和选用模板一样,删除模板也必须添加content-type为application/json,否则删除会报错。
// 删除模板:这个是真的删除(模板消息推送里面的删除是假的删除,就修改了下templateId) url = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate?access_token=" + accessToken; param = new JSONObject(); param.put("priTmplId", "vyIiro57RIuW2b0XlZYFa4NOVOsMYIe1dnCwNWZAF1M"); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST) .header("content-type", "application/json") //其他接口不需要,只有本篇的几个接口需要,不加content-type就报错 .requestBody(param.toString()).execute().body(); System.out.println(result);获取私有模板列表
这里也有个小坑:必须从后台手动删除掉已删除的私人模板,不然这里还是能获取到,而且和正常的区分不出来。
// 获取私有模板列表:必须从后台手动删除掉已删除的私人模板,不然这里还是能获取到,而且和正常的区分不出来 url = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result);完整代码
package com.lootaa.wechat; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.digest.DigestUtils; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.jsoup.Connection.Method; import org.jsoup.Jsoup; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.lootaa.wechat.util.WXBizMsgCrypt; /** * 订阅通知 * 前置条件:公众号后台设置ip白名单(推送给用户需要),启用了服务器配置(接收订阅事件推送需要) */ @RestController public class Test010 { public static final String APPID = "wx276049d6a7551dca"; public static final String SECRET = "cbe109fdf6f399bd72ed3a4afafa21b1"; /** * 完整项目源码可关注公众号"lootaayun"(洛塔),回复010获取 */ public static void main(String[] args) throws Exception { // 先获取access_token,这部分正式环境需要配置定时获取,每天2000次调用限制 String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + SECRET; String result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result); String accessToken = JSON.parseObject(result).getString("access_token"); // 获取公众号类目 url = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory?access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"data":[{"id":612,"name":"信息查询"},{"id":413,"name":"软件服务提供商"},{"id":1041,"name":"其他医学健康服务"},{"id":1085,"name":"货物运输"}],"errmsg":"ok","errcode":0} System.out.println(result); // 获取类目下的公共模板 url = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles?ids=612,413,1041,1085&start=0&limit=30&access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"count":2335,"data":[{"categoryId":"1041","tid":370,"title":"基因检测结果提醒","type":2},{"categoryId":"612","tid":414,"title":"开奖结果通知","type":2},{"categoryId":"612","tid":420,"title":"线路调整通知","type":2},{"categoryId":"413","tid":492,"title":"预约通知","type":2},{"categoryId":"612","tid":513,"title":"签到提醒","type":2},{"categoryId":"612","tid":522,"title":"每日推壁纸更新通知","type":2},{"categoryId":"612","tid":525,"title":"新作品推荐提醒","type":2},{"categoryId":"612","tid":526,"title":"停电通知","type":2},{"categoryId":"612","tid":563,"title":"图书到期提醒","type":2},{"categoryId":"612","tid":565,"title":"停电通知","type":2},{"categoryId":"612","tid":576,"title":"实时地震通知","type":2},{"categoryId":"612","tid":624,"title":"活动预约提醒","type":2},{"categoryId":"612","tid":638,"title":"摇号结果通知","type":2},{"categoryId":"612","tid":648,"title":"代码更新提醒","type":2},{"categoryId":"612","tid":660,"title":"留言审核通知","type":2},{"categoryId":"612","tid":720,"title":"故障告警通知","type":2},{"categoryId":"612","tid":725,"title":"题库更新提醒","type":2},{"categoryId":"612","tid":730,"title":"客户分配提醒","type":2},{"categoryId":"612","tid":738,"title":"开通会员成功通知","type":2},{"categoryId":"612","tid":739,"title":"会员到期提醒","type":2},{"categoryId":"612","tid":786,"title":"审核结果通知","type":2},{"categoryId":"612","tid":789,"title":"样本状态变更提醒","type":2},{"categoryId":"612","tid":800,"title":"指标配置结果通知","type":2},{"categoryId":"612","tid":802,"title":"任务接收通知","type":2},{"categoryId":"612","tid":843,"title":"名言语录推荐通知","type":2},{"categoryId":"612","tid":867,"title":"监控告警通知","type":2},{"categoryId":"612","tid":895,"title":"审核通过通知","type":2},{"categoryId":"612","tid":1071,"title":"发货确认通知","type":2},{"categoryId":"612","tid":1072,"title":"订单签收通知","type":2},{"categoryId":"612","tid":1077,"title":"行程提醒","type":2}],"errmsg":"ok","errcode":0} System.out.println(result); // 获取模板中的关键词 url = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords?tid=370&access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); // {"data":[{"kid":1,"name":"温馨提示","example":"您好,您的检测已出结果。","rule":"thing","enumValueList":[]},{"kid":2,"name":"检测项目","example":"基因检测","rule":"thing","enumValueList":[]},{"kid":3,"name":"检测类别","example":"精准医疗","rule":"thing","enumValueList":[]},{"kid":4,"name":"检测时间","example":"2017年02月22日","rule":"date","enumValueList":[]},{"kid":5,"name":"备注","example":"点击“详情”查看详细报告","rule":"thing","enumValueList":[]},{"kid":6,"name":"样本编号","example":"2004001071","rule":"number","enumValueList":[]}],"errmsg":"ok","errcode":0} System.out.println(result); // 选用模板:从公共模板库中选用模板,到私有模板库中 url = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate?access_token=" + accessToken; JSONObject param = new JSONObject(); param.put("tid", "370"); // 公共模板的tid JSONArray kidList = new JSONArray(); kidList.add(1); kidList.add(4); kidList.add(2); param.put("kidList", kidList); // 模板关键词列表,顺序可以调整 param.put("sceneDesc", "给用户看的场景描述"); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST) .header("content-type", "application/json") //其他接口不需要,只有本篇的几个接口需要,不加content-type就报错 .requestBody(param.toString()).execute().body(); // {"priTmplId":"vyIiro57RIuW2b0XlZYFa4NOVOsMYIe1dnCwNWZAF1M","errmsg":"ok","errcode":0} System.out.println(result); // 删除模板:这个是真的删除(模板消息推送里面的删除是假的删除,就修改了下templateId) url = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate?access_token=" + accessToken; param = new JSONObject(); param.put("priTmplId", "vyIiro57RIuW2b0XlZYFa4NOVOsMYIe1dnCwNWZAF1M"); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST) .header("content-type", "application/json") //其他接口不需要,只有本篇的几个接口需要,不加content-type就报错 .requestBody(param.toString()).execute().body(); System.out.println(result); // 获取私有模板列表:必须从后台手动删除掉已删除的私人模板,不然这里还是能获取到,而且和正常的区分不出来 url = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token=" + accessToken; result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result); } /** * 完整项目源码可关注公众号"lootaayun"(洛塔),回复010获取 */ @GetMapping("wx10") public void wxGet(HttpServletRequest request, PrintWriter pw) { // 微信加密签名,需要使用本地计算出来的和这个对比,确认是微信发送的消息 String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); // 时间戳 String nonce = request.getParameter("nonce"); // 随机数 String echostr = request.getParameter("echostr"); // 随机字符串 // 将token、timestamp、nonce三个参数进行字典序排序 Listlist = new ArrayList(); list.add("lootaa"); // 公众号后台设置的token list.add(timestamp); list.add(nonce); Collections.sort(list); // 将三个参数字符串拼接成一个字符串进行sha1加密 String tokenStr = ""; for (int i = 0; i < list.size(); i++) { tokenStr += list.get(i); } String signatureStr = DigestUtils.sha1Hex(tokenStr); // 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 if (signature.equals(signatureStr)) { pw.write(echostr); // 原样返回echostr参数内容 } else { pw.write(""); } } public static Document getDocument(HttpServletRequest request) { SAXReader reader = new SAXReader(); try { InputStream ins = request.getInputStream(); Document doc = reader.read(ins); return doc; } catch (IOException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } return null; } public static JSONObject documentToJSONObject(String xml) { JSONObject jsonObject = null; try { jsonObject = elementToJSONObject(DocumentHelper.parseText(xml).getRootElement()); } catch (DocumentException e) { e.printStackTrace(); } return jsonObject; } @SuppressWarnings("unchecked") public static JSONObject elementToJSONObject(Element node) { JSONObject result = new JSONObject(); // 当前节点的名称、文本内容和属性 ListlistAttr = node.attributes();// 当前节点的所有属性的list for (Attribute attr : listAttr) {// 遍历当前节点的所有属性 result.put(attr.getName(), attr.getValue()); } // 递归遍历当前节点所有的子节点 ListlistElement = node.elements();// 所有一级子节点的list if (!listElement.isEmpty()) { for (Element e : listElement) {// 遍历所有一级子节点 if (e.attributes().isEmpty() && e.elements().isEmpty()) // 判断一级节点是否有属性和子节点 result.put(e.getName(), e.getTextTrim());// 沒有则将当前节点作为上级节点的属性对待 else { if (!result.containsKey(e.getName())) // 判断父节点是否存在该一级节点名称的属性 result.put(e.getName(), new JSONArray());// 没有则创建 ((JSONArray) result.get(e.getName())).add(elementToJSONObject(e));// 将该一级节点放入该节点名称的属性对应的值中 } } } return result; } @PostMapping("wx10") public void wxPost(HttpServletRequest request, HttpServletResponse response, PrintWriter pw) throws Exception { String token = "lootaa"; String encodingAesKey = "FpKEYJDuwK92k2juU2z0sUvTmc3hB4W5wGLJEKay8oK"; String appid = "wx276049d6a7551dca"; WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appid); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); String msgSignature = request.getParameter("msg_signature"); Document doc = getDocument(request); String result2 = pc.decryptMsg(msgSignature, timestamp, nonce, doc.asXML()); System.out.println("解密后明文: " + result2); JSONObject resultJson = documentToJSONObject(result2); String messageType = resultJson.getString("MsgType"); //这个值如果是event表示是事件推送;如果是其他字符,参照Test004 if(Objects.equals("event", messageType)) { String event = resultJson.getString("Event"); //这个是事件的具体类型 if(Objects.equals(event, "subscribe_msg_popup_event")) { // 订阅消息 // SubscribeMsgPopupEvent 列表中可以单独管理各个模板推送,这里直接演示收到之后将消息发送出去 // 真实场景肯定是先保存,然后在合适的时机执行下面推送的代码 String openid = resultJson.getString("FromUserName"); // 先获取access_token,这部分正式环境需要配置定时获取,每天2000次调用限制 String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + SECRET; String result = Jsoup.connect(url).ignoreContentType(true).method(Method.GET).execute().body(); System.out.println(result); String accessToken = JSON.parseObject(result).getString("access_token"); // 获取公众号的自动回复规则 url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend?access_token=" + accessToken; JSONObject param = new JSONObject(); param.put("touser", openid); //接收用户的openid param.put("template_id", "dnemOr1oZ7XLQApxzBaZFtwYJxfdYzvbVS5hjZyW4KI"); //可以为事件推送的模板id,这里测试直接使用后台配置好的 param.put("page", "https://blog.csdn.net/m0_58095675"); JSONObject miniprogram = new JSONObject(); miniprogram.put("appid", "wxa3b096d8546b270d"); miniprogram.put("pagepath", "pages/station/station"); param.put("miniprogram", miniprogram); JSONObject data = new JSONObject(); JSONObject thing1 = new JSONObject(); thing1.put("value", "20个以内字符。"); data.put("thing1", thing1); JSONObject thing2 = new JSONObject(); thing2.put("value", "超过了会报错47003。"); data.put("thing2", thing2); param.put("data", data); result = Jsoup.connect(url).ignoreContentType(true).method(Method.POST).requestBody(param.toString()) .timeout(60000).execute().body(); System.out.println(result); pw.write(nonce); } else if(Objects.equals(event, "subscribe_msg_popup_event")) { // 发送订阅通知 System.out.println("发送订阅通知的事件"); pw.write(nonce); } else if(Objects.equals(event, "subscribe_msg_change_event")) { // 用户管理订阅通知 System.out.println("如果用户之前订阅了,然后管理里面点击了拒绝,会到这里。根据具体业务调整推送逻辑"); pw.write(nonce); } } } }