Node微信公众号开发 自定义回复

3,318 阅读10分钟

一直想在日志中多穿插些旅行日志或者日常杂记,但是一来冬天实在没有什么可供娱乐的选项,二来今日随着新型冠状病毒的爆发,娱乐业基本上都进入停滞状态,索性还是老老实实跟家敲代码吧……其实也是所谓的敲代码,佩服自己的自律性,总不自觉地点开steam……

这几天一直在对自己的公众号进行完善、优化,现在可以继续更新一篇关于自定义被动回复的开发记录。

官方文档截取

被动回复用户消息分为以下6种回复类型,我们需要将所需的回复类型按照官方API拼成固定字符串返回给服务器,以下是不同类型及其对应的字符串:

  • 回复文本消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[你好]]></Content>
  </xml>

参数
是否必须 描述
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间 (整型)
MsgType

消息类型,文本为text
Content

回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)

  • 回复图片消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[image]]></MsgType>
    <Image>
      <MediaId><![CDATA[media_id]]></MediaId>
    </Image>
  </xml>

参数
是否必须 说明
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间 (整型)
MsgType

消息类型,图片为image
MediaId

通过素材管理中的接口上传多媒体文件,得到的id

  • 回复语音消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[voice]]></MsgType>
    <Voice>
      <MediaId><![CDATA[media_id]]></MediaId>
    </Voice>
  </xml>

参数
是否必须 说明
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间戳 (整型)
MsgType

消息类型,语音为voice
MediaId

通过素材管理中的接口上传多媒体文件,得到的id

  • 回复视频消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[video]]></MsgType>
    <Video>
      <MediaId><![CDATA[media_id]]></MediaId>
      <Title><![CDATA[title]]></Title>
      <Description><![CDATA[description]]></Description>
    </Video>
  </xml>

参数
是否必须 说明
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间 (整型)
MsgType

消息类型,视频为video
MediaId

通过素材管理中的接口上传多媒体文件,得到的id
Title

视频消息的标题
Description

视频消息的描述

  • 回复音乐消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[music]]></MsgType>
    <Music>
      <Title><![CDATA[TITLE]]></Title>
      <Description><![CDATA[DESCRIPTION]]></Description>
      <MusicUrl><![CDATA[MUSIC_Url]]></MusicUrl>
      <HQMusicUrl><![CDATA[HQ_MUSIC_Url]]></HQMusicUrl>
      <ThumbMediaId><![CDATA[media_id]]></ThumbMediaId>
    </Music>
  </xml>

参数
是否必须 说明
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间 (整型)
MsgType

消息类型,音乐为music
Title

音乐标题
Description

音乐描述
MusicURL

音乐链接
HQMusicUrl

高质量音乐链接,WIFI环境优先使用该链接播放音乐
ThumbMediaId
缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id

  • 回复图文消息
  <xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>12345678</CreateTime>
    <MsgType><![CDATA[news]]></MsgType>
    <ArticleCount>1</ArticleCount>
    <Articles>
      <item>
        <Title><![CDATA[title1]]></Title>
        <Description><![CDATA[description1]]></Description>
        <PicUrl><![CDATA[picurl]]></PicUrl>
        <Url><![CDATA[url]]></Url>
      </item>
    </Articles>
  </xml>

参数
是否必须 说明
ToUserName

接收方帐号(收到的OpenID)
FromUserName
开发者微信号
CreateTime

消息创建时间 (整型)
MsgType

消息类型,图文为news
ArticleCount
图文消息个数;当用户发送文本、图片、视频、图文、地理位置这五种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
Articles

图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
Title

图文消息标题
Description

图文消息描述
PicUrl

图片链接,支持JPG、PNG格式,较好的效果为大图360200,小图200200
Url

点击图文消息跳转链接

xml2js

xml2js 是一个简单的 XML 到 JavaScript 对象转换器,官方使用方法如下:

var parseString = require('xml2js').parseString;
var xml = "<root>Hello xml2js!</root>"
parseString(xml, function (err, result) {
    console.dir(result);
});

更多的使用方法,还请另行移步只 www.npmjs.com/package/xml…

