Spring Boot 微信公众号开发(二) 消息的接收与回复
微信公众号官方文档 --> 入门指引:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html
一、消息管理
1、消息接收
在上篇中,我们实现了应用服务器和微信腾讯服务器之间的对接操作,本篇主要实现java后台对微信公众号消息的接收。
当我们在完成了服务器验证之后,此后用户每次向公众号发送消息、或者产生自定义菜单点击事件时,开发者填写的服务器配置URL将得到微信服务器推送过来的消息和事件,然后开发者可以依据自身业务逻辑进行响应,例如回复消息等!通过这句话我们能知道后面所有的微信服务器和我们应用服务器之间的沟通都是通过post消息体来完成的,那么我们这里将讲述如何接受微信post的消息体!
1. 官方提供的消息类型和格式
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。(与上篇中校验是同一个路径,但是请求方法不同)
- 普通消息类型:文本消息、图片消息、语音消息、视频消息、小视频消息、地理位置消息、链接消息
- 事件消息类型:关注/取消关注事件、扫描带参数二维码事件、上报地理位置事件、自定义菜单事件、点击菜单拉取消息时的事件推送、点击菜单跳转链接时的事件推送
(链接-link /地理位置-location /小视频-shortvideo/视频-video /语音-voice /图片-image /文本-text)
请注意:
- 关于重试的消息排重,推荐使用MsgId排重。
- 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息-被动回复消息”。
- 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息-被动回复消息”接口向用户被动回复消息时,可以在
公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等API调用形式向用户发送消息,则不受影响)
举例:官方返回的信息值 以文本消息为例
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
参数解释:
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方帐号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,文本为text |
Content | 文本消息内容 |
MsgId | 消息id,64位整型 |
2、消息回复
在前面我们已经完成了对消息的分类和回复消息实体的建立,这里回复文本消息需要用到的就是我们的TextMessage,我们把回复文本消息在【文本消息】类型中给出回复!在我们做消息回复的时候需要设置消息的接收人ToUserName(openid)、消息的发送方FromUserName、消息类型MsgType、创建时间CreateTime以及消息体Content,由于我们我们的消息回复格式是需要为xml,所以最终我们需要将其装换成xml再做返回输出!
二、代码实现
1、根据官方文档创建相关实体类
1. 基类(公共参数)
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:13
* @description: 微信请求消息基本类
*/
@Data
public class BaseMessage {
// 开发者微信号
private String ToUserName;
// 发送方帐号(一个OpenID)
private String FromUserName;
// 消息创建时间 (整型)
private Long CreateTime;
// 消息类型(链接-link /地理位置-location /小视频-shortvideo/视频-video /语音-voice /图片-image /文本-text)
private String MsgType;
// 消息id,64位整型
private Long MsgId;
}
2. 链接消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:14
* @description: 链接消息
*/
@Data
public class LinkMessage extends BaseMessage{
// 消息标题
private String Title;
// 消息描述
private String Description;
// 消息链接
private String Url;
}
3. 图片消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:14
* @description: 图片消息
*/
@Data
public class ImageMessage extends BaseMessage {
// 图片链接
private String PicUrl;
//图片消息媒体id,可以调用获取临时素材接口拉取数据。
private String MediaId;
}
4. 地理位置消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:15
* @description: 地理位置消息
*/
@Data
public class LocationMessage extends BaseMessage {
// 地理位置维度
private String Location_X;
// 地理位置经度
private String Location_Y;
// 地图缩放大小
private String Scale;
// 地理位置信息
private String Label;
}
5. 文本消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:16
* @description: 文本消息
*/
@Data
public class TextMessage extends BaseMessage{
// 消息内容
private String Content;
}
6. 语音消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:17
* @description: 语音消息
*/
@Data
public class VoiceMessage extends BaseMessage {
// 语音消息媒体id,可以调用获取临时素材+接口拉取数据。
private String MediaId;
// 语音格式
private String Format;
}
7. 视频/小视频消息类
import lombok.Data;
/**
* @author yh
* @date 2020/8/21 10:16
* @description: 视频/小视频消息
*/
@Data
public class VideoMessage extends BaseMessage{
// 视频消息媒体id,可以调用多媒体文件下载接口拉取数据
private String MediaId;
// 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据
private String ThumbMediaId;
}
2、POM依赖
<!-- xml -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>
3、控制层
1.api
/**
* @author: yh
* @description: token验证
* @date: 2020/8/20
* @param request
* @return void
**/
@GetMapping("/wxRequest")
String doGet(HttpServletRequest request, HttpServletResponse response);
/**
* @author: yh
* @description: 消息的接收和处理
* @date: 2020/8/21
* @param request
* @param response
* @return void
**/
@PostMapping("/wxRequest")
void doPost(HttpServletRequest request, HttpServletResponse response);
2.controller
/**
* @author: yh
* @description: token验证
* @date: 2020/8/20
* @param request
* @return void
**/
@Override
public String doGet(HttpServletRequest request,HttpServletResponse response) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
log.info("signature[{}], timestamp[{}], nonce[{}], echostr[{}]", signature, timestamp, nonce, echostr);
if(SignUtil.checkSignature(signature, timestamp, nonce)){
log.info("数据源为微信后台,将echostr[{}]返回!", echostr);
return echostr;
}
return "";
}
/**
* @author: yh
* @description: 消息/事件的接收和处理
* @date: 2020/8/21
* @param request
* @param response
* @return void
**/
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
Map<String, String> messageMap = MessageUtil.parseXml(request);
System.out.println("消息:"+messageMap.get("Content"));
String type = messageMap.get("MsgType");
if(MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(type)){
//事件处理
EventDispatcher.processEvent(messageMap);
}else {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
//消息处理
String data = MsgDispatcher.processMessage(messageMap);
PrintWriter out = response.getWriter();
out.print(data);
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
4、util- MessageUtil
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author yh
* @date 2020/8/20 17:14
* @description: 消息工具类
*/
public class MessageUtil {
/**
* 返回消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/**
* 返回消息类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/**
* 返回消息类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
/**
* 请求消息类型:文本
*/
public static final String REQ_MESSAGE_TYPE_TEXT = "text";
/**
* 请求消息类型:图片
*/
public static final String REQ_MESSAGE_TYPE_IMAGE = "image";
/**
* 请求消息类型:语音
*/
public static final String REQ_MESSAGE_TYPE_VOICE = "voice";
/**
* 请求消息类型:视频
*/
public static final String REQ_MESSAGE_TYPE_VIDEO = "video";
/**
* 请求消息类型:小视频
*/
public static final String REQ_MESSAGE_TYPE_SHORTVIDEO = "shortvideo";
/**
* 请求消息类型:地理位置
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
/**
* 请求消息类型:链接
*/
public static final String REQ_MESSAGE_TYPE_LINK = "link";
/**
* 请求消息类型:推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/**
* 事件类型:subscribe(订阅)
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/**
* 事件类型:unsubscribe(取消订阅)
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/**
* 事件类型:CLICK(自定义菜单点击事件)
*/
public static final String EVENT_TYPE_CLICK = "CLICK";
/**
* 事件类型:VIEW(扫描二维码事件)
*/
public static final String EVENT_TYPE_SCAN = "SCAN";
/**
* 事件类型:LOCATION(位置上报事件)
*/
public static final String EVENT_TYPE_LOCATION = "LOCATION";
/**
* 事件类型:VIEW(自定义菜单View事件)
*/
public static final String EVENT_TYPE_VIEW = "VIEW";
/**
* @author: yh
* @description: 解析微信发来的请求(XML)
* @date: 2020/8/21
* @param request
* @return java.util.Map<java.lang.String,java.lang.String>
**/
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
/**
* @author: yh
* @description: 对象到xml的处理
* @date: 2020/8/21
* @param null
* @return
**/
@SuppressWarnings("unused")
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;
@SuppressWarnings("rawtypes")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
/**
* @author: yh
* @description: 文本消息对象转换成xml
* @date: 2020/8/21
* @param textMessageRes
* @return java.lang.String
**/
public static String textMessageToXml(TextMessageRes textMessageRes) {
xstream.alias("xml", textMessageRes.getClass());
return xstream.toXML(textMessageRes);
}
/**
* @author: yh
* @description: 图文消息对象转换成xml
* @date: 2020/8/21
* @param articlesMessageRes
* @return java.lang.String
**/
public static String newsMessageToXml(ArticlesMessageRes articlesMessageRes) {
xstream.alias("xml", articlesMessageRes.getClass());
xstream.alias("item", new Article().getClass());
return xstream.toXML(articlesMessageRes);
}
/**
* @author: yh
* @description: 图片消息对象转换成xml
* @date: 2020/8/21
* @param imageMessageRes
* @return java.lang.String
**/
public static String imageMessageToXml(ImageMessageRes imageMessageRes) {
xstream.alias("xml", imageMessageRes.getClass());
return xstream.toXML(imageMessageRes);
}
/**
* @author: yh
* @description: 语音消息对象转换成xml
* @date: 2020/8/21
* @param voiceMessageRes
* @return java.lang.String
**/
public static String voiceMessageToXml(VoiceMessageRes voiceMessageRes) {
xstream.alias("xml", voiceMessageRes.getClass());
return xstream.toXML(voiceMessageRes);
}
/**
* @author: yh
* @description: 视频消息对象转换成xml
* @date: 2020/8/21
* @param videoMessageRes
* @return java.lang.String
**/
public static String videoMessageToXml(VideoMessageRes videoMessageRes) {
xstream.alias("xml", videoMessageRes.getClass());
return xstream.toXML(videoMessageRes);
}
/**
* @author: yh
* @description: 音乐消息对象转换成xml
* @date: 2020/8/21
* @param musicMessageRes
* @return java.lang.String
**/
public static String musicMessageToXml(MusicMessageRes musicMessageRes) {
xstream.alias("xml", musicMessageRes.getClass());
return xstream.toXML(musicMessageRes);
}
}
5、业务分发处理
1. 消息分类
import java.util.Date;
import java.util.Map;
/**
* @author yh
* @date 2020/8/21 10:19
* @description: 微信消息业务处理分发器
*/
public class MsgDispatcher {
public static String processMessage(Map<String, String> map) {
String openid=map.get("FromUserName"); //用户openid
String mpid=map.get("ToUserName"); //公众号原始ID
TextMessageRes textMessageRes = new TextMessageRes();
textMessageRes.setToUserName(openid);
textMessageRes.setFromUserName(mpid);
textMessageRes.setCreateTime(new Date().getTime());
textMessageRes.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { // 文本消息
System.out.println("==============这是文本消息!");
String jsonString = JSONObject.toJSONString(map);
TextMessageReq textMessage = JSONObject.parseObject(jsonString, TextMessageReq.class);
textMessageRes.setContent("你好,这里是测试回复");
return MessageUtil.textMessageToXml(textMessageRes);
}
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { // 图片消息
System.out.println("==============这是图片消息!");
String jsonString = JSONObject.toJSONString(map);
ImageMessageReq imageMessage = JSONObject.parseObject(jsonString, ImageMessageReq.class);
}
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { // 链接消息
System.out.println("==============这是链接消息!");
String jsonString = JSONObject.toJSONString(map);
LinkMessageReq linkMessage = JSONObject.parseObject(jsonString, LinkMessageReq.class);
}
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { // 位置消息
System.out.println("==============这是位置消息!");
String jsonString = JSONObject.toJSONString(map);
LocationMessageReq locationMessage = JSONObject.parseObject(jsonString, LocationMessageReq.class);
}
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_VIDEO)) { // 视频/小视频消息
System.out.println("==============这是视频消息!");
String jsonString = JSONObject.toJSONString(map);
VideoMessageReq videoMessage = JSONObject.parseObject(jsonString, VideoMessageReq.class);
}
if (map.get("MsgType").equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { // 语音消息
System.out.println("==============这是语音消息!");
String jsonString = JSONObject.toJSONString(map);
VoiceMessageReq voiceMessage = JSONObject.parseObject(jsonString, VoiceMessageReq.class);
}
return "";
}
}
2. 事件分类
/**
* @author yh
* @date 2020/8/21 10:19
* @description: 事件消息业务分发器
*/
public class EventDispatcher {
public static void processEvent(Map<String, String> map) {
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { //关注事件
System.out.println("==============这是关注事件!");
}
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { //取消关注事件
System.out.println("==============这是取消关注事件!");
}
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_SCAN)) { //扫描二维码事件
System.out.println("==============这是扫描二维码事件!");
}
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_LOCATION)) { //位置上报事件
System.out.println("==============这是位置上报事件!");
}
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_CLICK)) { //自定义菜单点击事件
System.out.println("==============这是自定义菜单点击事件!");
}
if (map.get("Event").equals(MessageUtil.EVENT_TYPE_VIEW)) { //自定义菜单View事件
System.out.println("==============这是自定义菜单View事件!");
}
}
}