使用ThinkJs搭建微信中控服務(wù)的實(shí)現(xiàn)方法
本人前端渣渣一枚,這篇文章是第一次寫(xiě),如果有硬核bug,請(qǐng)大佬們輕噴、指出... 另外,本文不涉及任何接口安全、參數(shù)校驗(yàn)之類的東西,默認(rèn)對(duì)調(diào)用方無(wú)腦級(jí)的信任:joy: 目前自用的接口包括但不限于以下這些
|--- 微信相關(guān) | |--- 0. 處理微信推過(guò)來(lái)的一些消息 | |--- 1. 獲取微信SDK配置參數(shù) | |--- 2. 微信鑒權(quán)登陸 | |--- 3. 獲取微信用戶信息 | |--- 4. 獲取AccessToken | |--- 5. 批量發(fā)送模版消息 | |--- 6. 獲取模版消息列表 | |--- 7. 批量發(fā)送客服消息
背景
- 【需求】小項(xiàng)目很多很雜,而且大部分需求都是基于微信開(kāi)發(fā)的,每次都查微信文檔的話就會(huì)很郁悶:unamused:...
- 【號(hào)多】公眾號(hào)超級(jí)多,項(xiàng)目中偶爾會(huì)涉及借權(quán)獲取用戶信息(在不綁定微信開(kāi)放平臺(tái)的前提下,需要臨時(shí)自建各個(gè)公眾號(hào)的openid關(guān)聯(lián)關(guān)系),類似這樣同時(shí)需要不止一個(gè)公眾號(hào)配合來(lái)完成一件事的需求,就容易把人整懵逼...
- 【支付】微信支付的商戶號(hào)也很多,而且有時(shí)候支付需要用的商戶號(hào),還不能用關(guān)聯(lián)的公眾號(hào)取出來(lái)的openid去支付...
- 【官方】微信官方文檔建議!把獲取AccessToken等微信API抽離成單獨(dú)的服務(wù)... 等等等等........所以...:joy:
創(chuàng)建ThinkJS項(xiàng)目
官網(wǎng)
簡(jiǎn)介
ThinkJS 是一款面向未來(lái)開(kāi)發(fā)的 Node.js 框架,整合了大量的項(xiàng)目最佳實(shí)踐,讓企業(yè)級(jí)開(kāi)發(fā)變得如此簡(jiǎn)單、高效。從 3.0 開(kāi)始,框架底層基于 Koa 2.x 實(shí)現(xiàn),兼容 Koa 的所有功能。
安裝腳手架
$ npm install -g think-cli
創(chuàng)建及啟動(dòng)項(xiàng)目
$ thinkjs new demo; $ cd demo; $ npm install; $ npm start;
目錄結(jié)構(gòu)
|--- development.js //開(kāi)發(fā)環(huán)境下的入口文件 |--- nginx.conf //nginx 配置文件 |--- package.json |--- pm2.json //pm2 配置文件 |--- production.js //生產(chǎn)環(huán)境下的入口文件 |--- README.md |--- src | |--- bootstrap //啟動(dòng)自動(dòng)執(zhí)行目錄 | | |--- master.js //Master 進(jìn)程下自動(dòng)執(zhí)行 | | |--- worker.js //Worker 進(jìn)程下自動(dòng)執(zhí)行 | |--- config //配置文件目錄 | | |--- adapter.js // adapter 配置文件 | | |--- config.js // 默認(rèn)配置文件 | | |--- config.production.js //生產(chǎn)環(huán)境下的默認(rèn)配置文件,和 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: '', // 令牌,和公眾號(hào)/基本配置/服務(wù)器配置里面寫(xiě)一樣的即可
appid: '', // 這里貌似可以隨便填,因?yàn)槲覀兒竺嬉脭?shù)據(jù)庫(kù)配置多個(gè)公眾號(hào)
encodingAESKey: '',
checkSignature: false
}
}, {
handle: 'payload', // think-wechat 必須要在 payload 中間件前面加載,它會(huì)代替 payload 處理微信發(fā)過(guò)來(lái)的 post 請(qǐng)求中的數(shù)據(jù)。
options: {
keepExtensions: true,
limit: '5mb'
}
},
]
注:match下我這里寫(xiě)的是 /index ,對(duì)應(yīng)的項(xiàng)目文件是 /src/controller/index.js ,對(duì)應(yīng)的公眾號(hào)后臺(tái)所需配置的服務(wù)器地址就是 http(https)://域名:端口/index
創(chuàng)建數(shù)據(jù)庫(kù)和相關(guān)表
我這里創(chuàng)建了三個(gè)微信的相關(guān)表。
配置表:wx_config
| 字段 | 類型 | 說(shuō)明 |
|---|---|---|
| id | int | 主鍵 |
| name | varchar | 名稱 |
| appid | varchar | appid |
| secret | varchar | secret |
| 字段 | 類型 | 注釋 |
|---|---|---|
| id | int | 主鍵 |
| subscribe | int | 用戶是否訂閱該公眾號(hào)標(biāo)識(shí),值為0時(shí),代表此用戶沒(méi)有關(guān)注該公眾號(hào),拉取不到其余信息。 |
| nickname | varchar | 用戶的昵稱 |
| sex | int | 用戶的性別,值為1時(shí)是男性,值為2時(shí)是女性,值為0時(shí)是未知 |
| language | varchar | 用戶所在省份 |
| city | varchar | 用戶所在城市 |
| province | varchar | 用戶所在省份 |
| country | varchar | 用戶所在國(guó)家 |
| headimgurl | longtext | 用戶頭像,最后一個(gè)數(shù)值代表正方形頭像大?。ㄓ?、46、64、96、132數(shù)值可選,0代表640*640正方形頭像),用戶沒(méi)有頭像時(shí)該項(xiàng)為空。若用戶更換頭像,原有頭像URL將失效。 |
| subscribe_time | double | 用戶關(guān)注時(shí)間,為時(shí)間戳。如果用戶曾多次關(guān)注,則取最后關(guān)注時(shí)間 |
| unionid | varchar | 只有在用戶將公眾號(hào)綁定到微信開(kāi)放平臺(tái)帳號(hào)后,才會(huì)出現(xiàn)該字段。 |
| openid | varchar | 用戶的標(biāo)識(shí),對(duì)當(dāng)前公眾號(hào)唯一 |
| wx_config_id | int | 對(duì)應(yīng)配置的微信號(hào)id |
模版消息日志表:wx_template_log
| 字段 | 類型 | 注釋 |
|---|---|---|
| id | int | 主鍵 |
| template_id | varchar | 模版id |
| openid | varchar | 用戶的標(biāo)識(shí),對(duì)當(dāng)前公眾號(hào)唯一 |
| url | varchar | 跳轉(zhuǎn)url |
| miniprogram | varchar | 跳轉(zhuǎn)小程序 |
| data | varchar | 發(fā)送內(nèi)容json字符串 |
| add_time | double | 添加時(shí)間戳 |
| send_time | double | 發(fā)送時(shí)間戳 |
| send_status | varchar | 發(fā)送結(jié)果 |
| wx_config_id | double | 對(duì)應(yīng)配置的微信號(hào)id |
| uuid | varchar | 本次發(fā)送的uuid,業(yè)務(wù)系統(tǒng)可通過(guò)uuid查詢模版消息推送結(jié)果 |
處理微信推送消息
文件目錄
/src/controller/index.js
文件內(nèi)容
module.exports = class extends think.Controller {
/*
* 入口:驗(yàn)證開(kāi)發(fā)者服務(wù)器
* 驗(yàn)證開(kāi)發(fā)者服務(wù)器,這里只是演示,所以沒(méi)做簽名校驗(yàn),實(shí)際上應(yīng)該要根據(jù)微信要求進(jìn)行簽名校驗(yàn)
*/
async indexAction() {
let that = this;
if (that.method != 'REPLY') {
return that.json({code: 1, msg: '非法請(qǐng)求', data: null})
}
const {echostr} = that.get();
return that.end(echostr);
}
/*
* 文字
* 用于處理微信推過(guò)來(lái)的文字消息
*/
async textAction() {
let that = this;
let {id, signature, timestamp, nonce, openid} = that.get();
let {ToUserName, FromUserName, CreateTime, MsgType, Content, MsgId} = that.post();
.....
that.success('')
}
/*
* 事件
* 用于處理微信推過(guò)來(lái)的事件消息,例如點(diǎn)擊菜單等
*/
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': // 關(guān)注公眾號(hào)
...
break;
case 'unsubscribe': // 取消關(guān)注公眾號(hào)
...
break;
case 'SCAN': // 已關(guān)注掃碼
...
break;
case 'LOCATION': // 地理位置
...
break;
case 'CLICK': // 自定義菜菜單
...
break;
case 'VIEW': // 跳轉(zhuǎn)
...
break;
case 'TEMPLATESENDJOBFINISH':// 模版消息發(fā)送完畢
...
break;
}
that.success('')
}
}
注:支持的action包括: textAction 、 imageAction 、 voiceAction 、 videoAction 、 shortvideoAction 、 locationAction 、 linkAction 、 eventAction 、 deviceTextAction 、 deviceEventAction 。
公眾號(hào)后臺(tái)配置

注:后面跟的id參數(shù)是為了區(qū)分是哪個(gè)公眾號(hào)推過(guò)來(lái)的消息,在上面的接口參數(shù)中也有體現(xiàn)
微信相關(guān)API的編寫(xiě)
目錄結(jié)構(gòu)
|--- src | |--- controller //控制器目錄 | | |--- index.js // 處理微信推送的消息,上面有寫(xiě)到 | | |--- common.js // 一些公共方法 | | |--- open // 開(kāi)放給其他業(yè)務(wù)服務(wù)的api接口 | | | |--- wx.js | | |--- private // 放一些內(nèi)部調(diào)用的方法,調(diào)用微信api的方法主要在這里面 | | | |--- wx.js
這個(gè)目錄結(jié)構(gòu)可能不太合理,后期再改進(jìn)吧:grin:
公共方法
// src/controller/common.js
import 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 () => {
// 數(shù)據(jù)庫(kù)內(nèi)取
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
}
}
接口過(guò)濾器
所有開(kāi)放出來(lái)的接口的前置方法,俗稱過(guò)濾器?所有開(kāi)放的接口必傳get參數(shù)是 wxid ,對(duì)應(yīng)數(shù)據(jù)庫(kù)表wx_config里面 id
// src/controller/open/wx.js
async __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.js
async 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.js
async get_wxsdk_configAction() {
let that = this;
let {url} = that.get();
if (think.isEmpty(url)) {
return that.json({code: 1, msg: '參數(shù)不正確'})
}
let sdkConfig = await that.controller('private/wx').getSdkConfig(that.wxConfig.id, url);
return that.json({code: 0, msg: '', data: sdkConfig})
}
// src/controller/private/wx.js
const 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.js
async get_userinfoAction() {
let that = this;
let {openid} = that.get();
if (think.isEmpty(openid)) {
return that.json({code: 1, msg: '參數(shù)不正確'})
}
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.js
async getUserInfo(id, openid) {
let that = this;
let userInfo = await that.cache(`wx_userinfo:wxid_${id}:${openid}`, async () => {
//先取數(shù)據(jù)庫(kù)
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
}
//如果數(shù)據(jù)庫(kù)內(nèi)沒(méi)有,取新的存入數(shù)據(jù)庫(kù)
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) {
//命中修改,沒(méi)有命中添加
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
}
文檔
接口 - 批量發(fā)送文字客服消息
代碼
// src/controller/open/wx.js
async send_msg_textAction() {
let that = this;
let {list} = that.post();
if (think.isEmpty(list)) {
return that.json({code: 1, msg: '參數(shù)不正確'})
}
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.js
async 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;
}
文檔
寫(xiě)在結(jié)尾
其實(shí)還有很多接口,這里就不全部列出來(lái)了。
應(yīng)該能看出來(lái),在這個(gè)項(xiàng)目里面并不僅僅是把微信的接口做了個(gè)簡(jiǎn)單的轉(zhuǎn)發(fā),而是有一些自己的處理邏輯在里面。
比如獲取微信用戶信息的時(shí)候,會(huì)先判斷緩存里有沒(méi)有,如果沒(méi)有就取數(shù)據(jù)庫(kù),如果還沒(méi)有再去微信的接口取;如果數(shù)據(jù)庫(kù)有,并且關(guān)注字段是未關(guān)注的話,還是會(huì)調(diào)用微信的接口取一波再更新。 反正一天內(nèi),微信接口的調(diào)用次數(shù)是絕對(duì)夠用的。
再比如批量發(fā)送模版消息,中控服務(wù)在收到請(qǐng)求后會(huì)先創(chuàng)建一個(gè)uuid,要發(fā)的模版消息全部保存到數(shù)據(jù)庫(kù)內(nèi),直接把uuid返給調(diào)用方。 然后中控會(huì)異步用uuid取出來(lái)這批模版消息,一個(gè)一個(gè)發(fā),一個(gè)一個(gè)更新結(jié)果。 這樣在業(yè)務(wù)方調(diào)用發(fā)送模版消息之后,無(wú)需等待全部發(fā)送完畢,就可以用拿到的uuid,去中控查詢這次批量發(fā)送的狀態(tài)結(jié)果。
目前是綁了七八個(gè)公眾號(hào),在沒(méi)燒過(guò)香的前提下,還沒(méi)出過(guò)什么問(wèn)題
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
通過(guò)JS 獲取Mouse Position(鼠標(biāo)坐標(biāo))的代碼
最近我發(fā)現(xiàn)在webpage中獲取空間的絕對(duì)坐標(biāo)時(shí),如果有滾動(dòng)條就會(huì)有錯(cuò),后來(lái)用無(wú)名發(fā)現(xiàn)的方法得以解決。2009-09-09
原生js實(shí)現(xiàn)移動(dòng)端觸摸輪播的示例代碼
下面小編就為大家分享一篇原生js實(shí)現(xiàn)移動(dòng)端觸摸輪播的示例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
layui使用form表單實(shí)現(xiàn)post請(qǐng)求頁(yè)面跳轉(zhuǎn)的方法
今天小編就為大家分享一篇layui使用form表單實(shí)現(xiàn)post請(qǐng)求頁(yè)面跳轉(zhuǎn)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09
多個(gè)jquery.datatable共存,checkbox全選異常的快速解決方法
這篇文章主要介紹了多個(gè)jquery.datatable共存,checkbox全選異常的快速解決方法。需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12
微信小程序項(xiàng)目實(shí)踐之主頁(yè)tab選項(xiàng)實(shí)現(xiàn)
這篇文章主要介紹了微信小程序項(xiàng)目實(shí)踐之主頁(yè)tab選項(xiàng)實(shí)現(xiàn),本文通過(guò)實(shí)例代碼相結(jié)合的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-07-07

