首页 > 技术文章 > 微信公众号接入第三方服务器,设置自动回复、关键回复、自定义菜单,配置及开发流程

kakayang 2018-09-30 14:46 原文

首先需要确认一点,一旦接入第三方服务器,微信就认为你已经具备了开发能力,像自动回复、关键词回复、自定义菜单这些功能,微信公众平台就不再提供了(需要开发者调用相关接口),停用服务器之后,这些功能也就恢复了,二者是互斥的。

说明一下,本文的例子是node+express搭建服务,消息加解密方式为明文模式,请酌情参考。

一、搭建本地调试环境,需要将本地服务穿透出去,便于外网访问,可以用花生壳或者ngrok等,能穿透内网就可以,这里就不多说了。

二、服务器配置及校验

  现在我们已经有了一个可供外网访问的本地服务,接下来说一下微信公众平台的相关配置:

  登录微信公众平台,找到基本配置

  

  可以看到服务器配置一项(我这里是已经启用过的),点击右侧的修改配置

  

  先说一下大致流程,信息编辑完毕之后点击提交,微信服务器会向你所填写的URL发送一条get请求,你的服务器必须要能接到这条请求,然后拿微信服务器带来的参数进行验证,验证完毕之后,返回验证结果给微信,微信拿到想要的结果之后(至于具体返回什么,后面会说),你的服务器就算是在微信服务器“备案”成功了,接下来点击启用就可以了,启用之后,微信服务器一旦收到消息,就会向你所填写的URL发送一条post请求(确保你的服务器在5秒内做出响应,不然会发生一些错误,具体错误可查看微信文档),请求携带的参数是xml格式的,注意配置一下,不要以json的形式去接收,解析xml,能拿到信息发送者、接收者、信息内容、事件类型等数据,然后就可以根据事件类型、信息等做出相应的处理。

  填写URL:支持https和http,格式为http://xxxxxx.com+接口路径,例如https://www.baidu.com/authorize,https://www.baidu.com是你的服务地址,authotize是你的接口路径。

  填写Token:这儿的token是验证服务器的令牌,是你自己定义的,符合格式要求就行,后面会用到(注意区别access_token,两者不是一回事)。

  填写EncodingAESKey:可以随机生成,也可以自己定义,符合格式要求就好,当设置消息加解密方式为加密模式时会用来解密消息(本文采用明文模式)。

  选择消息加解密方式:本文选择明文模式。

  服务器验证:

  

// 服务器验证
// /authorize为接口路径,

router.get('/authorize', (req, res) => {
  //接收到微信服务器的请求后,取出参数signature,timestamp,echostr,nonce
    let signature = req.query.signature;
    let timestamp = req.query.timestamp;
    let echostr = req.query.echostr;
    let nonce = req.query.nonce;
    // 把token、timestamp、nonce进行字典排序,CONFIG.token换为你自己的token就好
    let arr = [CONFIG.token, timestamp, nonce].sort();
    // sha1加密
    let str = arr.join('');
    let hashCode = crypto.createHash('sha1');
    let result = hashCode.update(str).digest('hex');
    // 与signature对比后返回结果
    if (result === signature) {
    // 验证正确之后,把echostr原封不动返回给微信就行了
        res.send(echostr);
    } else {
     // 验证错误的话也要返回信息,告诉微信不要再尝试请求了,微信官方建议直接返回success字符串,当然返回空也是可以的
        res.send('success');
    }
});

一定要处理好服务器验证逻辑之后再点击提交按钮,否则是提交不成功的。

提交成功之后,就算是接入服务器了,但是点击启用按钮,服务器才能起作用。

接下来是消息处理逻辑:

微信服务器在接收到用户消息之后,就会向你的服务器发送请求,URL和验证服务器的URL一样,只不过请求方式为post

