关闭

微信开发学习总结(三)——开发微信公众号的最基本功能——普通消息的接收和回复

983人阅读 评论(0) 收藏 举报
在上篇《微信开发学习总结(二)——微信开发入门》我们介绍了微信公众平台的基本原理,如何接入微信公众号,如何保持access_token的长期有效性以及进行了简单的文本消息测试,本篇再来具体细说一如何实现微信公众号的最基本功能:普通消息的接收和回复。

一、微信公众平台消息管理接口介绍

  要实现微信公众号的普通消息的接收和回复,我们需要先熟悉微信公众平台API中消息接口部分,点此进入,点击后将进入到【消息管理】部分,如下图所示:

  

  对于普通消息的接收和回复我们只需要关注上图中的"接收消息——接收普通消息"和"发送消息——被动回复消息"

1.1、消息接收

  先来说说接收消息, 当普通微信用户向公众账号发消息时,微信服务器会先接收到用户发送的消息,然后将用户消息按照指定的XML格式组装好数据,最后POST消息的XML数据包到开发者填写的URL上。

  接收到的普通消息的消息类型目前有以下几种:

  1 文本消息
  2 图片消息
  3 语音消息
  4 视频消息
  5 小视频消息
  6 地理位置消息
  7 链接消息

  每一种消息类型都有其指定的XML数据格式,这7种消息的xml格式请到官方文档查看,有具体的格式定义和属性说明。格式很简单,基本共有属性包括ToUserName、FromUserName、CreateTime、MsgType、MsgId,并且每种类型有自己特殊的属性。

  接收消息的过程其实就是获取post请求的这个xml,然后对这个xml进行分析的过程。post请求的入口还是之前提到的微信公众号接入的那个地址,整个公众号的所有请求都会走这个入口,只是接入时是get请求,其它情况下是post请求。

1.2、消息回复

  微信服务器在将用户的消息发给公众号的开发者服务器地址后,会等待开发者服务器回复响应消息。微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。

  假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示。详见下面说明:

  1、(推荐方式)直接回复success
  2、直接回复空串(指字节长度为0的空字符串,而不是XML结构体中content字段的内容为空)

 一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:

  1、开发者在5秒内未回复任何内容
  2、开发者回复了异常数据,比如JSON数据等

  另外,请注意,回复图片等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。

  消息回复目前支持回复文本、图片、图文、语音、视频、音乐,每一种类型的消息都有特定的XML数据格式。这几种回复消息的xml数据格式请参考官方文档,有具体的格式定义和属性说明。格式很简单,基本共有属性包括ToUserName、FromUserName、CreateTime、MsgType并且每种类型有自己特殊的属性。

 二、微信公众号的普通消息的接收和回复

2.1、接收消息

  接收消息和被动回复消息这两个动作是不分家的,这本来就是一个交互场景,一般情况就是公众号通过分析接收到的消息,会给出对应的回复。

  之前说过了,接收消息的过程其实就是获取微信服务器通过post请求的发送给我们公众号服务器的xml数据,然后我们的公众号服务器再对这个xml进行解析处理的过程。为了方便解析XML数据,我们借助于dom4j,dom4j是一个十分优秀的JavaXML API,具有性能优异、功能强大和极其易使用的特点,是用来读写XML文件的。针对微信服务器发来的xml请求数据,我们写一个parseXml方法来处理,parseXml方法的代码如下:

 1  /**
 2      * 解析微信发来的请求(XML)
 3      *
 4      * @param request 封装了请求信息的HttpServletRequest对象
 5      * @return map 解析结果
 6      * @throws Exception
 7      */
 8     public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
 9         // 将解析结果存储在HashMap中
