东莞微信小程序制作_Koa2微信大众号开发之音讯管

阅读  ·  发布日期 2021-01-08 14:58  ·  admin
Koa2微信公众号开发之消息管理       这篇文章主要介绍了Koa2微信公众号开发之消息管理,这一节我们就来看看公众号的消息管理。并实现一个自动回复功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

上一节我们搭建好了本地调试环境并且接入了微信公众测试号。这一节我们就来看看公众号的消息管理。并实现一个自动回复功能。

Github源码:

阅读建议:

二、接收消息

当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。

2.1 接收普通消息数据格式

XML的结构基本固定,不同的消息类型略有不同。

用户发送文本消息时,微信公众账号接收到的XML数据格式如下所示:

 xml 
 ToUserName ![CDATA[toUser]] /ToUserName 
 FromUserName ![CDATA[fromUser]] /FromUserName 
 CreateTime createTime /CreateTime 
 MsgType ![CDATA[text]] /MsgType 
 Content ![CDATA[this is a test]] /Content 
 MsgId 23456 /MsgId 
 /xml 

用户发送图片消息时,微信公众账号接收到的XML数据格式如下所示:

 xml 
 ToUserName ![CDATA[toUser]] /ToUserName 
 FromUserName ![CDATA[fromUser]] /FromUserName 
 CreateTime  /CreateTime 
 MsgType ![CDATA[image]] /MsgType 
 PicUrl ![CDATA[this is a url]] /PicUrl 
 MediaId ![CDATA[media_id]] /MediaId 
 MsgId 23456 /MsgId 
 /xml 

其他消息消息类型的结构请查阅【微信公众平台开发文档】

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过自己解析上下文context中的原生node.js请求对象request。我们将用到这个模块来拿到数据。

2.2 先来优化之前的代码

这一节的代码紧接着上一届实现的代码,在上一届的基础上轻微改动了下。