Node.js 自定义被动回复开发

首先,我们需要先配置路由,来接收服务器发送的请求和数据;

app.post("/", (req,res, next)=> { …… })

需要使用事件监听来监视和控制数据;

app.post("/", (req,res, next)=> {
  // 获取到微信返回的二进制数据
  var buffer = []
  req.on('data', (data) => {
    buffer.push(data)
  })
  req.on('end', () => {
    // 将数据转化成 utf-8 格式
    var msgXml = Buffer.concat(buffer).toString('utf-8')
  })
})

我们先将微信返回来的二进制数据存放到 buffer 数组中,并在请求结束时将其处理为普通 XML 数据;

这里存在一个问题,Node.js 无法直接将 XML 转换成 Javascript 对象 ,这时就需要前文介绍的组件 xml2js 来帮我们实现这一步操作;

var reply = require('./reply') // 自定义回复
……
  req.on('end', () => {
    // 将数据转化成 utf-8 格式
    var msgXml = Buffer.concat(buffer).toString('utf-8')

    // 调用 xml2js 模块的 parseString 方法
    parseString(msgXml, { explicitArray: false }, (error, result) => {
      // 如果有错误直接抛出
      if (error) {
        console.log(error)
        return
      }
      result = result.xml // result.xml 是获取到的真实数据
      
      reply(result, (resultXml) => { // 调用回复功能
        res.send(resultXml)
      })
    })
  })
……

通过组件我们就可以获得到真正能够使用的数据了 result.xml ,这里面包含了提交回复时所用到的 ToUserNameFromUserName 等……

这里有一个坑要特别注意!!!注意!!注意!重要的事说三遍:这里获得的 ToUserNameFromUserName 与提交模板中的 ToUserNameFromUserName 对应关系正好相反,也就是说 result.xml.ToUserName 对应的模板数据应该是 FromUserName ,而 result.xml.FromUserName 则对应模板数据 ToUserName

回复功能除了以上需要加入 app.js 中的部分,我另外做了两个脚本用于单独对回复功能进行处理,分别为 reply.jsreplyType.jsreplyType.js 存放了所有涉及的模板,辅助 reply.js 完成自定义回复的工作;

以下是 replyType.js 中的代码:

// 核心模块
var fs = require('fs')
// 引入开发模块
var request = require('./request')

// 回复文本消息
exports.textMsg = (result, content) => {
  console.log('reply type text !')
  var xmlContent = '<xml><ToUserName><![CDATA[' + result.FromUserName + ']]></ToUserName>'
  xmlContent += '<FromUserName><![CDATA[' + result.ToUserName + ']]></FromUserName>'
  xmlContent += '<CreateTime>' + new Date().getTime() + '</CreateTime>'
  xmlContent += '<MsgType><![CDATA[text]]></MsgType>'
  xmlContent += '<Content><![CDATA[' + content + ']]></Content></xml>'
  return xmlContent
}

// 回复图片
exports.imgMsg = function (result, urlPath, callback) {
  uploadFile(urlPath, 'image').then((media_id) => {
    console.log('reply type image !', media_id)
    var xmlContent = '<xml><ToUserName><![CDATA[' + result.FromUserName + ']]></ToUserName>'
    xmlContent += '<FromUserName><![CDATA[' + result.ToUserName + ']]></FromUserName>'
    xmlContent += '<CreateTime>' + new Date().getTime() + '</CreateTime>'
    xmlContent += '<MsgType><![CDATA[image]]></MsgType>'
    xmlContent += '<Image><MediaId><![CDATA[' + media_id + ']]></MediaId></Image></xml>'
    callback(xmlContent)
  })
}