10         Map<String, String> map = new HashMap<String, String>();
11         // 从request中取得输入流
12         InputStream inputStream = request.getInputStream();
13         // 读取输入流
14         SAXReader reader = new SAXReader();
15         Document document = reader.read(inputStream);
16         // 得到xml根元素
17         Element root = document.getRootElement();
18         // 得到根元素的所有子节点
19         List<Element> elementList = root.elements();
20 
21         // 遍历所有子节点
22         for (Element e : elementList) {
23             System.out.println(e.getName() + "|" + e.getText());
24             map.put(e.getName(), e.getText());
25         }
26 
27         // 释放资源
28         inputStream.close();
29         inputStream = null;
30         return map;
31     }

  然后在处理微信请求的入口servlet的doPost方法中调用parseXml方法即可,调用代码如下:

 1  /**
 2      * 处理微信服务器发来的消息
 3      */
 4     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 5         // TODO 接收、处理、响应由微信服务器转发的用户发送给公众帐号的消息
 6         // 将请求、响应的编码均设置为UTF-8(防止中文乱码)
 7         request.setCharacterEncoding("UTF-8");
 8         response.setCharacterEncoding("UTF-8");
 9         System.out.println("请求进入");
10         String responseMessage;
11         try {
12             //解析微信发来的请求,将解析后的结果封装成Map返回
13             Map<String,String> map = MessageHandlerUtil.parseXml(request);
14             System.out.println("开始构造响应消息");
15             responseMessage = MessageHandlerUtil.buildResponseMessage(map);
16             System.out.println(responseMessage);
17             if(responseMessage.equals("")){
18                 responseMessage ="未正确响应";
19             }
20         } catch (Exception e) {
21             e.printStackTrace();
22             System.out.println("发生异常:"+ e.getMessage());
23             responseMessage ="未正确响应";
24         }
25         //发送响应消息
26         response.getWriter().println(responseMessage);
27     }

  这样我们就完成了消息的接收,消息接收之后,我们就要根据消息类型进行响应了,写一个根据消息类型构造返回消息的方法,代码如下:

 1 /**
 2      * 根据消息类型构造返回消息
 3      * @param map 封装了解析结果的Map
 4      * @return responseMessage(响应消息)
 5      */
 6     public static String buildResponseMessage(Map map) {
 7         //响应消息
 8         String responseMessage = "";
 9         //得到消息类型
10         String msgType = map.get("MsgType").toString();
11         System.out.println("MsgType:" + msgType);
12         //消息类型
13         MessageType messageEnumType = MessageType.valueOf(MessageType.class, msgType.toUpperCase());
14         switch (messageEnumType) {
15             case TEXT:
16                 //处理文本消息
17                 responseMessage = handleTextMessage(map);
18                 break;
19             case IMAGE:
20                 //处理图片消息
21                 responseMessage = handleImageMessage(map);
22                 break;
23             case VOICE:
24                 //处理语音消息
25                 responseMessage = handleVoiceMessage(map);
26                 break;
27             case VIDEO:
28                 //处理视频消息
29                 responseMessage = handleVideoMessage(map);
30                 break;
31             case SHORTVIDEO:
32                 //处理小视频消息
33                 responseMessage = handleSmallVideoMessage(map);
34                 break;
35             case LOCATION:
36                 //处理位置消息
37                 responseMessage = handleLocationMessage(map);
38                 break;
39             case LINK:
40                 //处理链接消息
41                 responseMessage = handleLinkMessage(map);
42                 break;
43             case EVENT:
44                 //处理事件消息,用户在关注与取消关注公众号时,微信会向我们的公众号服务器发送事件消息,开发者接收到事件消息后就可以给用户下发欢迎消息
45                 responseMessage = handleEventMessage(map);
46             default:
47                 break;
48         }
49         //返回响应消息
50         return responseMessage;
51     }

  这样我们就完成了根据消息类型进行响应了,在处理微信请求的入口servlet的doPost方法中调用buildResponseMessage方法即可,doPost方法的完整代码在上面已经贴出来了.buildResponseMessage方法中使用到了一个MessageType类,这是一个消息类型枚举类,MessageType类的代码如下:

 1 /**
 2  * 接收到的消息类型
 3  */
 4 public enum MessageType {
 5     TEXT,//文本消息
 6     IMAGE,//图片消息
 7     VOICE,//语音消息
 8     VIDEO,//视频消息
 9     SHORTVIDEO,//小视频消息
10     LOCATION,//地理位置消息
11     LINK,//链接消息
12     EVENT//事件消息
13 }

