微信公众号开发教程(二)消息接收与响应处理

作者:陈惠,叩丁狼教育高级讲师。原创文章,转载请注明出处。

上一篇文章我们已经实现了URL接入,接下来就可以利用微信官方的开发文档来实现不同的案例。(注意:若还没有进行接入操作,请参考开发教程(一))

实现思路

接下来我们先打开微信的开发文档,选择“消息管理”模块中的”接收普通消息“

文档.png

文档中已经告诉我们,当普通微信用户向公众账号发送消息时,微信服务器会把该消息封装成XML数据包通过POST的方式发送到开发者填写的URL上。我们设置的URL仅仅只有一个,上篇文章中是用来做接入验证的,当时是微信服务器发送GET请求过来,而现在是用来做消息处理的,此时微信服务器发送的是POST请求,因此想要区分开来应该做什么事情,只需要根据请求方式来判断即可。

因此,我们需要再创建一个handleMessage方法来做消息处理。

方法签名.png

观察图中的两个方法,其实就是请求路径相同,但请求方式不同,一个是GET方式一个是POST方式。

参数理解

现在我们再来看下开发文档,当用户发送普通消息到公众号,微信服务器发送的XML数据中会包含下面的参数。
普通消息*.png

如果是图片消息会包含下面的参数

图片消息.png

实际上,用户可发送的类型还有很多,比如语音,视频,地理位置等等。
我们对比一下不同类型的xml数据包中的参数,ToUserName,FromUserName,CreateTime,MsgType,MsgId这五个是公共的,所有类型都会带上这些参数。

接下来,我们需要来了解这5个参数的具体意义。

ToUserName:文档上描述的是开发者微信号,实际上,直接把它当做你的公众号的微信号即可,表示的是发到那个公众号的意思。
FromUserName:与ToUserName相反,这是代表是由哪个用户发过来的,同一个用户发多条信息过来,FromUserName都是不变的。但这并不是用户的微信号,而是一个OpenID。
那什么是OpenID呢:当用户和公众号发生了交互,微信服务器会为每个用户针对每个公众号产生一个OpenID(也就是指该OpenID是利用两个因素:用户和公众号来产生的,也就意味着如果该用户跟另外一个公众号交互,产生的OpenID也是不同的,这样安全性会比较高),如果一个公司有多个公众号,并且需要在多公众号、移动应用之间做用户共通,则需要使用UnionID,前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID,可以在用户管理-获取用户基本信息(UnionID机制)文档了解详情。
CreateTime:消息创建时间,这个没什么好说的了。
MsgType:用户发送的消息的类型,如text代表文本消息,image代表图片消息等。
MsgId:用户发送的每个消息都有自己的id,可以用于消息排重,比如微信服务器把xml消息包发送到URL了,但是五秒内微信服务器没有收到我们的响应,则会重新发起请求,总共重试三次。如果不做消息排重,那么用户可能就收到多条相同的响应消息了。

接下来,我们可以创建一个封装消息的实体类,把所有可接收到的参数都放进入,其他类型的暂时不演示,所以只在最后加入了文本和图片的参数。

@Setter
@Getter
public class InMsgEntity {
    // 开发者微信号
    protected String FromUserName;
    // 发送方帐号(一个OpenID)
    protected String ToUserName;
    // 消息创建时间
    protected Long CreateTime;
    /**
     * 消息类型
     * text 文本消息
     * image 图片消息
     * voice 语音消息
     * video 视频消息
     * music 音乐消息
     */
    protected String MsgType;
    // 消息id
    protected Long MsgId;
    // 文本内容
    private String Content;
    // 图片链接(由系统生成)
    private String PicUrl;
    // 图片消息媒体id,可以调用多媒体文件下载接口拉取数据
    private String MediaId;
}

这时候大家可能会有个疑问,为什么字段名称都是大写开头呢?
因为微信服务器传过来的xml数据包中的xml元素都是大写开头的,如下所示:

<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>

因为xml解析是大小写敏感的,所以为了方便封装,我直接把字段名设置为大写开头。
当然,如果还是想要小写开头的字段,也是可以的,我们待会再说处理方式。

接收消息

实体已经建好之后,我们就可以开始接收微信传过来的xml数据了。
第1步:在handleMessage方法的形参上添加InMsgEntity类型的参数,并且贴上@RequestBody注解,如下代码所示:

    /**
     * 微信消息处理
     */
    @RequestMapping(value = "/weChat", method = RequestMethod.POST)
    @ResponseBody
    public Object handleMessage(@RequestBody InMsgEntity msg) {
        return null;
    }