// 消息处理
// 用xml2js模块来处理xml
let parseString = require('xml2js').parseString;
router.post('/authorize', (req, res) => {
    try {
        let buffer = [];
        // 监听data事件,用于接收数据,用req.body是拿不到数据的
        req.on('data', (data) => {
            buffer.push(data);
        });
        // 监听end事件,用于处理接收完成的数据
        req.on('end', () => {
            parseString(Buffer.concat(buffer).toString('utf-8'), {
                explicitArray: false
            }, (err, result) => {
                // 处理错误
                if (err) {
                    console.log('解析微信服务器发来的消息出错了:');
                    console.log(err);
                    res.send('success');
                    return false;
                }

                if (!result || !result.xml) {
                    // 未接收到有效消息,告诉微信服务器不要再尝试连接
                    res.send('success');
                    return console.log('未接收到任何消息也未发生任何事件');
                }

                result = result.xml;
                // 接收方微信(注意接收方和发送方的转换)
                let toUser = result.FromUserName;
                // 发送方微信
                let fromUser = result.ToUserName;
                let userMessage = result.Content;

                console.log('-----------------------开始处理消息-----------------------');

                if (result.Event == 'subscribe') {
                    // 如果是用户关注
                    console.log('--------------------有用户关注了---------------------------');
                    handleAutoReply(res, toUser, fromUser, 'subscribe');
                } else {
                    // 其他消息
                    if (result.MsgType != 'text') {
                        res.send('success');
                        console.log('------------------不是文本类型的消息暂不处理----------------------');
                        return false;
                    }
                    
                    // 文本消息

                    // 这里可以处理一些特殊回复,比如发送编码查询等

                    // 处理关键词自动回复
                    console.log('-----------------------现在处理关键词回复------------------------');
                    handleAutoReply(res, toUser, fromUser, userMessage);
                }
            });
        });
    } catch(err) {
        console.log(err);
        res.send('success');
    }
});
/**
 * [handleAutoReply description]
 * @param  {Object} res         [response对象]
 * @param  {String} toUser      [接收方]
 * @param  {String} fromUser    [发送方]
 * @param  {String} keyword     [关键词]
 * @return {String} xmlContent  [消息模板]
 */
function handleAutoReply(res, toUser, fromUser, keyword) {
    // messageMap是含有关键词回复key-value的json,根据不同的关键词,向用户发送不同消息
    let messageMap = JSON.parse(JSON.stringify(messageJson));
    let content = messageMap[keyword];
    if (!content) {
        res.send('success');
        return false;
    }

    let xml = returnText(toUser, fromUser, content);
    res.send(xml); 
}
/**
 * [returnText description]
 * @param  {String} toUser      [接收方]
 * @param  {String} fromUser    [发送方]
 * @param  {String} content     [消息内容]
 * @return {String} xmlContent  [消息模板]
 */
function returnText(toUser, fromUser, content) {
    let xmlContent = `<xml><ToUserName><![CDATA[${toUser}]]></ToUserName>
    <FromUserName><![CDATA[${fromUser}]]></FromUserName>
    <CreateTime>${new Date().getTime()}</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[${content}]]></Content></xml>`;
    return xmlContent;
}

一定要注意错误处理,微信收不到正确响应时,会尝试重新请求,所以一旦程序发生未知错误,要及时处理,并且通知微信不要再尝试发送请求了(发送success字符串即可),否则微信会提示用户接入的服务器异常。

至此,消息回复的逻辑已经处理完了。但是接入自己的服务器之后,之前在微信公众平台设置的自定义菜单也没了,需要我们调用接口去配置;

 

配置自定义菜单:

打开微信接口调试页面:https://mp.weixin.qq.com/debug

输入你的appid和secret,由于配置自定义菜单之后,菜单就会一直存在,不需要代码去维持,所以我选择了在这儿获取access_token,当然你也可以在你的程序中去获取,然后再写个配置菜单的页面,那就更方便了。

然后选择接口类型为自定义菜单:

access_token填你刚才获取的就好,注意这个是有时效的,一般为7200秒,过期的话再重新获取就好了。

body是你配置菜单的json,简单讲一下:

 {
     "button":[
     {    
          "type":"click",
          "name":"今日歌曲",
          "key":"V1001_TODAY_MUSIC"
      },
      {
           "name":"菜单",
           "sub_button":[
           {    
               "type":"view",
               "name":"搜索",
               "url":"http://www.soso.com/"
            },
            {
                 "type":"miniprogram",
                 "name":"wxa",
                 "url":"http://mp.weixin.qq.com",
                 "appid":"wx286b93c14bbf93aa",
                 "pagepath":"pages/lunar/index"
             },
            {
               "type":"click",
               "name":"赞一下我们",
               "key":"V1001_GOOD"
            }]
       }]
 }

button是一级菜单数组,每个元素代表一个一级菜单,注意一级菜单最多三个,每个菜单最多4个字,超出显示...,每个一级菜单下的二级菜单最多5个,每个二级菜单最多7个字,超出显示...。

type是按钮类型,根据需要选择就好:

1、click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
2、view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
3、scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
4、scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
5、pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
6、pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
7、pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
8、location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
9、media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
10、view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。

 

根据需要,组织好你的json,填入body输入框就行了,点击检查问题,如果检查通过,菜单就创建成功了,检查失败的话,再具体看一下报错信息。首次设置会立即生效,修改的话需要5分钟才刷新,可以选择先取消关注公众号,然后再关注,就能立即看到效果了。

这里讲的都是通过微信接口调试页面做的,流程都是一样的,当然也可以写在你的程序里,按步骤调用相关接口就行了。

推荐阅读