egg公众号开发

新建项目

$ npm i egg-init -g
$ egg-init wechat_public_number_demo --type=simple
$ cd wechat_public_number_demo
$ npm i
$ npm run dev
$ open localhost:7001

接入公众号

由于配置的URL必须要域名,为了实现本地开发,本人采用Sunny-Ngrok进行端口映射,具体用法请自行百度。
注意:端口若不为80,则无法配置成功。
config/config.default.js

config.wechat_config = {
  token: 'wechat_public_number_demo',
};

router.js

router.get('/wechat', controller.home.fromWechat);

controller/home.js

const crypto = require('crypto');

async fromWechat() {
  const token = this.ctx.app.config.wechat_config.token;
  const query = this.ctx.query;
  const timestamp = query.timestamp;
  const nonce = query.nonce;
  const signature = query.signature;
  const echostr = query.echostr;
  const str = [ token, timestamp, nonce ].sort().join('');
  const hash = crypto.createHash('sha1');
  hash.update(str);
  const sha = hash.digest('hex');
  if (sha === signature) {
    this.ctx.body = echostr;
  }
}

package.json

"dev": "egg-bin dev --port=80",

获取access_token

config/config.default.js

appid: 'wx230f799414023398',
secret: '27118b180d47a9b11c094a03cea63a74',
getAccessTokenUrl: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET',

schedule/access_token.js

'use strict';

const Subscription = require('egg').Subscription;

class UpdateCache extends Subscription {
  static get schedule() {
    return {
      interval: '2h',
      type: 'all',
    };
  }

  async subscribe() {
    const config = this.ctx.app.config.wechat_config;
    const url = config.getAccessTokenUrl.replace('APPID', config.appid).replace('APPSECRET', config.secret);
    const res = await this.ctx.curl(url, {
      dataType: 'json',
    });
    console.log(res.data.access_token);
    this.ctx.app.access_token = res.data.access_token;
  }
}

module.exports = UpdateCache;

app.js

'use strict';

module.exports = app => {
  app.beforeStart(async () => {
    await app.runSchedule('access_token');
  });
};

自定义菜单

由于egg内置了安全插件,请求会报错,暂时的解决方案是关闭csrf检查。
config.default.js

postCreateMenuUrl: 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN',

config.security = {
  csrf: {
    enable: false,
  },
};

app.js

const menu = {
  button: [
    {
      name: '组一',
      sub_button: [
        {
          type: 'click',
          name: 'click',
          key: 'click',
        },
        {
          type: 'view',
          name: 'view',
          url: 'http://chrish.free.ngrok.cc',
        },
      ],
    },
    {
      name: '组二',
      sub_button: [
        {
          type: 'scancode_waitmsg',
          name: '扫码带提示',
          key: 'scancode_waitmsg',
        },
        {
          type: 'scancode_push',
          name: '扫码推事件',
          key: 'scancode_push',
        },
        {
          type: 'pic_sysphoto',
          name: '系统拍照发图',
          key: 'pic_sysphoto',
        },
        {
          type: 'pic_photo_or_album',
          name: '拍照或者相册发图',
          key: 'pic_photo_or_album',
        },
        {
          type: 'pic_weixin',
          name: '微信相册发图',
          key: 'pic_weixin',
        },
      ],
    },
    {
      type: 'location_select',
      name: '发送位置',
      key: 'location_select',
    },
  ],
};
const config = app.config.wechat_config;
const url = config.postCreateMenuUrl.replace('ACCESS_TOKEN', app.access_token);
const res = await app.curl(url, {
  method: 'POST',
  contentType: 'json',
  data: menu,
  dataType: 'json',
});
console.log(res.data.errcode);

消息管理

由于微信服务器发送和接收的都是XML数据包,所以引入xml2js对XML进行解析和封装。
特别注意: xml2js对XML的解析是异步的,若在其中直接返回数据,微信服务器是收不到的,会一直提示“该公众号提供的服务出现故障,请稍后再试”。
package.json

