🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 微信服务接入教程文档 [TOC] **前言** 本人不喜欢网络上的各种文章,上来就把完整代码放脸上,让你琢磨不懂,这篇文章会尽量让你不要出现这种情况。 # 微信开发技术文档官网 [https://mp.weixin.qq.com/](https://mp.weixin.qq.com/) # 注册成为微信开发者 * 普通用户建议注册订阅号 * 打开基本配置菜单,成为开发者 ## 配置接受推送消息服务器 ![](https://img.kancloud.cn/5b/31/5b31ab7babc850ba31d023216b5f7f9d_949x754.png) * URL必须有域名,且必须在外网可以访问到,因此我们需要**natapp**来进行**内网穿透**,简单点说就是把外网的IP映射到你当前的内网下,外网ip接受的内容会被转到你映射的内网ip:端口下。 * Token 随便填 * key随便填 * 建议选择明文模式 * **此时提交之后由于后端没有对应的服务,所有无法成功,待下文可以开始提交的时候建议大家再进行提交** # natapp下载,使用 官网:[https://natapp.cn](https://natapp.cn) ![](https://img.kancloud.cn/eb/8f/eb8f8b57e291614afc2ea44193e86702_1884x88.png) 1. 点击客户端下载即可,先不用急着点教程/文档,里面并没有我们当前想要的 ![](https://img.kancloud.cn/f4/d5/f4d5f532fad29706630ce8a9884dd624_1097x672.png) 2. 选择系统对应的版本,这里使用的是win64,之后点击箭头指向的地方进行快速的安装使用教程! 3. 安装教程里面第一步是需要注册 1. ![](https://img.kancloud.cn/db/7a/db7a4d0efa088813b1fd2199d8af65a6_1206x349.png) *购买隧道是有免费的和付费的,但是免费的需要进行支付宝实名认证,乍一看似乎能白嫖,但是我本人使用的时候,一到支付宝登陆的时候就显示我支付宝账号有危险,要我改密码,死活上不去,无奈只好购买付费的。* *我也对客服进行了邮箱反馈,但是只是建议我进行付费的购买,并没有说排查错误什么的!* 2. ![](https://img.kancloud.cn/b7/64/b7648bf26e7cfa626c2bfa519d74319c_1346x818.png) 隧道协议选择Web即可 二级域名如果没有先选择不需要,如果你有自然更好。 本地端口填写一个即可,本人这里选择的80 带宽&流量选择**小流量即可** 购买完之后进行注册域名 4. ![](https://img.kancloud.cn/49/56/495640b4a1c8ca9d4f367dad80c192b6_1530x772.png) 选择一个可用于微信开发的即可 之后再回到我的隧道里面进行绑定域名即可 # 后台构建 **构建之前,请大家大致的看微信开发平台文档一遍,比避免有些地方不懂**(强烈告诫至少看一遍) ![](https://img.kancloud.cn/06/39/06392854924abd696546a93412bc88f7_335x848.png) **本教程只需要大家看以上四个大章即可** **本教程以大家都有一个springboot的基本启动程序,为前提(能启动,controller能返回个hello,word即可)** 先给出本教程需要的全部**maven**包 ---jdk 1.8 开发工具 **idea** ``` <!--devtools热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>true</scope> </dependency> <!-- xml --> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.11.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- fastJSON --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency> ``` ## 后台配置微信推送消息服务器 ``` import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; import com.alibaba.fastjson.JSON; //引用的其他自建类包,这里就不列出了 @Slf4j @RestController @RequestMapping("/springsecurity/test") public class HelloController { @GetMapping("/weixin") public String weixin(HttpServletRequest request, HttpServletResponse response){ return null; } } ``` 先定义这样的一个类,加上一个接受微信推送消息服务器接口匹配的方法(暂时不需要定义其他类) ![](https://img.kancloud.cn/4b/88/4b889bf1f8d6b91bd45e4fe5d596d9d6_1054x827.png) * 按要求填写这些输入框 * 建议大家先点击提交,查看是否已经进入此方法 完整代码: ``` @GetMapping("/weixin") public String weixin(HttpServletRequest request, HttpServletResponse response){ String echostr = null; //token验证代码段 try{ log.info("请求已到达,开始校验token"); //这里对应的文档里面的几个参数,如果不清楚,请查看文档 if (StringUtils.isNotBlank(request.getParameter("signature"))) { String signature = request.getParameter("signature"); String timestamp = request.getParameter("timestamp"); String nonce = request.getParameter("nonce"); echostr = request.getParameter("echostr"); log.info("signature[{}], timestamp[{}], nonce[{}], echostr[{}]", signature, timestamp, nonce, echostr); if (SignUtil.checkSignature(signature, timestamp, nonce)) { log.info("数据源为微信后台,将echostr[{}]返回!", echostr); response.getOutputStream().println(echostr); return echostr; } } }catch (IOException e){ log.error("校验出错"); e.printStackTrace(); } return echostr; } ``` ## 接受消息推送 接受信息推送这里需要进行新加类: 1. 新建**utils** package(包) 2. 在utils包下新建**messagehandle**包 3. 在messagehandle包下,新建**ParseXml**类内容为: ``` import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.SAXReader; //引用的其他自建类包,这里就不列出了 //此部分代码借鉴自网络 @Slf4j public class ParseXml { /** * @author: wwy * @description: 解析微信发来的请求(XML) * @date 2021/1/20 * @param request * @return java.util.Map<java.lang.String,java.lang.String> ** java.util.Map<java.lang.String,java.lang.String> **/ 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()); log.info("name:" + e.getName() + " value:"+map.get(e.getName())); } // 释放资源 inputStream.close(); return map; } } ``` 4. 然后在上文的**HelloController**中新建方法如下: ``` @PostMapping("/weixin") public void message(HttpServletRequest request, HttpServletResponse response){ try { Map<String, String> paramMap = ParseXml.parseXml(request); log.info(JSON.toJSONString(paramMap)); //这里把request参数传入parsexml方法进行xml到map对象的转换。 //使用map接受返回值即可 //建议大家先发给公众号消息,查看查看这玩意长什么样子! //当然我们parseXml里面已经打印出来了 } catch (Exception e) { e.printStackTrace(); } } ``` ![](https://img.kancloud.cn/d2/dd/d2dde7760f60469e662c2c8d5f7beb13_674x365.png) 附图供大家看! 根据官方文档可知: ![](https://img.kancloud.cn/0c/f1/0cf1084e878794521f7206e757ed2721_696x525.png) 基本每一个消息都会有一个MsgType来确定是什么类型的,所以我们这里要获取它: ``` Map<String, String> paramMap = ParseXml.parseXml(request); String type = paramMap.get("MsgType"); ``` ![](https://img.kancloud.cn/b2/81/b2810e225ca4ee0ace91058c89c23ac9_1484x841.png) 看**被动回复用户消息**章节文档,我们最后是需要返回xml形式的给微信服务器。 所以我们的大体步骤是: 1. **获取微信服务器的请求,解析xml为对象** 2. **操作对象,获取请求类型(比如文本、图片等)进行相应的处理,进行数据的变更** 3. **把对象变为xml形式返回** ## *操作对象,获取请求类型(比如文本、图片等)进行相应的处理,进行数据的变更处理* **操作对象** * 首先每个返回的事件类型有一个自己的返回格式,但是都有几个共同的字段,所以我们可以先定一个实体基类,然后其他的各种格式来继承它,获取共同的属性(**这里的思路出自网络代码,但是网络代码完整性,实用性比较低,扩展性也不行所以我进行了大幅的修改**)。 * 实体基类**BaseMessage**代码如下(请自行新建包来进行放置,建议不要和utils放置在一起): ``` import lombok.Data; @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; } ``` * 文字消息实体类**TextMessage**: ``` @Data public class TextMessage extends BaseMessage{ /** * 消息内容 */ private String Content; //由于打印本类toString时只会打印本类有的属性,不会打印父类的,所以我们需要重写类的toString,加上本类属性和父类属性 public String toString(){ return super.toString() + "[TextMessage]:" + " Content:" + this.Content; } } ``` * 图片消息实体类**ImageMessage**: ``` @Data public class ImageMessage extends BaseMessage { /** * 图片链接 */ private String PicUrl; /** * 图片消息媒体id,可以调用获取临时素材接口拉取数据。 */ private String MediaId; public String toString(){ return super.toString() + "[ImageMessage]:" + " PriUrl:" + this.PicUrl + " MediaId:" + this.MediaId; } } ``` * 语音消息实体类**VoiceMessage**: ``` @Data public class VoiceMessage extends BaseMessage { /** * 语音消息媒体id,可以调用获取临时素材+接口拉取数据。 */ private String MediaId; /** * 语音格式,如amr,speex等 */ private String Format; public String toString(){ return super.toString() + "[VoiceMessage]:" + " MediaId:" + this.MediaId + " Format:" + this.Format; } } ``` * 视频消息实体类**VideoMessage**: ``` @Data public class VideoMessage extends BaseMessage{ /** * 视频消息媒体id,可以调用获取临时素材接口拉取数据。 */ private String MediaId; /** * 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据 */ private String ThumbMediaId; public String toString(){ return super.toString() + "[VideoMessage]:" + " MediaId:" + this.MediaId + " ThumbMediaId:" + this.ThumbMediaId; } } ``` * 暂时不提供小视频处理类,请自行添加 * 地理位置处理类**LocationMessage**: ``` @Data public class LocationMessage extends BaseMessage { /** * 地理位置维度 */ private String Location_X; /** * 地理位置经度 */ private String Location_Y; /** * 地图缩放大小 */ private String Scale; /** * 地理位置信息 */ private String Label; public String toString(){ return super.toString() + "[LocationMessage]:" + " Location_X:" + this.Location_X + " Location_Y:" + this.Location_Y + " Scale:" + this.Scale + " Lable:" + this.Label; } } ``` * 链接消息实体类**LinkMessage**: ``` @Data public class LinkMessage extends BaseMessage{ /** * 消息标题 */ private String Title; /** * 消息描述 */ private String Description; /** * 消息链接 */ private String Url; public String toString(){ return super.toString() + "[LinkMessage]:" + " Title:" + this.Title + " Description:" + this.Description + " Url:" + this.Url; } } ``` * 现在有了对象之间的映射但是还少一个判断是什么事件类型的枚举类,请在**utils**包下新建**code**包 * 在**code**包下新建**MessageCode**类 * 类如下(这个其实还少一两个,但是懒的加了,有需要的自己加上,此代码完全来自网上,没有多少需要修改的): ``` @Slf4j public class MessageCode { /** * 请求消息类型:文本 */ 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"; } ``` * 此时我们就可以在helloController里面进行事件类型的判断了,代码如下: ``` try { //try-catch后面会用到 Map<String, String> paramMap = ParseXml.parseXml(request); String type = paramMap.get("MsgType"); //处理消息事件 if(MessageCode.REQ_MESSAGE_TYPE_TEXT.equals(type)){ log.warn("进入消息事件!"); } } catch (Exception e) { e.printStackTrace(); } } ``` * 这里我们已经进行成功的判断了,那么接下来就需要根据传来的参数对象,进行返回参数xml的构建 * **utils**包新建**msghandle**包 * msghandle包下,新建**MsgHandle**,**MsgHelpClass**2个类 * MsgHandle : 处理消息的分发,因为每一个消息类型的返回值,各有个的特点 * MsgHelpClass:每一个消息类型的返回值,虽然各有各的特点,但是依然有共同的地方,用于辅助**MsgHandle**类 ![](https://img.kancloud.cn/ed/05/ed051145cbede77f395883c229b6ee5e_1052x463.png) ![](https://img.kancloud.cn/84/10/841088b466de222fb8cf480bc0980654_917x453.png) * **MsgHandle** 代码如下: ``` import java.util.Date; import java.util.Map; import lombok.Data; import lombok.extern.slf4j.Slf4j; import com.alibaba.fastjson.JSONObject; //自建包请自导入 @Data @Slf4j public class MsgHandle { /** * 发送方账号(一个openId) */ private String FromUserName; /** * 开发者微信号 */ private String ToUserName; /** * 消息创建时间 */ private long CreateTime; /** * 消息类型: * 文本:text * 图片:image * 语音:voice * 视频:video * 小视频:shortvideo * 地理位置:location * 链接:link */ private String MsgType; /** * 消息id,64位整数 */ private long MsgId; } ``` * 此类有4个共同的属性,下面的代码直接加在此类中即可,分开只是为了更好理解,避免第一眼看上去太多,难以理解 ``` public String processMessage(Map<String, String> map) throws InstantiationException, IllegalAccessException{ //首先对自己的私有属性进行赋值,接着创建基类实体对象, this.FromUserName = map.get("ToUserName"); //!!!!!这里是调换的 //特别说明这里,看上面两个图中,我们会发现关于ToUserName的说明是收到openId,所以这里是调换的! this.ToUserName = map.get("FromUserName"); this.MsgType = map.get("MsgType"); this.CreateTime = Long.valueOf(map.get("CreateTime")); this.MsgId = Long.valueOf(map.get("MsgId")); BaseMessage baseMessage = null; //目前只支持文字消息回复 //用枚举获取是什么类型,再进入里面进行具体操作 if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_TEXT)) { // 文本消息 log.info("这是文本消息!"); //这里用到了MsgHelpClass的方法,请看下文的此方法代码 //参数1:this processMessage对象即可 //参数2:对应消息处理类即可 baseMessage = MsgHelpClass.setAttribute(this, TextMessage.class); //向下转型,小心,如果报了什么class异常,就是这里的问题。 TextMessage textMessage = (TextMessage) baseMessage; //向文字消息实体类添加私有的属性数据 textMessage.setContent("这里是测试回复"); //这里为生成xml数据的类,需要我们提供一个要生成xml数据的实体类,下文放代码 return ParseXml.textMessageToXml(textMessage); } if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_IMAGE)) { // 图片消息 log.info("这是图片消息!"); } if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_LINK)) { // 链接消息 log.info("这是链接消息!"); } if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_LOCATION)) { // 位置消息 log.info("这是位置消息!"); } if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_VIDEO)) { // 视频/小视频消息 log.info("这是视频消息!"); } if (this.MsgType.equals(MessageCode.REQ_MESSAGE_TYPE_VOICE)) { // 语音消息 log.info("这是语音消息!"); } return ""; } ``` * **MsgHelpClass**类代码如下: ``` import java.util.Date; import lombok.extern.slf4j.Slf4j; //自建类请自行导入 @Slf4j public class MsgHelpClass { //其实这里多加一个这个方法,而不是在processmessage里面直接进行转型,是因为会报转型错误的异常,用instanceof这里不太管用。 //方法讲解: //规定第一个参数必须为 MsgHandle对象,用他的私有属性给实体基类进行赋值 //规定第二个参数必须为继承自BaseMessage基类的子类,用来向下转型 public static <E extends BaseMessage>E setAttribute(MsgHandle msgHandle,Class<E> eClass) throws IllegalAccessException, InstantiationException { //newInstance 获取对象,相当于new 对象 BaseMessage baseMessage = eClass.newInstance(); baseMessage.setCreateTime(new Date().getTime()); baseMessage.setFromUserName(msgHandle.getFromUserName()); baseMessage.setMsgId(msgHandle.getMsgId()); baseMessage.setToUserName(msgHandle.getToUserName()); baseMessage.setMsgType(msgHandle.getMsgType()); log.warn("MsgHelpClass-setAttribute方法返回值如下:\n" + baseMessage.toString()); return (E) baseMessage; } } ``` * 上面两个类就是处理分发的类,但是上面我们也提到了一个把数据处理返回为xml,ParseXml类的静态方法 * 此方法在ParseXml类中为: ``` public static String textMessageToXml(TextMessage textMessage) { log.warn("ParseXml类TextMressage对象值如下:\n" + textMessage.toString()); //真正用来处理的方法,也在本类中 return XmlHandleFun(textMessage); } //图片处理 暂时无实现,请后续自行实现 public static String imageMessageToXml(ImageMessage imageMessage) { log.warn("ParseXml类ImageMessage对象值如下:\n" + imageMessage.toString()); return ""; } //音频处理 暂时无实现,请后续自行实现 public static String voiceMessageToXml(VoiceMessage voiceMessage) { log.warn("ParseXml类VoiceMessage对象值如下:\n" + voiceMessage.toString()); return ""; } //视频处理 暂时无实现,请后续自行实现 public static String videoMessageToXml(VideoMessage videoMessage) { log.warn("ParseXml类VideoMessage对象值如下:\n" + videoMessage.toString()); return ""; } //其余无写的,请自行添加 ``` * 上文提到的**XmlHandleFun**方法代码如下: ``` //本类讲解,参数必须是一个继承自baseMessage基类的子类对象 //返回值为String,生成的xml数据我们需要的string类型的,返回给服务器的也是String类型的 //本文需要一定的dom4j知识,推荐大家去https://www.cnblogs.com/qqran/p/12520901.html这里进行速成,很快几分钟 //当然不想看的话,后面也有简单的介绍 private static <T extends BaseMessage> String XmlHandleFun(T object){ //Document对象,后续用他生成xml结构,并调用他的方法进行string类型数据返回 Document dou = null; //用来判断下文的if是否还继续判断,当然这里这个判断是重复的,大家可以在看懂此代码块之后,自己决定是否删除 boolean isif = true; try { //开始创建dom结构 dou = DocumentHelper.createDocument(); //dou.addElement 创建唯一的全局父节点,根据官方文档的格式,返回的xml'格式基本只有最多3级 Element root = dou.addElement("xml"); //root.addElement 在root节点下,创建一个节点,相当于二级节点<ToUserName></ToUserName> //attText 为添加二级节点的内容,<ToUserName>内容</ToUserName> //补充知识:如果要给此行添加xml属性,使用如下代码-> root.addAttribute("id", "属性"); //值为<xml id="属性"></xml> //获取对象的值,进行字符串拼接 Element emp = root.addElement("ToUserName").addText("<![CDATA[" + object.getToUserName() + "]]>"); Element emp1 = root.addElement("FromUserName").addText("<![CDATA[" + object.getFromUserName() + "]]>"); Element emp2 = root.addElement("CreateTime").addText(String.valueOf(object.getCreateTime())); Element emp3 = root.addElement("MsgType").addText("<![CDATA[" + object.getMsgType() + "]]>"); //判断传入的对象是否是它的实例 //是的话进行转型,并添加属于自己类的特有的属性! if(object instanceof TextMessage && isif){ TextMessage textMessage = (TextMessage) object; Element emp4 = root.addElement("Content").addText("<![CDATA[" + textMessage.getContent() + "]]>"); isif = false; } if(object instanceof ImageMessage && isif){ ImageMessage imageMessage = (ImageMessage) object; Element emp4 = root.addElement("Image"); emp4.addElement("MediaId").addText("<![CDATA[" + imageMessage.getMediaId() + "]]>"); isif = false; } if(object instanceof VoiceMessage && isif){ VoiceMessage voiceMessage = (VoiceMessage) object; Element emp4 = root.addElement("Voice"); emp4.addElement("MediaId").addText("<![CDATA[" + voiceMessage.getMediaId() + "]]>"); isif = false; } if(object instanceof VideoMessage && isif) { VideoMessage videoMessage = (VideoMessage) object; Element emp4 = root.addElement("Video"); emp4.addElement("MediaId").addText("<![CDATA[" + videoMessage.getMediaId() + "]]>"); emp4.addElement("Title").addText("<![CDATA[" + videoMessage.getTitle() + "]]>"); emp4.addElement("Description").addText("<![CDATA[" + videoMessage.getDescription() + "]]>"); isif = false; } }catch(Exception e){ log.error("出现错误!XmlHandleFun"); e.printStackTrace(); } //生成的xml是附带<?xml version="1.0" encoding="UTF-8"?>此行的,我还并没有测试带上返回给微信服务器是否可行,当前没被注释的是去除此行的,如果使用注释的一行则是直接返回生成的,带上此头部的 int count = "encoding=\"UTF-8\"?".length(); String result = dou.asXML(); result = result.substring(result.indexOf("encoding=\"UTF-8\"?") + count + 1); return result.trim(); // return dou.asXML(); } ``` ## 数据返回 以上配置完成之后我们就可以进行HelloController剩下的部分了 ``` //我们只需要在原有的判断条件下增加如下的代码: if(MessageCode.REQ_MESSAGE_TYPE_TEXT.equals(type)){ MsgHandle msgHandle = new MsgHandle(); ResultRes.response(msgHandle.processMessage(paramMap),response); } ``` * 看到这里你可以会说这**ResultRes.response**又是个什么东西,都配置怎么多了怎么还有,我答应你们这真的是最后一个了,别骂了别骂了! * 在**utilsl**包新建**reresult**包(我承认我这个名字不规范,不合适 ......) * 在**reresult**包下新建**ResultRes**类 * ResultRes类代码如下: ``` import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; import java.io.StringWriter; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringEscapeUtils; import org.dom4j.Document; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; import org.thymeleaf.util.StringUtils; @Slf4j public class reresult { public static void response(String data, HttpServletResponse response) { //用此方法进行xml文件的转义和format效果类似,因为上文的代码中,返回的string数据里面的,< >符合已经被转义无法正常传输给微信服务器识别 data = StringEscapeUtils.unescapeXml(data); log.info(data); //如果数据为空,直接返回 if(StringUtils.isEmpty(data)){ log.error("数据为空!"); return; } try { //进行编码规定,返回数据! response.setCharacterEncoding("UTF-8"); PrintWriter printWriter = response.getWriter(); printWriter.print(data); printWriter.close(); }catch (IOException io){ log.error(io.getMessage()); io.printStackTrace(); } } /** * @Title: format * @Description: 格式化输出xml字符串 * @param str * @return String * @throws Exception * 这里的代码是网上的,本来要用的,但是也最后没用,是用来解决xml里面的<>符合传输的时候被转义的问题 */ public static String format(String str) throws Exception { SAXReader reader = new SAXReader(); // 创建一个串的字符输入流 StringReader in = new StringReader(str); Document doc = reader.read(in); // 创建输出格式 OutputFormat formater = OutputFormat.createPrettyPrint(); // 设置xml的输出编码 formater.setEncoding("utf-8"); // 创建输出(目标) StringWriter out = new StringWriter(); // 创建输出流 XMLWriter writer = new XMLWriter(out, formater); // 输出格式化的串到目标中,执行后。格式化后的串保存在out中。 writer.write(doc); writer.close(); // 返回格式化后的结果 return out.toString(); } } ``` 到这里正常情况下,已经可以对用户输入的普通文本消息进行自动回复了! ## 自定义菜单 * 在**HelloController**类**message(get)**方法里,`String type = paramMap.get("MsgType");下`加入 * ~~~ String event = null; //获取自定义点击/推送事件 if(paramMap.get("Event") != null){ event  = paramMap.get("Event"); } ~~~ * 获取事件字符串之后,我们需要进行匹配,来进入相应的处理 ### 订阅消息事件,返回创建自定义菜单json数据 * 在**message(get)**方法里加入 ``` //订阅消息事件 if(MessageCode.EVENT_TYPE_SUBSCRIBE.equals(event)){ String token = GetBodyMessage.getAcces_Token("wx59fa3e56c3448f46"); String body = GetBodyMessage.getBodyJson(); String s = httpsRequest.httpsRequests("https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" + token, "POST", body); System.err.println(s); } ``` * 我们需要新建**GetBodyMessage**类,代码如下: ``` import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import java.util.HashMap; import java.util.Map; import lombok.extern.slf4j.Slf4j; @Slf4j public class GetBodyMessage { //添加菜单自定义json数据 public static String getBodyJson(){ Map<String, Object> mapbutton = new HashMap<>(); Map<String, Object> mapbody = new HashMap<>(); Map [] strargs = new Map[1]; mapbody.put("type","view"); mapbody.put("name","成绩查询!"); mapbody.put("key","message"); mapbody.put("url","http://www.baidu.com"); strargs[0] = mapbody; mapbutton.put("button", strargs); String body = JSON.toJSONString(mapbutton); log.error(body); //自定义菜单官网返回数据为json数据! return body; } //获取access_token public static String getAcces_Token(String appid){ String token = "grant_type=client_credential&appid=" + appid +"&secret=xxxxxxxxxxxxxxxxxxxxxx"; //httpRequest代码在下文 String result = httpRequest.httpRequests("https://api.weixin.qq.com/cgi-bin/token","GET",token); Map map = JSONArray.parseObject(result); System.out.println(map.get("access_token")); return (String) map.get("access_token"); } } ``` * **getBodyJson**方法返回的json结构如下:** ![](https://img.kancloud.cn/51/db/51db0f5d8b572452aa09e30aa07c8c52_570x333.png) * **官网token获取请求实例如下:** ![](https://img.kancloud.cn/dc/d0/dcd07604cf7370400619f8425400ee3b_545x93.png) * 创建**httpRequest**类和**httpsRequest**类,代码如下(代码源自网络): * 网址:[链接](https://www.cnblogs.com/ncy1/p/9684330.html) ``` import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; public class httpRequest { //处理http请求 requestUrl为请求地址 requestMethod请求方式,值为"GET"或"POST" public static String httpRequests(String requestUrl,String requestMethod,String outputStr){ StringBuffer buffer=null; try{ URL url=new URL(requestUrl); HttpURLConnection conn=(HttpURLConnection)url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setRequestMethod(requestMethod); conn.connect(); //往服务器端写内容 也就是发起http请求需要带的参数 if(null!=outputStr){ OutputStream os=conn.getOutputStream(); os.write(outputStr.getBytes("utf-8")); os.close(); } //读取服务器端返回的内容 InputStream is=conn.getInputStream(); InputStreamReader isr=new InputStreamReader(is,"utf-8"); BufferedReader br=new BufferedReader(isr); buffer=new StringBuffer(); String line=null; while((line=br.readLine())!=null){ buffer.append(line); } }catch(Exception e){ e.printStackTrace(); } return buffer.toString(); } } ``` ``` import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; public class httpsRequest { /* * 处理https GET/POST请求 * 请求地址、请求方法、参数 * */ public static String httpsRequests(String requestUrl,String requestMethod,String outputStr){ StringBuffer buffer=null; try{ //创建SSLContext SSLContext sslContext=SSLContext.getInstance("SSL"); TrustManager[] tm={new MyX509TrustManager()}; //初始化 sslContext.init(null, tm, new java.security.SecureRandom());; //获取SSLSocketFactory对象 SSLSocketFactory ssf=sslContext.getSocketFactory(); URL url=new URL(requestUrl); HttpsURLConnection conn=(HttpsURLConnection)url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setUseCaches(false); conn.setRequestMethod(requestMethod); //设置当前实例使用的SSLSoctetFactory conn.setSSLSocketFactory(ssf); conn.connect(); //往服务器端写内容 if(null!=outputStr){ OutputStream os=conn.getOutputStream(); os.write(outputStr.getBytes("utf-8")); os.close(); } //读取服务器端返回的内容 InputStream is=conn.getInputStream(); InputStreamReader isr=new InputStreamReader(is,"utf-8"); BufferedReader br=new BufferedReader(isr); buffer=new StringBuffer(); String line=null; while((line=br.readLine())!=null){ buffer.append(line); } }catch(Exception e){ e.printStackTrace(); } return buffer.toString(); } } ``` https请求如果报错,可能还需要导入服务端的安全证书步骤如下: 1. 例:点击下图中红线指向的小锁(谷歌浏览器) ![](https://img.kancloud.cn/b7/74/b7741e4d587b1f151aeb3ec46d045322_418x332.png) 2. 点击**证书**一行 3. 点击**详细信息** ![](https://img.kancloud.cn/35/3e/353e46980337c4cdee0af98b60a59669_607x758.png) 4. 点击**复制到文件** ![](https://img.kancloud.cn/9e/33/9e33096d0078e3a774d7b356c6ebb71c_578x728.png) 5. 先**下一步**然后来到下图页面 ![](https://img.kancloud.cn/70/c5/70c54f472bb91fd34071f084f4d6d03c_764x714.png) 6. 选择**DER编码二进制X.509**后点击**下一步** 7. 随意选择一个文件名和文件路径。 8. 打开cmd进入刚刚创建的文件路径,命令行输入Keytool -import -alias 文件名 -file 文件名.cer -keystore cacerts 9. 回车之后会让输入口令,一般口令默认是**changeit**。输入密钥时并不会显示在界面上。 10. 输入正确之后会让你选择是否信任该证书,输入y,会提示导入成功! * 完成之后,用户订阅该公众号之后就会出现菜单。 ### 用户点击自定义菜单事件 && 用户点击自定义链接事件 * 由于上文新建的菜单里面包括了**用户点击自定义菜单事件 && 用户点击自定义链接事件**这两个,所以我们只需要添加如下代码,观察到用户的操作即可: ``` //用户点击自定义菜单事件 if(MessageCode.EVENT_TYPE_CLICK.equals(event)){ //事件KEY值,与自定义菜单接口中KEY值对应 String eventKey = "message"; log.warn(JSON.toJSONString(paramMap)); if(eventKey.equals(paramMap.get("EventKey"))){ log.warn("进入成绩查询点击事件,开始处理并返回!"); } } //用户点击自定义链接事件 if(MessageCode.EVENT_TYPE_VIEW.equals(event)){ log.warn("进入百度页面跳转事件!"); } ``` ![](https://img.kancloud.cn/06/fd/06fd425efcc814a56787d7d19e213317_755x513.png)