接收与回复消息:文本、图片、语音、视频、地理位置

好,咱们接着往下聊。上一章我们把服务器验证和基础消息收发搞定了。这一章,我带你看看公众号里最常见的五种消息类型:文本、图片、语音、视频、地理位置。

说实话,这五种类型覆盖了90%以上的用户交互场景。我自己做过的几十个公众号项目里,基本就靠它们撑场面。你想想看,用户发个图片、说句话、分享个位置,你都能自动回复——这不就是智能客服的雏形吗?

消息的通用结构

在动手写代码之前,咱们先搞清楚一件事:所有消息都长一个样。嗯,我是说结构上。

每条用户发来的消息,XML里都包含这些公共字段:

字段名 说明
ToUserName 开发者微信号(也就是你的公众号)
FromUserName 发送方账号(用户的OpenID)
CreateTime 消息创建时间(整型时间戳)
MsgType 消息类型,比如 text、image、voice 等
MsgId 消息ID,64位整型,用于去重

我刚开始做的时候,没注意MsgId的去重作用。结果有一次用户连续发了两次同样的消息,我回复了两遍,用户直接投诉了。后来我才知道,MsgId是全局唯一的,可以用来判断是不是重复消息。

文本消息

文本消息是最简单的,也是我们最先要处理的。

用户发来的文本消息XML长这样:

