后端接入微信公众号-自动回复功能-express

目录

1、配置服务器并填写相关信息

1-1)解决80端口正在被nginx服务的端口占用情况,完成服务端验证

2、接收用户的消息

2-1)在express项目中新增一个监听 post 请求的方法来获取用户消息

2-2)使用 xmljson 解析和处理用户消息

3、回复用户的消息-文本回复

4、回复用户的内容-图片

4-1)到公众号的素材库中上传图片:

4-2) 获取 access-token

4-3)获取公众号中的图片资源(永久素材列表)

4-4)返回一个图片给用户作为回复


1、配置服务器并填写相关信息

        接入微信公众号的第一步,首先要登录微信公众平台官网,在公众平台官网左侧导航栏的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey。

        其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。

         当填写好以上的信息后,微信服务器将发送GET请求到填写的服务器地址URL上,请求会携带以下的参数:

        开发者通过下方的检验方式对请求进行校验。若确认此次GET请求来自微信服务器,则原样返回echostr参数内容,便接入成功,否则接入失败。加密/校验流程如下:

        1)将token(就是我们设置的token)、timestamp、nonce三个参数进行字典序排序;

        2)将三个参数字符串拼接成一个字符串进行sha1加密;

        3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信;

1-1)解决80端口正在被nginx服务的端口占用情况,完成服务端验证

        根据官方文档给出的校验方法和示例,完成 express 的校验服务,并进行部署:

const express = require('express');
const crypto = require('crypto');

app.get('/wechat-test', (req, res)=>{
    const token = 'xxx_xxx';
    let {signature, timestamp, nonce, echostr} = req.query;
    let arr = [nonce, timestamp, token];
    arr.sort();                         // 排序
    let str = arr.join('');             // 转成字符串
    let shanum = crypto.createHash('sha1');
    let mysignature = shanum.update(str).digest('hex');

    let result = (mysignature === signature);
    if(result){
        res.send(echostr);
    }else{
        res.send();
    }
});

app.listen(80, ()=>{
    console.log('80 listening');
})

        填写 http 协议,80 端口的服务器地址。 将上面的 express 服务端程序部署到服务器中,试运行:

cd 项目部署的目录下
pm2 start index.js --name wechat-test

        发现报错:EADDRINUSE,即该80端口已经被占用。查看80端口的占用情况:

sudo lsof -i :80

         发现80端口nginx正在监听并提供服务:

         为了不破坏主页面原有的服务,选择使用请求转发的方式,将express项目的监听端口设置为3001,并在控制台安全组中开放该端口在nginx中配置将80的访问转发到3001端口

server{
    listen 80;

    ......

    # 请求转发到本地的3001端口进行处理
    location /wechat-test {
        proxy_pass http://localhost:3001;
    }
}

        请求 http://www.example.cn/wechat-test 将转发到 http://www.example.cn:3001/wechat-test

        重新在服务器中运行express服务,运行成功后在微信提交服务器配置,微信会get请求到目标URL进行验证,验证成功后即显示接入成功,选择启动服务器配置:

        

2、接收用户的消息

        当普通微信用户向公众账号发消息时,微信服务器将发送一个post请求到我们填写的URL地址中,并携带 XML 数据(包含用户输入的消息和一些其他的信息)。

        a. 文本消息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>
  <MsgDataId>xxxx</MsgDataId>
  <Idx>xxxx</Idx>
</xml>

        其中:

toUserName 开发者微信号
FromUserName 发送方账号(openid)
CreateTime 消息创建的时间
MsgType 消息的类型,如果是文本即为text
Content 文本消息的内容

        b.图片消息:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[image]]></MsgType>
  <PicUrl><![CDATA[this is a url]]></PicUrl>
  <MediaId><![CDATA[media_id]]></MediaId>
  <MsgId>1234567890123456</MsgId>
   <MsgDataId>xxxx</MsgDataId>
  <Idx>xxxx</Idx>
</xml>

        其中:

