搭建微信令牌中控服务器,使用ThinkJs搭建微信中控服务的实现方法
本人前端渣渣一枚,這篇文章是第一次寫,如果有硬核bug,請大佬們輕噴、指出... 另外,本文不涉及任何接口安全、參數校驗之類的東西,默認對調用方無腦級的信任:joy: 目前自用的接口包括但不限于以下這些
|--- 微信相關
| |--- 0. 處理微信推過來的一些消息
| |--- 1. 獲取微信SDK配置參數
| |--- 2. 微信鑒權登陸
| |--- 3. 獲取微信用戶信息
| |--- 4. 獲取AccessToken
| |--- 5. 批量發送模版消息
| |--- 6. 獲取模版消息列表
| |--- 7. 批量發送客服消息
背景
【需求】小項目很多很雜,而且大部分需求都是基于微信開發的,每次都查微信文檔的話就會很郁悶:unamused:...
【號多】公眾號超級多,項目中偶爾會涉及借權獲取用戶信息(在不綁定微信開放平臺的前提下,需要臨時自建各個公眾號的openid關聯關系),類似這樣同時需要不止一個公眾號配合來完成一件事的需求,就容易把人整懵逼...
【支付】微信支付的商戶號也很多,而且有時候支付需要用的商戶號,還不能用關聯的公眾號取出來的openid去支付...
【官方】微信官方文檔建議!把獲取AccessToken等微信API抽離成單獨的服務... 等等等等........所以...:joy:
創建ThinkJS項目
官網
thinkjs.org/
簡介
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
用戶表:wx_userinfo
字段
類型
注釋
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.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 () => {
// 數據庫內取
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.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: '參數不正確'})
}
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: '參數不正確'})
}
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 () => {
//先取數據庫
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.js
async 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.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;
}
文檔
寫在結尾
其實還有很多接口,這里就不全部列出來了。
應該能看出來,在這個項目里面并不僅僅是把微信的接口做了個簡單的轉發,而是有一些自己的處理邏輯在里面。
比如獲取微信用戶信息的時候,會先判斷緩存里有沒有,如果沒有就取數據庫,如果還沒有再去微信的接口取;如果數據庫有,并且關注字段是未關注的話,還是會調用微信的接口取一波再更新。 反正一天內,微信接口的調用次數是絕對夠用的。
再比如批量發送模版消息,中控服務在收到請求后會先創建一個uuid,要發的模版消息全部保存到數據庫內,直接把uuid返給調用方。 然后中控會異步用uuid取出來這批模版消息,一個一個發,一個一個更新結果。 這樣在業務方調用發送模版消息之后,無需等待全部發送完畢,就可以用拿到的uuid,去中控查詢這次批量發送的狀態結果。
目前是綁了七八個公眾號,在沒燒過香的前提下,還沒出過什么問題
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:Thinkjs3新手入門之如何使用靜態資源目錄
Thinkjs3新手入門之添加一個新的頁面
thinkjs 文件上傳功能實例代碼
thinkjs之頁面跳轉同步異步操作
ThinkJS中如何使用MongoDB的CURD操作
總結
以上是生活随笔為你收集整理的搭建微信令牌中控服务器,使用ThinkJs搭建微信中控服务的实现方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: idea 保存设置 新建项目_配置、创建
- 下一篇: 用户表如何区分普通用户和管理员_Gate