上一节课我们讲到了微信公众号的配置和接入,今天我们要讲一下最简单的响应之后台文本回复,就是当关注微信公众号的用户对公众号发送一段文本的时候,我们在后台应该怎么处理,先看一下我的效果界面
当我对微信公众号发送:“我是你爸爸”这段文本的时候,微信公众号后台给我回复了一段“你发送的是:我是你爸爸”,我们在看看我们的项目控制台有什么,如图:
是的,我们的后台也收到了用户发送过来的“我是你爸爸”这段话,这里我们有必要再简单的复习一下这个消息的发送流程的:
再次申明一下关键点,我们后台的所有的消息接口都是直接对接腾讯的后台微信服务器的,而不是直接从客户端收到的,这点务必要明确,所以当我们收到用户发来的消息之后,我们同样只能先回复给腾讯服务器,腾讯服务器再转发给客户端,客户端就能看到我们后台发来的消息了,他的回复流程图如下:
这里我们看看微信开发文档是怎么说的,进入【开发】----》【开发者工具】,可以看到【开发者文档】
在截图所在的【接收普通消息】文档内可以看到,当用户给腾讯服务器发送一段文本的时候,腾讯服务器对这段文本做了包装,这段包装漂亮的消息包中不但有客户发送的文本消息,还附带了一些关于这段消息的一些其他属性:
文档中这些属性参数的含义表示如下:
这里有个属性是MsgType表示用户发送的是文本类型的消息,所以我们的后台可以判断这个属性,如果MsgType="type"我们就可以自定义一段文本发送给微信服务器让他帮我们转发给客户端,那么既然我们是需要腾讯的微信服务帮我们转发,我们当然看看服务器对我们发送的格式是怎么要求的,只有符合要求的消息包,腾讯的微信公众号服务器才能对消息包正确的翻译解读,翻译解读成功后才能正确发送给客户端,我们再看看这块腾讯的微信公众号服务器是怎么要求格式的:
是的,我们需要根据上面的格式要求返回给腾讯服务器一个XML格式的数据包,腾讯服务器才懂得我们的意图和想要说的内容,才能帮我们想对客户说的成功转发给客户,话不多说,我们开始吧,首先我们要在上次的验证接入Controller类中创建一个RestAPI接口,这个接口跟我们再配置中的填写的URL的RestAPI接口是一样的,只是这里的提交方式是POST,我们可以简单理解为GET是获得腾讯服务器的消息,POST是提交消息给腾讯服务器,RestAPI配置部分看截图:
Controller具体代码我完整贴出在下面:
WechatController.java
package com.xu.wemall.controller.wechat;
import com.xu.wemall.commons.utils.CheckUtil;
import com.xu.wemall.commons.utils.UploadUtil;
import com.xu.wemall.commons.utils.MessageUtil;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Map;
/**
* 类名称: LoginController
* 类描述: 与微信对接登陆验证
*
* @author yuanjun
* 创建时间:2017年12月5日上午10:52:13
*/
@Slf4j
@RestController
@Api(tags = "接入验证接口")
@RequestMapping(value = "/weChart")
public class WechatController {
@Autowired
private UploadUtil uploadUtil;
@Autowired
private MessageUtil messageUtil;
@RequestMapping(value = "/connect", method = RequestMethod.GET)
public String connect(@RequestParam(value = "signature") String signature,
@RequestParam(value = "timestamp") String timestamp,
@RequestParam(value = "nonce") String nonce,
@RequestParam(value = "echostr") String echostr) {
log.info("-----开始校验签名-----");
PrintWriter out = null;
if (CheckUtil.checkSignature(signature, timestamp, nonce)) {
log.info("-----签名校验通过-----");
return echostr;
} else {
log.info("-----校验签名失败-----");
return null;
}
}
@RequestMapping(value = "connect", method = RequestMethod.POST)
public String dopost(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setCharacterEncoding("utf-8");
//将微信请求xml转为map格式,获取所需的参数
Map<String, String> map = MessageUtil.parseXml(request);
String ToUserName = map.get("ToUserName");
String FromUserName = map.get("FromUserName");
String MsgType = map.get("MsgType");
String Content = map.get("Content");
String Event = map.get("Event");
if(MessageUtil.REQ_MESSAGE_TYPE_EVENT.equals(MsgType)){
if(MessageUtil.EVENT_TYPE_SUBSCRIBE.equals(Event)){
String xmlString = messageUtil.subscribeForText(ToUserName,FromUserName);
return xmlString;
}else if(MessageUtil.EVENT_TYPE_UNSUBSCRIBE.equals(Event)){
String xmlString = messageUtil.unsubscribeForText(ToUserName,FromUserName);
return xmlString;
}
}
//处理文本类型,实现输入1,回复相应的封装的内容
if (MessageUtil.REQ_MESSAGE_TYPE_TEXT.equals(MsgType)) {
String xmlString = messageUtil.replyForText(ToUserName,FromUserName,"你发送的是:" + Content);
log.info(xmlString);
return xmlString;
}
if (MessageUtil.REQ_MESSAGE_TYPE_IMAGE.equals(MsgType)) {
String filePath = "C:\\Users\\RonnieXu\\Pictures\\2.jpg";
String xmlString = messageUtil.replyForImage(ToUserName,FromUserName,filePath);
return xmlString;
}
return null;
}
}
MessageUtil.java
package com.xu.wemall.commons.utils;
/**
* 封装微信消息类型,有一个解析微信发过的xml消息的工具
*/
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 com.xu.wemall.commons.constants.TypeConstant;
import com.xu.wemall.pojo.response.*;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.Writer;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>Company: B505信息技术研究所 </p>
*
* @Description: 封装微信消息类型,有一个解析xml格式的工具
* @Create Date: 2017年10月11日上午11:28:48
* @Version: V1.00
* @Author: 来日可期
*/
@Slf4j
@Component
public class MessageUtil {
@Autowired
private UploadUtil uploadUtil;
//====================================================================================
/**
* 请求消息类型:文本
*/
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 final String REQ_MESSAGE_TYPE_VIDEO = "video";
/**
* 请求消息类型:链接
*/
public final String REQ_MESSAGE_TYPE_LINK = "link";
/**
* 请求消息类型:地理位置
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
/**
* 请求消息类型:小视频
*/
public static final String REQ_MESSAGE_TYPE_SHORTVIDEO = "shortvideo";
/**
* 请求消息类型:事件推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/**
* 返回消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/**
* 消息返回类型:图片
*/
public static final String RESP_MESSAGE_TYPE_IMAGE = "image";
/**
* 消息返回类型:语音
*/
public static final String RESP_MESSAGE_TYPE_VOICE = "voice";
/**
* 消息返回类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/**
* 消息返回类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
/**
* 消息返回类型:视频
*/
public static final String RESP_MESSAGE_TYPE_VIDEO = "video";
/**
* 事件类型:订阅
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/**
* 事件类型:取消订阅
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
//=============================================================================
/**
* 事件类型:scan(关注用户扫描带参二维码)
*/
public final String EVENT_TYPE_SCAN = "scan";
/**
* 事件类型:location(上报地理位置)
*/
public final String EVENT_TYPE_LOCATION = "location";
/**
* 事件类型:CLICK(点击菜单拉取消息)
*/
public final String EVENT_TYPE_CLICK = "CLICK";
/**
* 事件类型:VIEW(自定义菜单URl视图)
*/
public final String EVENT_TYPE_VIEW = "VIEW";
/**
* 事件类型:TEMPLATESENDJOBFINISH(模板消息送达情况提醒)
*/
public final String EVENT_TYPE_TEMPLATESENDJOBFINISH = "TEMPLATESENDJOBFINISH";
/**
* @Description: 解析微信服务器发过来的xml格式的消息将其转换为map
* @Parameters: WeixinMessageUtil
* @Return: Map<String, String> * @Create Date: 2017年10月11日上午11:41:23 * @Version: V1.00 * @author:来日可期 */ 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(); // 得到根元素的所有子节点 @SuppressWarnings("unchecked") List<Element> elementList = root.elements(); // 判断又没有子元素列表 if (elementList.size() == 0) { map.put(root.getName(), root.getText()); } else { for (Element e : elementList) map.put(e.getName(), e.getText()); } // 释放资源 inputStream.close(); inputStream = null; System.out.println("---------xml转换为map-----:" + map); return map; } /** * 回复关注 * @param toUserName * @param fromUserName * @return */ public String subscribeForText(String toUserName, String fromUserName){ log.info("有人关注你的公众号了!"); TextMessage textMessage = new TextMessage(); //必填 textMessage.setFromUserName(toUserName); //必填 textMessage.setToUserName(fromUserName); //必填 textMessage.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_TEXT); //必填 textMessage.setContent("帅锅美女,谢谢你的关注:你可以随便发文字图片试试,有惊喜哦!……"); textMessage.setCreateTime( LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()); String xmlString = MessageUtil.textMessageToXml(textMessage); log.info(xmlString); return xmlString; } /** * 回复取消关注 * @param toUserName * @param fromUserName * @return */ public String unsubscribeForText(String toUserName, String fromUserName){ log.info(fromUserName+"取消了对你【"+toUserName+"】的微信公众号关注"); TextMessage textMessage = new TextMessage(); //必填 textMessage.setFromUserName(toUserName); //必填 textMessage.setToUserName(fromUserName); //必填 textMessage.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_TEXT); //必填 textMessage.setContent(fromUserName+"取消了对你【"+toUserName+"】的微信公众号关注"); textMessage.setCreateTime( LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()); String xmlString = MessageUtil.textMessageToXml(textMessage); log.info(xmlString); return xmlString; } /** * 回复文本消息 * @param toUserName * @param fromUserName * @param msg * @return */ public String replyForText(String toUserName, String fromUserName, String msg){ log.info("这是文本消息回复!"); TextMessage textMessage = new TextMessage(); //必填 textMessage.setFromUserName(toUserName); //必填 textMessage.setToUserName(fromUserName); //必填 textMessage.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_TEXT); //必填 textMessage.setContent(msg); textMessage.setCreateTime( LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()); String xmlString = MessageUtil.textMessageToXml(textMessage); log.info(xmlString); return xmlString; } /** * 回复文本图片 * @param toUserName * @param fromUserName * @param filePath * @return */ public String replyForImage(String toUserName, String fromUserName, String filePath) throws Exception{ log.info("这是图片消息回复!"); ImageMessage imageMessage = new ImageMessage(); //必填 imageMessage.setFromUserName(toUserName); //必填 imageMessage.setToUserName(fromUserName); //必填 imageMessage.setMsgType(MessageUtil.REQ_MESSAGE_TYPE_IMAGE); //必填 imageMessage.setCreateTime( LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()); String fileMediaId = uploadUtil.uploadFile(filePath, TypeConstant.REQ_MESSAGE_TYPE_IMAGE); if (!StringUtils.isEmpty(fileMediaId)) { Image image = new Image(); image.setMediaId(fileMediaId); imageMessage.setImage(image); String xmlString = MessageUtil.imageMessageToXml(imageMessage); log.info(xmlString); return xmlString; } return null; } //=================================================================================================== /** * @param textMessage * @return xml * @Description: 文本消息对象转换成xml * @date 2016-12-01 */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * @param newsMessage * @return xml * @Description: 图文消息对象转换成xml * @date 2016-12-01 */ public String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * @param imageMessage * @return xml * @Description: 图片消息对象转换成xml * @date 2016-12-01 */ public static String imageMessageToXml(ImageMessage imageMessage) { xstream.alias("xml", imageMessage.getClass()); return xstream.toXML(imageMessage); } /** * @param voiceMessage * @return xml * @Description: 语音消息对象转换成xml * @date 2016-12-01 */ public String voiceMessageToXml(VoiceMessage voiceMessage) { xstream.alias("xml", voiceMessage.getClass()); return xstream.toXML(voiceMessage); } /** * @param videoMessage * @return xml * @Description: 视频消息对象转换成xml * @date 2016-12-01 */ public String videoMessageToXml(VideoMessage videoMessage) { xstream.alias("xml", videoMessage.getClass()); return xstream.toXML(videoMessage); } /** * @param musicMessage * @return xml * @Description: 音乐消息对象转换成xml * @date 2016-12-01 */ public String musicMessageToXml(MusicMessage musicMessage) { xstream.alias("xml", musicMessage.getClass()); return xstream.toXML(musicMessage); } /** * 对象到xml的处理 * 扩展xstream,使其支持CDATA块 */ private static XStream xstream = new XStream(new XppDriver() { @Override public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点的转换都增加CDATA标记 boolean cdata = true; @Override @SuppressWarnings("rawtypes") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } @Override protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); } else { writer.write(text); } } }; } }); }
BaseMessage.java
package com.xu.wemall.pojo.response;
import lombok.*;
/**
*
* @Description: 响应消息基类(公众账号→普通用户)
* @Parameters:
* @Return:
* @Create Date:
* @Version: V1.00
* @author: 来日可期
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString(callSuper = true)
public class BaseMessage {
protected String ToUserName;
protected String FromUserName;
protected Long CreateTime;
protected String MsgType;
}
TextMessage.java
package com.xu.wemall.pojo.response;
import lombok.*;
/**
*
* @Description: 文本消息
* @Parameters:
* @Return:
* @Create Date:
* @Version: V1.00
* @author: 来日可期
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class TextMessage extends BaseMessage {
private String Content;// 文本消息内容
private String MsgId;// 消息id,64位整型
}
这里有一些其他功能的模块,请关心WechatController中的这段代码(深入的请自己追踪代码)
从这段代码中我们可以看到,我们后台把用户发来的消息做了一点点的修饰(当然你可以自己想怎么处理和修饰那就看你自己的心情了),然后根据腾讯服务器要求的XML格式重新包装数据返回给服务器,服务器就能成功解读这个数据结构并帮我们把我们的Content中的内容返回给客户端了。
PS:这里我是用一个SpringBoot项目,利用用到了一些工具包,方便读者使用,我把我的pom.xml文件代码贴出来,读者自行选取借鉴使用,本次学习就到此为止,有疏忽错误的地方请在下面留言,我会第一时间答复。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xu</groupId> <artifactId>wemall</artifactId> <version>v0.6.1-beta-build20190718</version> <packaging>jar</packaging> <name>wemall</name> <description>this is a wemall project</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <swagger-ui.version>2.9.2</swagger-ui.version> <swagger2.version>2.9.2</swagger2.version> </properties> <dependencies> <!-- spring-boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- 使thymeleaf支持h5标签 --> <dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>1.9.22</version> </dependency> <!-- log related --> <dependency> <!-- exclude掉spring-boot的默认log配置 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--aspectj--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <!-- 表示开发的时候引入,发布的时候不会加载此包 --> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> <version>1.3.2</version> </dependency> <!-- mybatis-plus begin --> <!--mybatis-plus自动的维护了mybatis以及mybatis-spring的依赖,在springboot中这三者不能同时的出现,避免版本的冲突,表示:跳进过这个坑--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.1.0</version> </dependency> <!--swagger2--> <dependency> <