项目结构
>config/wechat.json 微信公众号的配置文件
>controllers/oauth.js 微信网页授权接口(下一篇再细讲讲)
>controllers/wechat.js 微信公众号接口(包括接入接口和其他调用微信api的接口)
>wechat/access_token.json 请求微信api接口之前都需要使用的access_token
>wechat/crytoGraphy.js 加密解密文件(这里使用的是明文方式,未用到)
>wechat/menus.json 微信公众号的自定义菜单
>wechat/wechat.js 调用微信api的方法
项目的架构说明和使用步骤可以参考前面一篇的‘node.js 接口调用示例’: https://www.cnblogs.com/eye-like/p/11743744.html
一些说明点
1、公众号的接入接口和接收消息接口
两个接口的地址是一致的,区别是:
> 接入接口是GET请求,需要的是对get的接口参数进行解析验证,然后按需返回就可以了
> 接收消息接口是POST请求,需要先解析微信发送过来的消息,然后根据情况决定是否返回消息
WeChat.prototype.handleMsg = async function (ctx) { return new Promise((resolve, reject) => { // let req = ctx.request; // let res = ctx.response; let req = ctx.req; var buffer = [], that = this; //实例微信消息加解密 // var cryptoGraphy = new CryptoGraphy(that.config,ctx.request); //监听 data 事件 用于接收数据 req.on('data', function (data) { // logger.info("on data", data); buffer.push(data); }); req.on('end', function () { // logger.info("on end"); var msgXml = Buffer.concat(buffer).toString('utf-8'); parseString(msgXml, { explicitArray: false }, function (err, result) { // logger.info("on result", result); result = result.xml; resolve(result) }) }); }) },
* 接收消息使用的是Promise函数封装,目的是将微信的收发消息两块做隔离
* 涉及到node.js 接收post参数的方式,需要ctx.req.on('data',function(){})方法接受参数,并在ctx.req.on(‘end’,function(){})函数中接受最终的获取参数
* 因为微信发过来的消息是xml格式,所以在node.js 中需要xml2js模块(非node.js内置模块,需要先安装依赖)将xml文件解析
/** * 微信消息回复 * @param {ctx} context 对象 * @param {result} 微信消息 */ WeChat.prototype.responseMsg = function (ctx, result) { var toUser = result.ToUserName; //接收方微信 var fromUser = result.FromUserName; //发送仿微信 var reportMsg = ""; //声明回复消息的变量 if (result.MsgType.toLowerCase() === "event") { //判断事件类型 switch (result.Event.toLowerCase()) { case 'subscribe': //回复消息 var content = "欢迎关注 线上随访 公众号 "; content += "我们致力于帮助出院康复病人与医生建立便捷的沟通渠道~ "; reportMsg = msg.txtMsg(fromUser, toUser, content); break; case 'click': var contentArr = [{ Title: "Node.js 微信自定义菜单", Description: "使用Node.js实现自定义微信菜单", PicUrl: "http://img.blog.csdn.net/20170605162832842?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72868520" }, { Title: "Node.js access_token的获取、存储及更新", Description: "Node.js access_token的获取、存储及更新", PicUrl: "http://img.blog.csdn.net/20170528151333883?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72783631" }, { Title: "Node.js 接入微信公众平台开发", Description: "Node.js 接入微信公众平台开发", PicUrl: "http://img.blog.csdn.net/20170605162832842?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72765279" } ]; //回复图文消息 reportMsg = msg.graphicMsg(fromUser, toUser, contentArr); break; } } else { //判断消息类型为 文本消息 if (result.MsgType.toLowerCase() === "text") { switch (result.Content) { case '1': reportMsg = msg.txtMsg(fromUser, toUser, 'Hello ,线上随访公众号开通了,快来使用吧……'); break; case '2': reportMsg = msg.txtMsg(fromUser, toUser, 'Ha Ha,我还有更多的功能待发掘呢,敬请期待吧……'); break; case '文章': var contentArr = [{ Title: "Node.js 微信自定义菜单", Description: "使用Node.js实现自定义微信菜单", PicUrl: "http://img.blog.csdn.net/20170605162832842?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72868520" }, { Title: "Node.js access_token的获取、存储及更新", Description: "Node.js access_token的获取、存储及更新", PicUrl: "http://img.blog.csdn.net/20170528151333883?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72783631" }, { Title: "Node.js 接入微信公众平台开发", Description: "Node.js 接入微信公众平台开发", PicUrl: "http://img.blog.csdn.net/20170605162832842?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHZrQ29kZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast", Url: "http://blog.csdn.net/hvkcoder/article/details/72765279" } ]; //回复图文消息 reportMsg = msg.graphicMsg(fromUser, toUser, contentArr); break; default: reportMsg = msg.txtMsg(fromUser, toUser, '没有这个选项哦'); break; } // logger.info("on reportMsg", reportMsg); } } //返回给微信服务器 ctx.body = reportMsg },
# 主要任务是根据微信消息的类型‘MsgType’,来执行不同的动作
# 事件推送消息(MsgType==‘event’)
* 关注取消事件 subscribe,unsubscribe
此时可以做数据库接入录入订阅者信息或者更新订阅者信息
* 扫描带参数二维码事件
用户未关注时:qrscene_ +二维码参数值
用户已关注时:scan
* 上报地理位置事件 LOCATION
* 自定义菜单事件 CLICK
# 普通消息推送 (MsgType=='text')
可分为 文本消息、图片消息、语音消息、视频消息、小视频消息、地理位置消息、链接消息
2、有参考的node.js 发送get、post请求的两个方法
/** * 用于处理 https Get请求方法 * @param {String} url 请求地址 */ this.requestGet = function (url) { return new Promise(function (resolve, reject) { https.get(url, function (res) { var buffer = [], result = ""; //监听 data 事件 res.on('data', function (data) { buffer.push(data); }); //监听 数据传输完成事件 res.on('end', function () { result = Buffer.concat(buffer).toString('utf-8'); //将最后结果返回 resolve(result); }); }).on('error', function (err) { reject(err); }); }); }
/** * 用于处理 https Post请求方法 * @param {String} url 请求地址 * @param {JSON} data 提交的数据 */ this.requestPost = function (url, data) { return new Promise(function (resolve, reject) { //解析 url 地址 var urlData = urltil.parse(url); //设置 https.request options 传入的参数对象 var options = { //目标主机地址 hostname: urlData.hostname, //目标地址 path: urlData.path, //请求方法 method: 'POST', //头部协议 headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(data, 'utf-8') } }; var req = https.request(options, function (res) { var buffer = [], result = ''; //用于监听 data 事件 接收数据 res.on('data', function (data) { buffer.push(data); }); //用于监听 end 事件 完成数据的接收 res.on('end', function () { result = Buffer.concat(buffer).toString('utf-8'); resolve(result); }) }) //监听错误事件 .on('error', function (err) { console.log(err); reject(err); }); //传入数据 req.write(data); req.end(); }); } }
注:使用前需要引用内置的https中间件
3、关于access_token
调用微信接口的时候都需要发送微信接口凭证access_token(除微信网页授权中获取的access_token并不是这个access_token),因此请求微信接口的第一步都是要先获取这个access_oken
/** * 获取微信 access_token */ WeChat.prototype.getAccessToken = function () { var that = this; return new Promise(function (resolve, reject) { //获取当前时间 var currentTime = new Date().getTime(); //格式化请求地址 var url = util.format(that.apiURL.accessTokenApi, that.apiDomain, that.appID, that.appScrect); //判断 本地存储的 access_token 是否有效 if (accessTokenJson.access_token === "" || accessTokenJson.expires_time < currentTime) { that.requestGet(url).then(function (data) { var result = JSON.parse(data); if (data.indexOf("errcode") < 0) { accessTokenJson.access_token = result.access_token; accessTokenJson.expires_time = new Date().getTime() + (parseInt(result.expires_in) - 200) * 1000; //更新本地存储的 fs.writeFile('./wechat/access_token.json', JSON.stringify(accessTokenJson)); //将获取后的 access_token 返回 resolve(accessTokenJson.access_token); } else { //将错误返回 resolve(result); } }); } else { //将本地存储的 access_token 返回 resolve(accessTokenJson.access_token); } }); }
var url = util.format(that.apiURL.accessTokenApi, that.apiDomain, that.appID, that.appScrect);
url参数依次为:
that.apiURL.accessTokenApi:"%scgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"
that.apiDomain:"https://api.weixin.qq.com/"
that.appID:公众号的appID
that.appScrect:公众号的密匙
请求后获取的accee_token 有效时间为7200s,在此时间内,再次请求微信接口都可以使用已经缓存的accee_token
4、关于菜单
最好是在请求接口的时候加入请求密匙(如oauth2认证),尤其是更新和删除