'use strict'
const Koa = require('koa')
const app = new Koa()
const crypto = require('crypto')
// 将配置文件独立到config.js
const config = require('./config')
app.use(async ctx = {
 // GET 验证服务器
 if (ctx.method === 'GET') {
 const { signature, timestamp, nonce, echostr } = ctx.query
 const TOKEN = config.wechat.token
 let hash = crypto.createHash('sha1')
 const arr = [TOKEN, timestamp, nonce].sort()
 hash.update(arr.join(''))
 const shasum = hash.digest('hex')
 if (shasum === signature) {
 return ctx.body = echostr
 ctx.status = 401
 ctx.body = 'Invalid signature'
 } else if (ctx.method === 'POST') { // POST接收数据
 // TODO
app.listen(7001);

这儿我们在只在GET中验证了签名值是否合法,实际上我们在POST中也应该验证签名。

将签名验证写成一个函数

function getSignature (timestamp, nonce, token) {
 let hash = crypto.createHash('sha1')
 const arr = [token, timestamp, nonce].sort()
 hash.update(arr.join(''))
 return hash.digest('hex')

优化代码,再POST中也加入验证

app.use(async ctx = {
 const { signature, timestamp, nonce, echostr } = ctx.query
 const TOKEN = config.wechat.token
 if (ctx.method === 'GET') {
 if (signature === getSignature(timestamp, nonce, TOKEN)) {
 return ctx.body = echostr
 ctx.status = 401
 ctx.body = 'Invalid signature'
 }else if (ctx.method === 'POST') {
 if (signature !== getSignature(timestamp, nonce, TOKEN)) {
 ctx.status = 401
 return ctx.body = 'Invalid signature'
 // TODO
...

到这儿我们都没有开始实现接受XML数据包的功能,而是在修改之前的代码。这是为了演示在实际开发中的过程,写任何代码都不是一步到位的,好的代码都是改出来的。

2.3 接收公众号普通消息的XML数据包

现在开始进入本节的重点,接受XML数据包并转为JSON

$ npm install raw-body --save

console.log(xml) return ctx.body = 'success' // ess,微信服务器不会对此作任何处理

给你的测试号发送文本消息,你可以在命令行看见打印出如下数据

 xml 
 ToUserName ![CDATA[gh_9d2d49e7e006]] /ToUserName 
 FromUserName ![CDATA[oBp2T0wK8lM4vIkmMTJfFpk6Owlo]] /FromUserName 
 CreateTime  /CreateTime 
 MsgType ![CDATA[text]] /MsgType 
 Content ![CDATA[JavaScript之禅]] /Content 
 MsgId  /MsgId 
 /xml 

恭喜,到此你已经可以接收到XML数据了。 但是我们还需要将XML转为JSON方便我们的使用,我们将用到这个包

$ npm install xml2js --save

我们需要写一个解析XML的异步函数,返回一个Promise对象

function parseXML(xml) {
 return new Promise((resolve, reject) = {
 xml2js.parseString(xml, { trim: true, explicitArray: false, ignoreAttrs: true }, function (err, result) {
 if (err) {
 return reject(err)
 resolve(result.xml)
}

接着调用parseXML方法,并打印出结果

const formatted = await parseXML(xml)
console.log(formatted)
return ctx.body = 'success'

一切正常的话*(实际开发中你可能会遇到各种问题)*,命令行将打印出如下JSON数据

{ ToUserName: 'gh_9d2d49e7e006',
 FromUserName: 'oBp2T0wK8lM4vIkmMTJfFpk6Owlo',
 CreateTime: '',
 MsgType: 'text',
 Content: 'JavaScript之禅',
 MsgId: '' }

到此,我们就能处理微信接收到的消息了,你可以自己测试关注、取消关注、发送各种类型的消息看看这个类型的消息所对应的XML数据格式都是怎么样的

三、回复消息

当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。

3.1 被动回复用户消息数据格式

前面说了交互的数据格式为XML,接收消息是XML的,我们回复回去也应该是XML。

微信公众账号回复用户文本消息时的XML数据格式如下所示:

 xml 
 ToUserName ![CDATA[toUser]] /ToUserName 
 FromUserName ![CDATA[fromUser]] /FromUserName 
 CreateTime  /CreateTime 
 MsgType ![CDATA[text]] /MsgType 
 Content ![CDATA[你好]] /Content 
 /xml 

微信公众账号回复用户图片消息时的XML数据格式如下所示:

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

篇幅所限就不一一列举了,请查阅【微信公众平台开发文档】

// return ctx.body = 'success' // ess
ctx.type = 'application/xml'
return ctx.body = ` xml 
 ToUserName ![CDATA[${formatted.FromUserName}]] /ToUserName 
 FromUserName ![CDATA[${formatted.ToUserName}]] /FromUserName 
 CreateTime ${new Date().getTime()} /CreateTime 
 MsgType ![CDATA[text]] /MsgType 
 Content ![CDATA[这儿是JavaScript之禅]] /Content 
 /xml `

3.2 使用ejs模板引擎处理回复内容

从这一小段代码中可以看出,被动回复消息就是把你想要回复的内容按照约定的XML格式返回即可。但是一段一段的拼XML那多麻烦。我们来加个模板引擎方便我们处理XML。模板引擎有很多,是其中一种,它使用起来十分简单

首先下载并引入

$ npm install ejs --save

pile()方法

% code % :运行 JavaScript 代码,不输出 %= code % :显示转义后的 HTML内容 %- code % :显示原始 HTML 内容

可以先看看这个ejs的小demo:

const ejs = require('ejs')
let tpl = `
 xml 
 ToUserName ![CDATA[ %-toUsername% ]] /ToUserName 
 FromUserName ![CDATA[ %-fromUsername% ]] /FromUserName 
 CreateTime %=createTime% /CreateTime 
 MsgType ![CDATA[ %=msgType% ]] /MsgType 
 Content ![CDATA[ %-content% ]] /Content 
 /xml 
piled = pile(tpl)
let mess = compiled({
 toUsername: '1234',
 fromUsername: '12345',
 createTime: new Date().getTime(),
 msgType: 'text',
 content: 'JavaScript之禅',
console.log(mess)
/* 将打印出如下信息 
 *================
 xml 
 ToUserName ![CDATA[1234]] /ToUserName 
 FromUserName ![CDATA[12345]] /FromUserName 
 CreateTime 94 /CreateTime 
 MsgType ![CDATA[text]] /MsgType 
 Content ![CDATA[JavaScript之禅]] /Content 
 /xml 

现在来编写被动回复消息的模板,各种if else,这儿就直接贴代码了

 xml 
 ToUserName ![CDATA[ %-toUsername% ]] /ToUserName 
 FromUserName ![CDATA[ %-fromUsername% ]] /FromUserName 
 CreateTime %=createTime% /CreateTime 
 MsgType ![CDATA[ %=msgType% ]] /MsgType 
 % if (msgType === 'news') { % 
 ArticleCount %=content.length% /ArticleCount 
 Articles 
 % content.forEach(function(item){ % 
 item 
 Title ![CDATA[ %-item.title% ]] /Title 
 Description ![CDATA[ %-item.description% ]] /Description 
 PicUrl ![CDATA[ %-item.picUrl || item.picurl || item.pic || item.thumb_url % ]] /PicUrl 
 Url ![CDATA[ %-item.url% ]] /Url 
 /item 
 % }); % 
 /Articles 
 % } else if (msgType === 'music') { % 
 Music 
 Title ![CDATA[ %-content.title% ]] /Title 
 Description ![CDATA[ %-content.description% ]] /Description 
 MusicUrl ![CDATA[ %-content.musicUrl || content.url % ]] /MusicUrl 
 HQMusicUrl ![CDATA[ %-content.hqMusicUrl || content.hqUrl % ]] /HQMusicUrl 
 /Music 
 % } else if (msgType === 'voice') { % 
 Voice 
 MediaId ![CDATA[ %-content.mediaId% ]] /MediaId 
 /Voice 
 % } else if (msgType === 'image') { % 
 Image 
 MediaId ![CDATA[ %-content.mediaId% ]] /MediaId 
 /Image 
 % } else if (msgType === 'video') { % 
 Video 
 MediaId ![CDATA[ %-content.mediaId% ]] /MediaId 
 Title ![CDATA[ %-content.title% ]] /Title 
 Description ![CDATA[ %-content.description% ]] /Description 
 /Video 
 % } else { % 
 Content ![CDATA[ %-content% ]] /Content 
 % } % 
 /xml 

现在就可以使用我们写好的模板回复XML消息了

const formatted = await parseXML(xml)
console.log(formatted)
 = {}
let type = 'text'
info.msgType = type
info.createTime = new Date().getTime()
info.toUsername = formatted.FromUserName
info.fromUsername = formatted.ToUserName
info.content = 'JavaScript之禅'
return ctx.body = )

我们可以把这个回复消息的功能写成一个函数

function reply (content, fromUsername, toUsername) {
  = {}
 var type = 'text'
 info.content = content || ''
 // 判断消息类型
 if (Array.isArray(content)) {
 type = 'news'
 } else if (typeof content === 'object') {
 if (content.hasOwnProperty('type')) {
 type = content.type
 info.content = content.content
 } else {
 type = 'music'
 info.msgType = type
 info.createTime = new Date().getTime()
 info.toUsername = toUsername
 info.fromUsername = fromUsername
 )

在回复消息的时候直接调用这个方法即可

const formatted = await parseXML(xml)
console.log(formatted)
const content = 'JavaScript之禅'
const replyMessageXml = reply(content, formatted.ToUserName, formatted.FromUserName)
return ctx.body = replyMessageXml

现在为了测试我们所写的这个功能,来实现一个【学我说话】的功能:

回复音乐将返回一个音乐类型的消息,回复文本图片,语音,公众号将返回同样的内容,当然了你可以在这个基础上进行各种发挥。

const formatted = await parseXML(xml)
console.log(formatted)
let content = ''
if (formatted.Content === '音乐') {
 content = {
 type: 'music',
 content: {
 title: 'Lemon Tree',
 description: 'Lemon Tree',
 musicUrl: 'xx.mp3'
} else if (formatted.MsgType === 'text') {
 content = formatted.Content
} else if (formatted.MsgType === 'image') {
 content = {
 type: 'image',
 content: {
 mediaId: formatted.MediaId
} else if (formatted.MsgType === 'voice') {
 content = {
 type: 'voice',
 content: {
 mediaId: formatted.MediaId
} else {
 content = 'JavaScript之禅'
const replyMessageXml = reply(content, formatted.ToUserName, formatted.FromUserName)
console.log(replyMessageXml)
ctx.type = 'application/xml'
return ctx.body = replyMessageXml

nice,到此时我们的测试号已经能够根据我们的消息做出相应的回应了

本篇再上一节的代码基础上做了一些优化,并重点讲解微信公众号的消息交互,最后实现了个【学我说话】的小功能。