<xml>
  <ToUserName><![CDATA[gh_xxx]]></ToUserName>
  <FromUserName><![CDATA[o_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>

注意看,Content字段就是用户输入的文字。我们回复的时候,也是返回一个类似的XML,只不过ToUserNameFromUserName要互换。

回复文本消息的XML格式:

<xml>
  <ToUserName><![CDATA[o_xxx]]></ToUserName>
  <FromUserName><![CDATA[gh_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[收到,你好呀!]]></Content>
</xml>
小技巧:Content里的文字可以带表情符号,比如😊。微信支持Unicode表情,用户看到会觉得很亲切。

图片消息

图片消息稍微复杂一点。用户发来图片时,XML里会带一个PicUrl字段,这是图片的临时链接。注意,这个链接有效期只有3天。

用户发来的图片消息:

<xml>
  <ToUserName><![CDATA[gh_xxx]]></ToUserName>
  <FromUserName><![CDATA[o_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[image]]></MsgType>
  <PicUrl><![CDATA[http://mmbiz.qpic.cn/xxx]]></PicUrl>
  <MediaId><![CDATA[media_id_xxx]]></MediaId>
  <MsgId>1234567890123456</MsgId>
</xml>

回复图片消息时,你不能直接发图片链接。微信要求你通过MediaId来回复。这个MediaId怎么来的?你得先通过素材管理接口上传图片,拿到MediaId,然后才能用。

回复图片消息的XML:

<xml>
  <ToUserName><![CDATA[o_xxx]]></ToUserName>
  <FromUserName><![CDATA[gh_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[image]]></MsgType>
  <Image>
    <MediaId><![CDATA[media_id_xxx]]></MediaId>
  </Image>
</xml>
注意:MediaId是有有效期的,永久素材的MediaId不会过期,临时素材只有3天。我建议你回复图片时,尽量用永久素材的MediaId,省得三天两头重新上传。

语音消息

语音消息和图片类似,也是通过MediaId来传递。不过语音多了一个Format字段,告诉你语音的格式,通常是amr或speex。

用户发来的语音消息:

<xml>
  <ToUserName><![CDATA[gh_xxx]]></ToUserName>
  <FromUserName><![CDATA[o_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[voice]]></MsgType>
  <MediaId><![CDATA[media_id_xxx]]></MediaId>
  <Format><![CDATA[amr]]></Format>
  <MsgId>1234567890123456</MsgId>
</xml>

回复语音消息时,同样需要MediaId。格式和图片类似,只是把Image标签换成Voice

<xml>
  <ToUserName><![CDATA[o_xxx]]></ToUserName>
  <FromUserName><![CDATA[gh_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[voice]]></MsgType>
  <Voice>
    <MediaId><![CDATA[media_id_xxx]]></MediaId>
  </Voice>
</xml>

个人经验:我曾经做过一个语音点歌的公众号。用户发语音说歌名,我通过语音识别接口转成文字,然后匹配歌曲。这里有个坑——语音识别接口返回的文字可能不准,尤其是方言。所以后来我加了一个确认环节,让用户说「是」或「不是」来确认。

视频消息

视频消息和图片、语音类似,但多了一个ThumbMediaId字段,这是视频封面的MediaId。

用户发来的视频消息:

<xml>
  <ToUserName><![CDATA[gh_xxx]]></ToUserName>
  <FromUserName><![CDATA[o_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[video]]></MsgType>
  <MediaId><![CDATA[media_id_xxx]]></MediaId>
  <ThumbMediaId><![CDATA[thumb_media_id_xxx]]></ThumbMediaId>
  <MsgId>1234567890123456</MsgId>
</xml>

回复视频消息时,你需要提供MediaIdTitleDescription

<xml>
  <ToUserName><![CDATA[o_xxx]]></ToUserName>
  <FromUserName><![CDATA[gh_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[video]]></MsgType>
  <Video>
    <MediaId><![CDATA[media_id_xxx]]></MediaId>
    <Title><![CDATA[视频标题]]></Title>
    <Description><![CDATA[视频描述]]></Description>
  </Video>
</xml>
提示:Title和Description是可选的,但建议填上。用户看到标题和描述,会更愿意点开看。我一般会把视频的核心内容浓缩成一句话放在Description里。

地理位置消息

地理位置消息比较特殊。用户发来位置时,XML里会带经纬度、缩放比例和具体地址。

用户发来的地理位置消息:

<xml>
  <ToUserName><![CDATA[gh_xxx]]></ToUserName>
  <FromUserName><![CDATA[o_xxx]]></FromUserName>
  <CreateTime>123456789</CreateTime>
  <MsgType><![CDATA[location]]></MsgType>
  <Location_X>23.137466</Location_X>
  <Location_Y>113.352425</Location_Y>
  <Scale>20</Scale>
  <Label><![CDATA[广州市天河区xxx路]]></Label>
  <MsgId>1234567890123456</MsgId>
</xml>

回复地理位置消息时,你可以回复文本,也可以回复图文。我个人习惯回复文本,告诉用户「已收到您的位置,我们正在为您查找附近的服务」。然后后台根据经纬度去查数据库,再通过客服消息接口推送给用户。

避坑指南:我曾经做过一个附近门店查询的功能。用户发位置,我回复附近的门店列表。一开始我直接用Location_XLocation_Y去查数据库,结果发现经纬度是反的!微信的Location_X是纬度,Location_Y是经度。很多新手都会搞混,你一定要注意。

实战:统一消息处理入口

好了,五种消息类型都讲完了。在实际项目中,你不会为每种消息单独写一个接口。而是写一个统一的消息处理入口,根据MsgType分发到不同的处理函数。

我一般这样写:

// 伪代码示例
function handleMessage($xmlData) {
    $msgType = $xmlData->MsgType;
    
    switch ($msgType) {
        case 'text':
            return handleText($xmlData);
        case 'image':
            return handleImage($xmlData);
        case 'voice':
            return handleVoice($xmlData);
        case 'video':
            return handleVideo($xmlData);
        case 'location':
            return handleLocation($xmlData);
        default:
            return handleDefault($xmlData);
    }
}

这样写的好处是,以后新增消息类型,只需要加一个case分支,不会影响已有的逻辑。我在好几个项目里都用这个模式,维护起来特别省心。

嗯,这一章的内容就到这。五种消息类型,说白了就是XML格式不同,回复方式不同。你只要把每种类型的XML结构记清楚,写代码就是体力活了。下一章,咱们聊聊图文消息和客服消息,那才是真正展示技术的地方。