@RequestBody 该注解用于读取request请求的body部分数据,根据Content-Type来判断把数据当做什么类型来解析,然后把相应的数据绑定到参数上。

第2步:需要配合JAXB的注解来解析xml。
在 InMsgEntity 上添加以下两个注解:

@XmlRootElement(name="xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class InMsgEntity {
     ......
}

@XmlRootElement是一个类级别注解,主要属性为name,意为指定根节点的名字。
往上面看前面举了个微信传过来的xml数据的例子里,里面的根节点就是”xml”,所以这里就直接设置name=”xml”

@XmlAccessorType用于定义这个类中的何种类型需要映射到XML中
XmlAccessType.PROPERTY:代表映射这个类中的属性(get/set方法)到XML
XmlAccessType.FIELD:代表映射这个类中的所有字段到XML(我选用的,现在的字段名刚好是大写开头了)

另外,刚才说到如果字段名是小写,怎么解决封装问题?
在每个字段或属性上添加@XmlElement注解来指定名称映射
如:

@XmlElement(name="FromUserName")
protected String fromUserName;

JAXB还有非常多的注解和类型,这里只介绍我所用到的,如需了解其他请自行百度。

现在我们可以扫描自己的公众号二维码来测试发送消息后台服务器是否能接收到。

通过debug可知,微信传过来的xml消息包已经成功转换为我们的java对象了。
图片.png

响应消息

现在我们可以先来尝试回复一条相同的内容给用户。

打开微信开发文档,选择”被动回复消息”。

发送被动消息其实不是一种接口,而是对微信服务器发过来消息的一次回复。

图片.png

我们可以看到文档里面接收的普通文本回复的格式和接收的格式基本是一样的,但是图片消息或其他消息的还是有些区别。

<xml>
<ToUserName>< ![CDATA[toUser] ]></ToUserName>
<FromUserName>< ![CDATA[fromUser] ]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType>< ![CDATA[image] ]></MsgType>
<Image>
    <MediaId>< ![CDATA[media_id] ]></MediaId>
</Image>
</xml>

如上例子,比之前多了Image的元素,所以我们需要再创建一个类来封装响应的xml消息。
这里我是把所有类型的属性统一放到OutMsgEntity类中,大家也可以抽取一个父类,不同的消息创建不同的子类也可以。

@XmlRootElement(name="xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class OutMsgEntity {
    // 发送方的账号
    protected String FromUserName;
    // 接收方的账号(OpenID)
    protected String ToUserName;
    // 消息创建时间
    protected Long CreateTime;
    /**
     * 消息类型
     * text 文本消息
     * image 图片消息
     * voice 语音消息
     * video 视频消息
     * music 音乐消息
     * news 图文消息
     */
    protected String MsgType;
    // 图片消息媒体id,可以调用多媒体文件下载接口拉取数据
    @XmlElementWrapper(name="Image")
    private String[] MediaId ;
    // 文本内容
    private String Content;
}

@XmlElementWrapper注解可以在原xml结点上再包装一层xml,但仅允许出现在数组或集合属性上。

实际上,我们现在的需求比较简单,用户给我们发什么,我们就回复什么,只需要把接收到 InMsgEntity 的内容设置到 OutMsgEntity 上,并且把ToUserName与FormUserName的值设置为相反即可。

代码如下:

 /**
     * 微信消息处理
     */
    @RequestMapping(value = "/weChat", method = RequestMethod.POST)
    @ResponseBody
    public Object handleMessage(@RequestBody InMsgEntity msg) {
        //创建消息响应对象
        OutMsgEntity out = new OutMsgEntity();
        //把原来的发送方设置为接收方
        out.setToUserName(msg.getFromUserName());
        //把原来的接收方设置为发送方
        out.setFromUserName(msg.getToUserName());
        //获取接收的消息类型
        String msgType = msg.getMsgType();
        //设置消息的响应类型
        out.setMsgType(msgType);
        //设置消息创建时间
        out.setCreateTime(new Date().getTime());
        //根据类型设置不同的消息数据
        if("text".equals(msgType)){
            out.setContent(msg.getContent());
        }else if("image".equals(msgType)){
            out.setMediaId(new String[]{msg.getMediaId()});
        }
        return out;
    }

测试效果:

图片.png

到此,我们已经实现了对消息的接收和响应简单操作。