2.2、回复消息

  下面我基于这样一个业务场景来演示构造回复的消息,接收到文本消息"文本",回复文本消息;接收到“图片”,回复图片消息;接收到“语音”,回复语音消息;接收到“视频”,回复视频消息;接收到“音乐”,回复音乐消息;接收到“图文”,回复图文消息。下面具体说明各种消息的构建,只贴出核心代码,一些辅助代码类请自行下载项目代码参考.

2.2.1、回复文本消息

  接收的文本消息的XML数据格式如下:

1 <xml>
2  <ToUserName><![CDATA[toUser]]></ToUserName>
3  <FromUserName><![CDATA[fromUser]]></FromUserName> 
4  <CreateTime>1348831860</CreateTime>
5  <MsgType><![CDATA[text]]></MsgType>
6  <Content><![CDATA[this is a test]]></Content>
7  <MsgId>1234567890123456</MsgId>
8 </xml>

  回复的文本消息的XML数据格式如下:

1 <xml>
2 <ToUserName><![CDATA[发消息的人,即订阅者]]></ToUserName>
3 <FromUserName><![CDATA[微信公众号本身]]></FromUserName>
4 <CreateTime>消息创建时间(整形)</CreateTime>
5 <MsgType><![CDATA[text]]></MsgType>
6 <Content><![CDATA[消息内容]]></Content>
7 </xml>

  其中接收消息格式中的ToUserName便是回复消息的FromUserName,接收消息格式中的FromUserName便是回复消息的ToUserName。CreateTime为消息发送的时间戳。MsgType为消息类型,文本为text。Content为消息内容。具体每一种类型消息的回复,就是构造此种类型的xml格式内容,格式大同小异,只是音乐、视频、语音、图文格式相对于文本消息构造的xml内容稍微复杂一点。

  写一个构建文本消息的方法,代码如下:

 1  /**
 2      * 构造文本消息
 3      * @param map 封装了解析结果的Map
 4      * @param content 文本消息内容
 5      * @return 文本消息XML字符串
 6      */
 7     private static String buildTextMessage(Map<String, String> map, String content) {
 8         //发送方帐号
 9         String fromUserName = map.get("FromUserName");
10         // 开发者微信号
11         String toUserName = map.get("ToUserName");
12         /**
13          * 文本消息XML数据格式
14          * <xml>
15          <ToUserName><![CDATA[toUser]]></ToUserName>
16          <FromUserName><![CDATA[fromUser]]></FromUserName>
17          <CreateTime>1348831860</CreateTime>
18          <MsgType><![CDATA[text]]></MsgType>
19          <Content><![CDATA[this is a test]]></Content>
20          <MsgId>1234567890123456</MsgId>
21          </xml>
22          */
23         return String.format(
24                 "<xml>" +
25                         "<ToUserName><![CDATA[%s]]></ToUserName>" +
26                         "<FromUserName><![CDATA[%s]]></FromUserName>" +
27                         "<CreateTime>%s</CreateTime>" +
28                         "<MsgType><![CDATA[text]]></MsgType>" +
29                         "<Content><![CDATA[%s]]></Content>" +
30                         "</xml>",
31                 fromUserName, toUserName, getMessageCreateTime(), content);
32     }

  2.2.2、回复图片消息

  写一个构建图片消息的方法,代码如下:

 1  /**
 2      * 构造图片消息
 3      * @param map 封装了解析结果的Map
 4      * @param mediaId 通过素材管理接口上传多媒体文件得到的id
 5      * @return 图片消息XML字符串
 6      */
 7     private static String buildImageMessage(Map<String, String> map, String mediaId) {
 8         //发送方帐号
 9         String fromUserName = map.get("FromUserName");
10         // 开发者微信号
11         String toUserName = map.get("ToUserName");
12         /**
13          * 图片消息XML数据格式
14          *<xml>
15          <ToUserName><![CDATA[toUser]]></ToUserName>
16          <FromUserName><![CDATA[fromUser]]></FromUserName>
17          <CreateTime>12345678</CreateTime>
18          <MsgType><![CDATA[image]]></MsgType>
19          <Image>
20          <MediaId><![CDATA[media_id]]></MediaId>
21          </Image>
22          </xml>
23          */
24         return String.format(
25                 "<xml>" +
26                         "<ToUserName><![CDATA[%s]]></ToUserName>" +
27                         "<FromUserName><![CDATA[%s]]></FromUserName>" +
28                         "<CreateTime>%s</CreateTime>" +
29                         "<MsgType><![CDATA[image]]></MsgType>" +
30                         "<Image>" +
31                         "   <MediaId><![CDATA[%s]]></MediaId>" +
32                         "</Image>" +
33                         "</xml>",
34                 fromUserName, toUserName, getMessageCreateTime(), mediaId);
35     }

  2.2.3、回复音乐消息

  写一个构建音乐消息的方法,代码如下:

 1  /**
 2      * 构造音乐消息
 3      * @param map 封装了解析结果的Map
 4      * @param music 封装好的音乐消息内容
 5      * @return 音乐消息XML字符串
 6      */
 7     private static String buildMusicMessage(Map<String, String> map, Music music) {
 8         //发送方帐号
 9         String fromUserName = map.get("FromUserName");
10         // 开发者微信号
11         String toUserName = map.get("ToUserName");
12         /**
13          * 音乐消息XML数据格式
14          *<xml>
15          <ToUserName><![CDATA[toUser]]></ToUserName>
16          <FromUserName><![CDATA[fromUser]]></FromUserName>
17          <CreateTime>12345678</CreateTime>
18          <MsgType><![CDATA[music]]></MsgType>
19          <Music>
20          <Title><![CDATA[TITLE]]></Title>
21          <Description><![CDATA[DESCRIPTION]]></Description>
22          <MusicUrl><![CDATA[MUSIC_Url]]></MusicUrl>
23          <HQMusicUrl><![CDATA[HQ_MUSIC_Url]]></HQMusicUrl>
24          <ThumbMediaId><![CDATA[media_id]]></ThumbMediaId>
25          </Music>
26          </xml>
27          */
28         return String.format(
29                 "<xml>" +
30                         "<ToUserName><![CDATA[%s]]></ToUserName>" +
31                         "<FromUserName><![CDATA[%s]]></FromUserName>" +
32                         "<CreateTime>%s</CreateTime>" +
33                         "<MsgType><![CDATA[music]]></MsgType>" +
34                         "<Music>" +
35                         "   <Title><![CDATA[%s]]></Title>" +
36                         "   <Description><![CDATA[%s]]></Description>" +
37                         "   <MusicUrl><![CDATA[%s]]></MusicUrl>" +
38                         "   <HQMusicUrl><![CDATA[%s]]></HQMusicUrl>" +
39                         "</Music>" +
40                         "</xml>",
41                 fromUserName, toUserName, getMessageCreateTime(), music.title, music.description, music.musicUrl, music.hqMusicUrl);
42     }

  2.2.4、回复视频消息

  写一个构建视频消息的方法,代码如下:

 1  /**
 2      * 构造视频消息
 3      * @param map 封装了解析结果的Map
 4      * @param video 封装好的视频消息内容
 5      * @return 视频消息XML字符串
 6      */
 7     private static String buildVideoMessage(Map<String, String> map, Video video) {
 8         //发送方帐号
 9         String fromUserName = map.get("FromUserName");
10         // 开发者微信号
11         String toUserName = map.get("ToUserName");
12         /**
13          * 音乐消息XML数据格式
14          *<xml>
15          <ToUserName><![CDATA[toUser]]></ToUserName>
16          <FromUserName><![CDATA[fromUser]]></FromUserName>
17          <CreateTime>12345678</CreateTime>
18          <MsgType><![CDATA[video]]></MsgType>
19          <Video>
20          <MediaId><![CDATA[media_id]]></MediaId>
21          <Title><![CDATA[title]]></Title>
22          <Description><![CDATA[description]]></Description>
23          </Video>
24          </xml>
25          */
26         return String.format(
27                 "<xml>" +
28                         "<ToUserName><![CDATA[%s]]></ToUserName>" +
29                         "<FromUserName><![CDATA[%s]]></FromUserName>" +
30                         "<CreateTime>%s</CreateTime>" +
31                         "<MsgType><![CDATA[video]]></MsgType>" +
32                         "<Video>" +
33                         "   <MediaId><![CDATA[%s]]></MediaId>" +
34                         "   <Title><![CDATA[%s]]></Title>" +
35                         "   <Description><![CDATA[%s]]></Description>" +
36                         "</Video>" +
37                         "</xml>",
38                 fromUserName, toUserName, getMessageCreateTime(), video.mediaId, video.title, video.description);
39     }

  2.2.5、回复语音消息

  写一个构建语音消息的方法,代码如下:

 1  /**
 2      * 构造语音消息
 3      * @param map 封装了解析结果的Map
 4      * @param mediaId 通过素材管理接口上传多媒体文件得到的id
 5      * @return 语音消息XML字符串
 6      */
 7     private static String buildVoiceMessage(Map<String, String> map, String mediaId) {
 8         //发送方帐号
 9         String fromUserName = map.get("FromUserName");
10         // 开发者微信号
11         String toUserName = map.get("ToUserName");
12         /**
13          * 语音消息XML数据格式
14          *<xml>
15          <ToUserName><![CDATA[toUser]]></ToUserName>
16          <FromUserName><![CDATA[fromUser]]></FromUserName>
17          <CreateTime>12345678</CreateTime>
18          <MsgType><![CDATA[voice]]></MsgType>
19          <Voice>
20          <MediaId><![CDATA[media_id]]></MediaId>
21          </Voice>
22          </xml>
23          */
24         return String.format(
25                 "<xml>" +
26                         "<ToUserName><![CDATA[%s]]></ToUserName>" +
27                         "<FromUserName><![CDATA[%s]]></FromUserName>" +
28                         "<CreateTime>%s</CreateTime>" +
29                         "<MsgType><![CDATA[voice]]></MsgType>" +
30                         "<Voice>" +
31                         "   <MediaId><![CDATA[%s]]></MediaId>" +
32                         "</Voice>" +
33                         "</xml>",
34                 fromUserName, toUserName, getMessageCreateTime(), mediaId);
35     }

  2.2.6、回复图文消息

  写一个构建图文消息的方法,代码如下:

 1   /**
 2      * 构造图文消息
 3      * @param map 封装了解析结果的Map
 4      * @return 图文消息XML字符串
 5      */
 6     private static String buildNewsMessage(Map<String, String> map) {
 7         String fromUserName = map.get("FromUserName");
 8         // 开发者微信号
 9         String toUserName = map.get("ToUserName");
10         NewsItem item = new NewsItem();
11         item.Title = "微信开发学习总结(一)——微信开发环境搭建";
12         item.Description = "工欲善其事,必先利其器。要做微信公众号开发,那么要先准备好两样必不可少的东西:\n" +
13                 "\n" +
14                 "  1、要有一个用来测试的公众号。\n" +
15                 "\n" +
16                 "  2、用来调式代码的开发环境";
17         item.PicUrl = "http://images2015.cnblogs.com/blog/289233/201601/289233-20160121164317343-2145023644.png";
18         item.Url = "http://www.cnblogs.com/xdp-gacl/p/5149171.html";
19         String itemContent1 = buildSingleItem(item);
20 
21         NewsItem item2 = new NewsItem();
22         item2.Title = "微信开发学习总结(二)——微信开发入门";
23         item2.Description = "微信服务器就相当于一个转发服务器,终端(手机、Pad等)发起请求至微信服务器,微信服务器然后将请求转发给我们的应用服务器。应用服务器处理完毕后,将响应数据回发给微信服务器,微信服务器再将具体响应信息回复到微信App终端。";
24         item2.PicUrl = "";
25         item2.Url = "http://www.cnblogs.com/xdp-gacl/p/5151857.html";
26         String itemContent2 = buildSingleItem(item2);
27 
28 
29         String content = String.format("<xml>\n" +
30                 "<ToUserName><![CDATA[%s]]></ToUserName>\n" +
31                 "<FromUserName><![CDATA[%s]]></FromUserName>\n" +
32                 "<CreateTime>%s</CreateTime>\n" +
33                 "<MsgType><![CDATA[news]]></MsgType>\n" +
34                 "<ArticleCount>%s</ArticleCount>\n" +
35                 "<Articles>\n" + "%s" +
36                 "</Articles>\n" +
37                 "</xml> ", fromUserName, toUserName, getMessageCreateTime(), 2, itemContent1 + itemContent2);
38         return content;
39 
40     }
41 
42     /**
43      * 生成图文消息的一条记录
44      *
45      * @param item
46      * @return
47      */
48     private static String buildSingleItem(NewsItem item) {
49         String itemContent = String.format("<item>\n" +
50                 "<Title><![CDATA[%s]]></Title> \n" +
51                 "<Description><![CDATA[%s]]></Description>\n" +
52                 "<PicUrl><![CDATA[%s]]></PicUrl>\n" +
53                 "<Url><![CDATA[%s]]></Url>\n" +
54                 "</item>", item.Title, item.Description, item.PicUrl, item.Url);
55         return itemContent;
56     }

  根据上述提到的消息回复业务场景,我们可以写一个handleTextMessage方法来作为构造各种回复消息的处理入口,代码如下:

 1  /**
 2      * 接收到文本消息后处理
 3      * @param map 封装了解析结果的Map
 4      * @return
 5      */
 6     private static String handleTextMessage(Map<String, String> map) {
 7         //响应消息
 8         String responseMessage;
 9         // 消息内容
10         String content = map.get("Content");
11         switch (content) {
12             case "文本":
13                 String msgText = "孤傲苍狼又要开始写博客总结了,欢迎朋友们访问我在博客园上面写的博客\n" +
14                         "<a href=\"http://www.cnblogs.com/xdp-gacl\">孤傲苍狼的博客</a>";
15                 responseMessage = buildTextMessage(map, msgText);
16                 break;
17             case "图片":
18                 //通过素材管理接口上传图片时得到的media_id
19                 String imgMediaId = "dSQCiEHYB-pgi7ib5KpeoFlqpg09J31H28rex6xKgwWrln3HY0BTsoxnRV-xC_SQ";
20                 responseMessage = buildImageMessage(map, imgMediaId);
21                 break;
22             case "语音":
23                 //通过素材管理接口上传语音文件时得到的media_id
24                 String voiceMediaId = "h3ul0TnwaRPut6Tl1Xlf0kk_9aUqtQvfM5Oq21unoWqJrwks505pkMGMbHnCHBBZ";
25                 responseMessage = buildVoiceMessage(map,voiceMediaId);
26                 break;
27             case "图文":
28                 responseMessage = buildNewsMessage(map);
29                 break;
30             case "音乐":
31                 Music music = new Music();
32                 music.title = "赵丽颖、许志安 - 乱世俱灭";
33                 music.description = "电视剧《蜀山战纪》插曲";
34                 music.musicUrl = "http://gacl.ngrok.natapp.cn/music/music.mp3";
35                 music.hqMusicUrl = "http://gacl.ngrok.natapp.cn/music/music.mp3";
36                 responseMessage = buildMusicMessage(map, music);
37                 break;
38             case "视频":
39                 Video video = new Video();
40                 video.mediaId = "GqmIGpLu41rtwaY7WCVtJAL3ZbslzKiuLEXfWIKYDnHXGObH1CBH71xtgrGwyCa3";
41                 video.title = "小苹果";
42                 video.description = "小苹果搞笑视频";
43                 responseMessage = buildVideoMessage(map, video);
44                 break;
45             default:
46                 responseMessage = buildWelcomeTextMessage(map);
47                 break;
48 
49         }
50         //返回响应消息
51         return responseMessage;
52     }

  到此,回复想消息的相关处理代码就编写完成了,将项目部署到Tomcat服务器进行测试,记得使用Ngrok将内网的服务器映射到外网,否则无法使用微信测试,如下:

  

  关于Ngrok的使用方式之前的《微信开发学习总结(一)——微信开发环境搭建》博客中已经介绍过了,这里就不再重复讲解了,没了解过Ngrok的朋友可以去看看《微信开发学习总结(一)——微信开发环境搭建》这篇博客.

  使用微信进行消息回复测试,测试效果如下: