本人前端渣渣一枚,這篇文章是第一次寫,如果有硬核bug,請大佬們輕噴、指出... 另外,本文不涉及任何接口安全、參數校驗之類的東西,默認對調用方無腦級的信任:joy: 目前自用的接口包括但不限于以下這些
|--- 微信相關| |--- 0. 處理微信推過來的一些消息| |--- 1. 獲取微信SDK配置參數| |--- 2. 微信鑒權登陸| |--- 3. 獲取微信用戶信息| |--- 4. 獲取AccessToken| |--- 5. 批量發送模版消息| |--- 6. 獲取模版消息列表| |--- 7. 批量發送客服消息
背景
創建ThinkJS項目
官網
簡介
ThinkJS 是一款面向未來開發的 Node.js 框架,整合了大量的項目最佳實踐,讓企業級開發變得如此簡單、高效。從 3.0 開始,框架底層基于 Koa 2.x 實現,兼容 Koa 的所有功能。
安裝腳手架
$ npm install -g think-cli
創建及啟動項目
$ thinkjs new demo;$ cd demo;$ npm install; $ npm start;
目錄結構
|--- development.js //開發環境下的入口文件|--- nginx.conf //nginx 配置文件|--- package.json|--- pm2.json //pm2 配置文件|--- production.js //生產環境下的入口文件|--- README.md|--- src| |--- bootstrap //啟動自動執行目錄 | | |--- master.js //Master 進程下自動執行| | |--- worker.js //Worker 進程下自動執行| |--- config //配置文件目錄| | |--- adapter.js // adapter 配置文件 | | |--- config.js // 默認配置文件 | | |--- config.production.js //生產環境下的默認配置文件,和 config.js 合并 | | |--- extend.js //extend 配置文件 | | |--- middleware.js //middleware 配置文件 | | |--- router.js //自定義路由配置文件| |--- controller //控制器目錄 | | |--- base.js| | |--- index.js| |--- logic //logic 目錄| | |--- index.js| |--- model //模型目錄| | |--- index.js|--- view //模板目錄| |--- index_index.html
安裝think-wechat插件
介紹
微信中間件,基于 node-webot/wechat,支持 thinkJS 3.0
安裝
$ npm install think-wechat --save
或
$ cnpm install think-wechat --save
配置
文件:/src/config/middleware.js
const wechat = require('think-wechat')module.exports = [  ...   {    handle: wechat,    match: '/index',    options: {      token: '', // 令牌,和公眾號/基本配置/服務器配置里面寫一樣的即可      appid: '', // 這里貌似可以隨便填,因為我們后面要用數據庫配置多個公眾號      encodingAESKey: '',      checkSignature: false    }  }, {    handle: 'payload', // think-wechat 必須要在 payload 中間件前面加載,它會代替 payload 處理微信發過來的 post 請求中的數據。    options: {      keepExtensions: true,      limit: '5mb'    }  },]注:match下我這里寫的是 /index ,對應的項目文件是 /src/controller/index.js ,對應的公眾號后臺所需配置的服務器地址就是 http(https)://域名:端口/index
創建數據庫和相關表
我這里創建了三個微信的相關表。
配置表:wx_config
| 字段 | 類型 | 說明 | 
|---|---|---|
| id | int | 主鍵 | 
| name | varchar | 名稱 | 
| appid | varchar | appid | 
| secret | varchar | secret | 
| 字段 | 類型 | 注釋 | 
|---|---|---|
| id | int | 主鍵 | 
| subscribe | int | 用戶是否訂閱該公眾號標識,值為0時,代表此用戶沒有關注該公眾號,拉取不到其余信息。 | 
| nickname | varchar | 用戶的昵稱 | 
| sex | int | 用戶的性別,值為1時是男性,值為2時是女性,值為0時是未知 | 
| language | varchar | 用戶所在省份 | 
| city | varchar | 用戶所在城市 | 
| province | varchar | 用戶所在省份 | 
| country | varchar | 用戶所在國家 | 
| headimgurl | longtext | 用戶頭像,最后一個數值代表正方形頭像大小(有0、46、64、96、132數值可選,0代表640*640正方形頭像),用戶沒有頭像時該項為空。若用戶更換頭像,原有頭像URL將失效。 | 
| subscribe_time | double | 用戶關注時間,為時間戳。如果用戶曾多次關注,則取最后關注時間 | 
| unionid | varchar | 只有在用戶將公眾號綁定到微信開放平臺帳號后,才會出現該字段。 | 
| openid | varchar | 用戶的標識,對當前公眾號唯一 | 
| wx_config_id | int | 對應配置的微信號id | 
模版消息日志表:wx_template_log
| 字段 | 類型 | 注釋 | 
|---|---|---|
| id | int | 主鍵 | 
| template_id | varchar | 模版id | 
| openid | varchar | 用戶的標識,對當前公眾號唯一 | 
| url | varchar | 跳轉url | 
| miniprogram | varchar | 跳轉小程序 | 
| data | varchar | 發送內容json字符串 | 
| add_time | double | 添加時間戳 | 
| send_time | double | 發送時間戳 | 
| send_status | varchar | 發送結果 | 
| wx_config_id | double | 對應配置的微信號id | 
| uuid | varchar | 本次發送的uuid,業務系統可通過uuid查詢模版消息推送結果 | 
處理微信推送消息
文件目錄
/src/controller/index.js
文件內容
module.exports = class extends think.Controller {  /*  * 入口:驗證開發者服務器  * 驗證開發者服務器,這里只是演示,所以沒做簽名校驗,實際上應該要根據微信要求進行簽名校驗  */  async indexAction() {    let that = this;    if (that.method != 'REPLY') {      return that.json({code: 1, msg: '非法請求', data: null})    }    const {echostr} = that.get();    return that.end(echostr);  }     /*  * 文字  * 用于處理微信推過來的文字消息  */  async textAction() {    let that = this;    let {id, signature, timestamp, nonce, openid} = that.get();    let {ToUserName, FromUserName, CreateTime, MsgType, Content, MsgId} = that.post();    .....    that.success('')  }    /*  * 事件  * 用于處理微信推過來的事件消息,例如點擊菜單等  */  async eventAction() {    let that = this;    let {id, signature, timestamp, nonce, openid} = that.get();    let {ToUserName, FromUserName, CreateTime, MsgType, Event, EventKey, Ticket, Latitude, Longitude, Precision} = that.post();    switch (Event) {      case 'subscribe': // 關注公眾號        ...        break;      case 'unsubscribe': // 取消關注公眾號        ...        break;      case 'SCAN': // 已關注掃碼        ...        break;      case 'LOCATION': // 地理位置        ...        break;      case 'CLICK': // 自定義菜菜單        ...        break;      case 'VIEW': // 跳轉        ...        break;      case 'TEMPLATESENDJOBFINISH':// 模版消息發送完畢        ...        break;    }     that.success('')  }}注:支持的action包括: textAction 、 imageAction 、 voiceAction 、 videoAction 、 shortvideoAction 、 locationAction 、 linkAction 、 eventAction 、 deviceTextAction 、 deviceEventAction 。
公眾號后臺配置

注:后面跟的id參數是為了區分是哪個公眾號推過來的消息,在上面的接口參數中也有體現
微信相關API的編寫
目錄結構
|--- src| |--- controller //控制器目錄 | | |--- index.js // 處理微信推送的消息,上面有寫到| | |--- common.js // 一些公共方法| | |--- open // 開放給其他業務服務的api接口| | | |--- wx.js| | |--- private // 放一些內部調用的方法,調用微信api的方法主要在這里面| | | |--- wx.js
這個目錄結構可能不太合理,后期再改進吧:grin:
公共方法
// src/controller/common.jsimport axios from 'axios'import {baseSql} from "./unit";module.exports = class extends think.Controller {  // 獲取appinfo  async getWxConfigById(id) {    let that = this;    let data = await that.cache(`wx_config:wxid_${id}`, async () => {      // 數據庫內取      let info = await that.model('wx_config', baseSql).where({id: id}).find();      if (!think.isEmpty(info)) {        return info      }    })    return data || {}  }  // 獲取access_token  async getAccessToken(id) {    let that = this;    let accessToken = await that.cache(`wx_access_token:wxid_${id}`, async () => {      let {appid, secret} = await that.getWxConfigById(id);      let {data} = await axios({        method: 'get',        url: `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`      });      return data.access_token    });    return accessToken  }}接口過濾器
所有開放出來的接口的前置方法,俗稱過濾器?所有開放的接口必傳get參數是 wxid ,對應數據庫表wx_config里面 id
// src/controller/open/wx.jsasync __before() {  let that = this;  let wxid = that.get('wxid');  if (think.isEmpty(wxid)) {    return that.json({code: 1, msg: 'wxid不存在'})  }  that.wxConfig = await that.controller('common').getWxConfigById(wxid);  if (think.isEmpty(that.wxConfig)) {    return that.json({code: 1, msg: 'wxid不存在'})  }}接口 - 獲取AccessToken
代碼
// src/controller/open/wx.jsasync get_access_tokenAction() {  let that = this;  let accessToken = await that.controller('common').getAccessToken(that.wxConfig.id);  return that.json({code: 0, msg: '', data: {access_token: accessToken}})}文檔
 
 
接口 - 獲取微信sdk的config
代碼
// src/controller/open/wx.jsasync get_wxsdk_configAction() {  let that = this;  let {url} = that.get();  if (think.isEmpty(url)) {    return that.json({code: 1, msg: '參數不正確'})  }  let sdkConfig = await that.controller('private/wx').getSdkConfig(that.wxConfig.id, url);  return that.json({code: 0, msg: '', data: sdkConfig})}// src/controller/private/wx.jsconst sha1 = require('sha1');const getTimestamp = () => parseInt(Date.now() / 1000)const getNonceStr = () => Math.random().toString(36).substr(2, 15)const getSignature = (params) => sha1(Object.keys(params).sort().map(key => `${key.toLowerCase()}=${params[key]}`).join('&'));async getSdkConfig(id, url) {  let that = this;  let {appid} = await that.controller('common').getWxConfigById(id);  let shareConfig = {    nonceStr: getNonceStr(),    jsapi_ticket: await that.getJsapiTicket(id),    timestamp: getTimestamp(),    url: url  }  return {    appId: appid,    timestamp: shareConfig.timestamp,    nonceStr: shareConfig.nonceStr,    signature: getSignature(shareConfig)  }}文檔
 
 
接口 - 獲取UserInfo
代碼
// src/controller/open/wx.jsasync get_userinfoAction() {  let that = this;  let {openid} = that.get();  if (think.isEmpty(openid)) {    return that.json({code: 1, msg: '參數不正確'})  }  let userInfo = await that.controller('private/wx').getUserInfo(that.wxConfig.id, openid);  if (think.isEmpty(userInfo)) {    return that.json({code: 1, msg: 'openid不存在', data: null})  }  return that.json({code: 0, msg: '', data: userInfo})}// src/controller/private/wx.jsasync getUserInfo(id, openid) {  let that = this;  let userInfo = await that.cache(`wx_userinfo:wxid_${id}:${openid}`, async () => {    //先取數據庫    let model = that.model('wx_userinfo', baseSql);    let userInfo = await model.where({wx_config_id: id, openid: openid}).find();    if (!think.isEmpty(userInfo) && userInfo.subscribe == 1 && userInfo.unionid != null) {      return userInfo    }    //如果數據庫內沒有,取新的存入數據庫    let accessToken = await that.controller('common').getAccessToken(id);    let url = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${accessToken}&openid=${openid}&lang=zh_CN`;    let {data} = await axios({method: 'get', url: url});    if (data.openid) {      //命中修改,沒有命中添加      let resId = await model.thenUpdate(        Object.assign(data, {wx_config_id: id}),        {openid: openid, wx_config_id: id});      return await model.where({id: resId}).find();    }  })  return userInfo}文檔
 
 
接口 - 批量發送文字客服消息
代碼
// src/controller/open/wx.jsasync send_msg_textAction() {  let that = this;  let {list} = that.post();  if (think.isEmpty(list)) {    return that.json({code: 1, msg: '參數不正確'})  }  that._sendMsgTextList(that.wxConfig.id, list);  return that.json({code: 0, msg: '', data: null}) }  async _sendMsgTextList(wxid, list) {  let that = this;  let apiWxController = that.controller('private/wx');  for (let item of list) {    let data = await apiWxController.sendMsgText(wxid, item.openid, item.text)  }}// src/controller/private/wx.jsasync sendMsgText(id, openid, content) {  let that = this;  let accessToken = await that.controller('common').getAccessToken(id);  let url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`  let {data} = await axios({    method: 'post', url: url, data: {"msgtype": 'text', "touser": openid, "text": {"content": content}}  })  return data;}文檔
 
 
寫在結尾
其實還有很多接口,這里就不全部列出來了。
應該能看出來,在這個項目里面并不僅僅是把微信的接口做了個簡單的轉發,而是有一些自己的處理邏輯在里面。
比如獲取微信用戶信息的時候,會先判斷緩存里有沒有,如果沒有就取數據庫,如果還沒有再去微信的接口取;如果數據庫有,并且關注字段是未關注的話,還是會調用微信的接口取一波再更新。 反正一天內,微信接口的調用次數是絕對夠用的。
再比如批量發送模版消息,中控服務在收到請求后會先創建一個uuid,要發的模版消息全部保存到數據庫內,直接把uuid返給調用方。 然后中控會異步用uuid取出來這批模版消息,一個一個發,一個一個更新結果。 這樣在業務方調用發送模版消息之后,無需等待全部發送完畢,就可以用拿到的uuid,去中控查詢這次批量發送的狀態結果。
目前是綁了七八個公眾號,在沒燒過香的前提下,還沒出過什么問題
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答