"xml2js": "^0.4.19"

util/xml2js.js

'use strict';
const xml2js = require('xml2js');

const parseXml = str => {
  return new Promise((resolve, reject) => {
    const parseString = xml2js.parseString;
    parseString(str, { explicitArray: false }, (err, json) => {
      if (json) {
        resolve(json.xml);
      } else {
        reject(err);
      }
    });
  });
};

const createXml = obj => {
  const builder = new xml2js.Builder({
    rootName: 'xml',
    headless: true,
    cdata: true,
  });
  return builder.buildObject(obj);
};

module.exports = {
  parseXml,
  createXml,
};

middleware/xml2js.js

'use strict';
const xml2js = require('../util/xml2js');

module.exports = () => {
  return async (ctx, next) => {
    if (ctx.method === 'POST' && ctx.is('text/xml')) {
      const promise = new Promise((resolve, reject) => {
        let data = '';
        ctx.req.on('data', chunk => {
          data += chunk;
        });
        ctx.req.on('end', () => {
          console.log(data);
          xml2js.parseXml(data).then(result => {
            resolve(result);
          }).catch(err => {
            reject(err);
          });
        });
      });
      await promise.then(result => {
        ctx.req.body = result;
      }).catch(() => {
        ctx.req.body = '';
      });
    }
    await next();
  };
};

config.default.js

config.middleware = [ 'xml2js' ];

router.js

router.post('/wechat', controller.home.toWechat);

controller/home.js

const replyMsg = require('../util/reply_msg');

async toWechat() {
  const message = this.ctx.req.body;
  if (message) {
    const MsgType = message.MsgType;
    let reply;
    if (MsgType === 'event') {
      reply = this.handleEvent(message);
    } else {
      reply = this.handleMsg(message);
    }
    if (reply) {
      const result = replyMsg(message, reply);
      console.log(result);
      this.ctx.body = result;
      return true;
    }
  }
  this.ctx.body = 'success';
}

handleEvent(message) {
  const { Event, EventKey, Ticket, Latitude, Longitude, Precision } = message;
  let reply;
  switch (Event) {
    case 'subscribe':
      console.log(EventKey);
      console.log(Ticket);
      reply = '欢迎关注XXX的测试公众号';
      break;
    case 'unsubscribe':
      reply = '';
      break;
    case 'SCAN':
      reply = 'EventKey:' + EventKey + ', Ticket:' + Ticket;
      break;
    case 'LOCATION':
      reply = 'Latitude:' + Latitude + ', Longitude:' + Longitude + ', Precision:' + Precision;
      break;
    case 'CLICK':
      reply = 'EventKey:' + EventKey;
      break;
    case 'VIEW':
      reply = 'EventKey:' + EventKey;
      break;
    default:
      reply = '';
      break;
  }
  return reply;
}

handleMsg(message) {
  const { MsgType, Content, PicUrl, MediaId, Recognition, Label, Url } = message;
  let reply;
  switch (MsgType) {
    case 'text':
      reply = Content;
      break;
    case 'image':
      reply = PicUrl;
      break;
    case 'voice':
      console.log(Recognition);
      reply = MediaId;
      break;
    case 'video':
      reply = MediaId;
      break;
    case 'shortvideo':
      reply = MediaId;
      break;
    case 'location':
      reply = Label;
      break;
    case 'link':
      reply = Url;
      break;
    default:
      reply = '';
      break;
  }
  return reply;
}

util/reply_msg.js

'use strict';
const xml2js = require('./xml2js');

module.exports = (message, Content) => {
  const obj = {
    ToUserName: message.FromUserName,
    FromUserName: message.ToUserName,
    CreateTime: new Date().getTime(),
    MsgType: 'text',
    Content,
  };
  return xml2js.createXml(obj);
};

猜你喜欢

转载自blog.csdn.net/u011628981/article/details/81021045
今日推荐