// 回复图文消息
exports.graphicMsg = (result, contentArr) => {
  console.log('reply type image and text !')
  var xmlContent = '<xml><ToUserName><![CDATA[' + result.FromUserName + ']]></ToUserName>'
  xmlContent += '<FromUserName><![CDATA[' + result.ToUserName + ']]></FromUserName>'
  xmlContent += '<CreateTime>' + new Date().getTime() + '</CreateTime>'
  xmlContent += '<MsgType><![CDATA[news]]></MsgType>'
  xmlContent += '<ArticleCount>' + contentArr.length + '</ArticleCount>'
  xmlContent += '<Articles>'
  contentArr.map((item, index) => {
    xmlContent += '<item>'
    xmlContent += '<Title><![CDATA[' + item.Title + ']]></Title>'
    xmlContent += '<Description><![CDATA[' + item.Description + ']]></Description>'
    xmlContent += '<PicUrl><![CDATA[' + item.PicUrl + ']]></PicUrl>'
    xmlContent += '<Url><![CDATA[' + item.Url + ']]></Url>'
    xmlContent += '</item>'
  })
  xmlContent += '</Articles></xml>'
  return xmlContent
}

上述代码中图片回复模板由于需要获得 media_id ,所以在开发过程中无法单独使用,需要取得公众号素材使用的权限,也就是上述代码中的 uploadFile 方法,稍后我将进行说明

上述代码中,除了单纯的文本回复,其他都用到了异步函数,所以此处使用回调函数的方法将最终的模板抛出给 reply.js 使用

这里存在着另一个坑,官方由于某些原因将图文回复做了限制:

  • 被动回复只能回复一条图文消息
  • 对于开发的公众号(不是操作公众号后台运维的),被动回复无法使用大图方式,大小图对比如图

replyType.js 引入 reply.js 页面,使用方法如下:

  • 图片回复
  var urlPath = path.join(__dirname, '自定义图片.jpg')
  resultXml = replyType.imgMsg(result, urlPath, (resultXml) => {
      callback(resultXml)
  })

  • 文本回复
  resultXml = replyType.textMsg(result, '回复内容')
  callback(resultXml)

  • 图文回复
  resultXml = replyType.graphicMsg(result, contentArr) // contentArr 为图文数组对象

使用自定义回复

前文提到的 result.xml 中不仅包含了 ToUserNameFromUserName 还包含了用户的操作信息:

  • result.xml.MsgType === 'event' 说明用户事件触发的,比如
    • result.xml.Event === 'subscribe' 代表用户关注了
    • result.xml.Event === 'CLICK' 代表用户是通过自定义菜单项操作
      • result.xml.EventKey 可以获取到自定义菜单的 key 属性
  • result.xml.MsgType === 'text' 说明用户是通过输入关键词触发的
    • result.xml.Content 获得用户所输入的具体内容

---

上传素材,获取 media_id

这里的素材可以是图片、语音等等,官方说明如下:

  • 请求方式:POST
  • 请求地址:https://api.weixin.qq.com/cgi-bin/media/upload?accesstoken=ACCESSTOKEN&type=TYPE
// 素材上传获取 media_id
function uploadFile (urlPath, type) {
  return new Promise((resolve, reject) => {
    fs.readFile('./config.json', 'utf-8', (error, data) => {
      if (error) {
        console.log('uploadFile read accessToken fail', error)
        return
      }
      var form = { //构造表单
        media: fs.createReadStream(urlPath)
      }
      var url = 'https://api.weixin.qq.com/cgi-bin/media/upload?access_token=' + JSON.parse(data).setAccessToken.accessToken + '&type=' + type
      request.post(url, form).then((result) => {
        resolve(JSON.parse(result).media_id)
      })
    })
  })
}

我的 access_token 保存在了 config.json 中,所以先通过 fs.readFile 获取到,之后拼接处接口,接口中的 type 为需要创建的临时媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)

fs.createReadStream 用于获得这个媒体文件的文件流,将这个文件流已请求参数形式发送个公众号,此时服务器将会返回一串字符串,该字符串就是媒体文件的对应 media_id

关于自定义回复的笔记就到这里。

我的数据源于我的博客站点,起初使用的是 WordPress 提供的 wp-json 接口文件,但后来由于安全性的考虑对接口获取方式进行了修改,目前我的公众号数据源于爬取站点获得,之后我将会分享关于 Node 爬取站点的学习记录。

还望各位客官能够喜欢!

文章已同步我的个人博客:《Node微信公众号开发 自定义菜单

相关文章:

---

资料参考:

本文由博客一文多发平台 OpenWrite 发布!