您的当前位置:首页正文

公众号订阅通知

2024-11-26 来源:个人技术集锦

洛塔服务号回复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三个参数进行字典序排序
		List<String> list = new ArrayList<String>();
		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:///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三个参数进行字典序排序
		List<String> list = new ArrayList<String>();
		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();
        // 当前节点的名称、文本内容和属性
		List<Attribute> listAttr = node.attributes();// 当前节点的所有属性的list
        for (Attribute attr : listAttr) {// 遍历当前节点的所有属性
            result.put(attr.getName(), attr.getValue());
        }
        // 递归遍历当前节点所有的子节点
        List<Element> listElement = 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:///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);
			}
			
		}
		
		

	}
	
}

显示全文