PicUrl 由系统生成的图片链接
MediaId 图片消息媒体id,可以调用获取临时素材接口拉取数据

2-1)在express项目中新增一个监听 post 请求的方法来获取用户消息

        直接添加到原来的项目文件中即可。

// 监听的路径要和填写的URL路径一致
app.post('/wechat-test',(req, res)=>{
    // 用来保存xml数据结果
    let data = '';

    // 接收xml数据
    req.on('data',(chunk)=>{
        data += chunk;
    })
    req.on('err',(err)=>{
        res.send('');
    })
    req.on('end',()=>{        // 数据接收完毕
        console.log('数据接收完毕\n', data);
        res.send();
    })
})

        在公众号中发送消息:1234。使用 pm2 logs 查看运行日志,查看项目的输出内容:

         此时在微信公众号中看不到任何来自公众号的回应。

2-2)使用 xmljson 解析和处理用户消息

         下载 xmljson :npm i xmljson;

        基本使用,将 xml 内容转换成对象的形式:

// 直接引入一个方法,将xml内容转换成对象
const toJson = require('xmljson').to_json;

app.post('/wechat-test',(req, res)=>{
    let data = '';
    req.on('data', chunk=>{
        data += chunk;
    })
    req.on('err',(err)=>{
        res.send('');
    })
    req.on('end',()=>{
        console.log('接收完毕:\n',data,'\n\n');
        toJson(data, (err, xmlObj)=>{
            if(err){
                console.log('转换错误');
                res.send('处理错误');
            }else{
                console.log('转换完成:\n', xmlObj);
                res.send('');
            }
        })
    })
})

        在公众号中发送消息:你好。

 

         转换后的对象又包含一个 xml 对象,该对象中的内容即为 “xml标签名:标签体内容”的形式,我们要得到用户的消息,只需要 xmlObj.xml.Content 即可。

3、回复用户的消息-文本回复

        回复的内容也需要是XML格式的,如:

         其中,Content 中的内容为回复的消息内容。ToUSerName 和 FromUserName 都是必须携带的内容。

        下面这个示例将回复用户:`你好,${用户发送过来的消息}`;

app.post('/wechat-test',(req, res)=>{
    let data = '';
    req.on('data',(chunk)=>{
        data += chunk;
    }).on('end',()=>{
        toJson(data, (err, obj)=>{
            if(err){
                console.log('错误:',new Date().toLocaleString());
                res.send(`
                    <xml>
                        <ToUserName><![CDATA[${obj.xml.FromUserName}]]></ToUserName>
                        <FromUserName><![CDATA[${obj.xml.ToUserName}]]></FromUserName>
                        <CreateTime>${new Date().getTime()}</CreateTime>
                        <MsgType><![CDATA[text]]></MsgType>
                        <Content><![CDATA[处理错误]]></Content>
                    </xml> 
                `)
            }else{
                let ans = `
                    <xml>
                        <ToUserName><![CDATA[${obj.xml.FromUserName}]]></ToUserName>
                        <FromUserName><![CDATA[${obj.xml.ToUserName}]]></FromUserName>
                        <CreateTime>${new Date().getTime()}</CreateTime>
                        <MsgType><![CDATA[text]]></MsgType>
                        <Content><![CDATA[你好,${obj.xml.Content}]]></Content>
                    </xml> 
                `
                res.send(ans);
            }
        })
    })
})

         在公众号发送消息,查看回复:

         

        注意,服务器需要在5s内做出回复,否则微信服务器将在5s后断掉连接,并重新发送请求,总共会发送3次请求,若服务器都没能按时回复,则会按超时处理。

        如果服务器无法保证在5秒内做出回复,可以考虑直接回复success,或返回一个空字符串(则什么都不回复)。

        如果服务器没有在5s内返回数据,或返回的数据不是xml格式的,微信都会在公众号对话中向用户发送系统提示消息“该公众号暂时无法提供服务,请稍后再试”。

4、回复用户的内容-图片

 4-1)到公众号的素材库中上传图片:

4-2) 获取 access-token

         获取素材库中的素材需要使用到 access-token ,可以发送以下的请求来后期 access-token:

GET  https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

        如果要获取的是 access-token 则 grant_type 填写 client_credential;

        appid 和 secret 可以到公众号-基本配置中配置并获取:

         请求成功后,在res.data 中会有以下的信息,其中expires_in 是有效时间s:

参考:微信开放文档

         

4-3)获取公众号中的图片资源(永久素材列表)

         获取永久素材列表可以发送以下请求:

post

https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=ACCESS_TOKEN

access-token 即刚刚获取到的access_token

需要携带的data:{
    "type":"image",  // 表示请求图片资源,还可以是视频(video)、语音 (voice)、图文(news)
    "offset":0,      // 从全部素材的该偏移位置开始返回,0表示从第一个素材 返回
    "count":5,       // 表示返回的数量在 1-n 之间,当前为 1-5
}

        一个示例:

// 处理图片类型的消息-返回固定的图片
function dealImgMsg(xmlObj, res){
    // 先获取 access_token
    axios.get('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${YOU_APPID}&secret=${YOU_SECRET}').then((result)=>{
        // 提取出来的 access_token
        let access_token = result.data.access_token;
        reqData = {
            'type':'image',
            'offset':0,
            'count':5
        }
        // 再请求获取图片类型的资源
        axios.post(`https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${access_token}`,reqData).then(result1=>{
            let data = result1.data;
            // 图片资源存放在data.item 中 ,查看结果
            console.log(data.item, data.item[0]);
            res.send();
        })
        
    })
}

        输出的内容(media_id 即我们需要使用到的内容):

参考:微信开放文档

4-4)返回一个图片给用户作为回复

const axios = require('axios');

app.post('/wechat-test',(req, res)=>{
    let data = '';
    req.on('data',(chunk)=>{
        data += chunk;
    }).on('end',()=>{
        toJson(data, (err, result)=>{
            let xmlObj = result.xml;
            if(err){
                console.log('错误:',new Date().toLocaleString());
                res.send()
            }else{
                switch (xmlObj.MsgType) {
                    case 'image':
                        dealImgMsg(xmlObj, res);
                        break;
                    case 'text':
                        dealTextMsg(xmlObj, res);
                        break;
                    default:
                        res.send()
                }
            }
            
        })
    })
})

// 处理图片类型的消息-返回固定的图片
function dealImgMsg(xmlObj, res){
    axios.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${YOU_APPID}&secret=${YOU_SECRET}`).then((result)=>{
        let access_token = result.data.access_token;
        reqData = {
            'type':'image',
            'offset':0,
            'count':5
        }
        axios.post(`https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${access_token}`,reqData).then(result1=>{
            let data = result1.data;
            let resXML = `
                <xml>
                    <ToUserName><![CDATA[${xmlObj.FromUserName}]]></ToUserName>
                    <FromUserName><![CDATA[${xmlObj.ToUserName}]]></FromUserName>
                    <CreateTime>${new Date().getTime()}</CreateTime>
                    <MsgType><![CDATA[image]]></MsgType>
                    <Image>
                        <MediaId><![CDATA[${data.item[0].media_id}]]></MediaId>
                    </Image>
                </xml>
            `
            res.send(resXML);
        })
    })
}
// 处理文字类型的消息-返回用户发送来的消息加自定义的内容
function dealTextMsg(xmlObj, res){
    let resXML = `
        <xml>
            <ToUserName><![CDATA[${xmlObj.FromUserName}]]></ToUserName>
            <FromUserName><![CDATA[${xmlObj.ToUserName}]]></FromUserName>
            <CreateTime>${new Date().getTime()}</CreateTime>
            <MsgType><![CDATA[text]]></MsgType>
            <Content><![CDATA[你好, ${xmlObj.Content}]]></Content>
        </xml>
    `;
    res.send(resXML);
}

猜你喜欢

转载自blog.csdn.net/hao_13/article/details/130939035