本人前端渣渣一枚,這篇文章是第一次寫,若是有硬核bug,請大佬們輕噴、指出... 另外,本文不涉及任何接口安全、參數校驗之類的東西,默認對調用方無腦級的信任😂 目前自用的接口包括但不限於如下這些html
|--- 微信相關
| |--- 0. 處理微信推過來的一些消息
| |--- 1. 獲取微信SDK配置參數
| |--- 2. 微信鑑權登錄
| |--- 3. 獲取微信用戶信息
| |--- 4. 獲取AccessToken
| |--- 5. 批量發送模版消息
| |--- 6. 獲取模版消息列表
| |--- 7. 批量發送客服消息
複製代碼
ThinkJS 是一款面向將來開發的 Node.js 框架,整合了大量的項目最佳實踐,讓企業級開發變得如此簡單、高效。從 3.0 開始,框架底層基於 Koa 2.x 實現,兼容 Koa 的全部功能。node
$ 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
複製代碼
微信中間件,基於 node-webot/wechat,支持 thinkJS 3.0ios
$ npm install think-wechat --save
複製代碼
或nginx
$ cnpm install think-wechat --save
複製代碼
文件:/src/config/middleware.jsgit
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
web
我這裏建立了三個微信的相關表。數據庫
字段 | 類型 | 說明 |
---|---|---|
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、4六、6四、9六、132數值可選,0表明640*640正方形頭像),用戶沒有頭像時該項爲空。若用戶更換頭像,原有頭像URL將失效。 |
subscribe_time | double | 用戶關注時間,爲時間戳。若是用戶曾屢次關注,則取最後關注時間 |
unionid | varchar | 只有在用戶將公衆號綁定到微信開放平臺賬號後,纔會出現該字段。 |
openid | varchar | 用戶的標識,對當前公衆號惟一 |
wx_config_id | int | 對應配置的微信號id |
字段 | 類型 | 註釋 |
---|---|---|
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
。npm
|--- src
| |--- controller //控制器目錄
| | |--- index.js // 處理微信推送的消息,上面有寫到
| | |--- common.js // 一些公共方法
| | |--- open // 開放給其餘業務服務的api接口
| | | |--- wx.js
| | |--- private // 放一些內部調用的方法,調用微信api的方法主要在這裏面
| | | |--- wx.js
複製代碼
這個目錄結構可能不太合理,後期再改進吧😁json
// 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不存在'})
}
}
複製代碼
代碼
// 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}})
}
複製代碼
文檔
代碼
// 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)
}
}
複製代碼
文檔
代碼
// 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,去中控查詢此次批量發送的狀態結果。
目前是綁了七八個公衆號,在沒燒過香的前提下,還沒出過什麼問題
沒了。 哈哈哈哈😂😂😂😂。
第一次寫啊...不知道怎麼結尾。 就這麼着吧。