搭建微信令牌中控服务器,使用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 。

公众号后台配置

ec0d15696c4b653e0d9660d6ab38034b.png

注:后面跟的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}})

}

文档

e0487309d43d0a64231dd418b88d7a82.png

接口 - 获取微信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)

}

}

文档

e0ae8f304dc507f0d2b9d04ae201063e.png

接口 - 获取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

}

文档

e258d26de295751f3e099c076a718173.png

接口 - 批量发送文字客服消息

代码

// 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;

}

文档

81c4415233c0ee1f7509b6fec50821a7.png

写在结尾

其实还有很多接口,这里就不全部列出来了。

应该能看出来,在这个项目里面并不仅仅是把微信的接口做了个简单的转发,而是有一些自己的处理逻辑在里面。

比如获取微信用户信息的时候,会先判断缓存里有没有,如果没有就取数据库,如果还没有再去微信的接口取;如果数据库有,并且关注字段是未关注的话,还是会调用微信的接口取一波再更新。 反正一天内,微信接口的调用次数是绝对够用的。

再比如批量发送模版消息,中控服务在收到请求后会先创建一个uuid,要发的模版消息全部保存到数据库内,直接把uuid返给调用方。 然后中控会异步用uuid取出来这批模版消息,一个一个发,一个一个更新结果。 这样在业务方调用发送模版消息之后,无需等待全部发送完毕,就可以用拿到的uuid,去中控查询这次批量发送的状态结果。

目前是绑了七八个公众号,在没烧过香的前提下,还没出过什么问题

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

您可能感兴趣的文章:Thinkjs3新手入门之如何使用静态资源目录

Thinkjs3新手入门之添加一个新的页面

thinkjs 文件上传功能实例代码

thinkjs之页面跳转同步异步操作

ThinkJS中如何使用MongoDB的CURD操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/504686.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为服务器sn号查询网站,linux 查询服务器sn

linux 查询服务器sn 内容精选换一换Linux云服务器变更规格时,可能会发生磁盘挂载失败的情况,因此,变更规格后,需检查磁盘挂载状态是否正常。本节操作介绍变更规格后检查磁盘挂载状态的操作步骤。以root用户登录云服务器。执行以下…

jdba访问mysql_mysql连接出现问题记录

解决:Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone valu//报错信息Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value ‘‘ is unrecognized or represents…

python制作表白神器_python制作exe可执行表白神器-Go语言中文社区

1、效果图2、程序源码import turtleimport time#writing txtturtle.hideturtle()turtle.penup()turtle.goto(130,50)# turtle.pendown()turtle.color("blue")turtle.write("亲爱的,给你画个东西",font ("Times",18,"bold"…

qtp连接mysql 无驱动_QTP连接MySQL

1、安装 Connector/ODBC2、查看数据源名称『控制面板』- 『管理工具』- 『数据源(ODBC)』-『添加』3、连接数据库Dim Conn,ConnString创建数据库实例Set ConnCreateObject("ADODB.Connection")连接字符串ConnString"Driver{Mysql ODBC 5.2w Driver};DATABASEmys…

python保存模型的路径怎么写_使用python在MongoDB中保存机器学习(ML)和深度学习(DL)模型...

我们知道,当我们训练机器学习或深入学习模型时,我们必须保存训练过的模型,以便将来进行预测。现在的训练模型非常昂贵,所以如果我们能够保存它们并将其用于解决其他一些问题。例如,一个训练过的能够识别汽车的神经网络…

java token_Java实现基于token认证的方法示例

随着互联网的不断发展,技术的迭代也非常之快。我们的用户认证也从刚开始的用户名密码转变到基于cookie的session认证,然而到了今天,这种认证已经不能满足与我们的业务需求了(分布式,微服务)。我们采用了另外一种认证方式&#xff…

java jni helloword_JNI学习一:编写HelloWorld程序

转载请说明出处~本文教程翻译jni官方文档的部分内容。要查看Jni官方文档,请点击这里先感叹一下时光如水,岁月不留人哇有木有!!!认真想想在XMU的这三年,真的改变了我好多。我还清楚地记得学习C语言写的第一个…

【学习记录】macOS的Redis安装及基本使用

【学习记录】macOS的Redis安装及基本使用一. Redis的安装与启动二. 简单使用① 尝试插入第一个key-value② Redis的数据类型与基本使用字符串列表字典(哈希表)集合有序集合三. 杂乱无章的笔记一. Redis的安装与启动 打开终端,输入以下命令即…

【学习笔记】JAVA基础——异常处理部分

文章目录前言简介一. try、catch与finally① try && catch② finally③ throws补充:JVM 相关二. 异常的分类① 分类解释与思维导图三. 自定义异常例子:Hero类的attack方法的isDeadException。四. 上传代码到GIT① 首先在github新建一个仓库Java_…

【学习笔记】数据链路层的差错控制——检错编码与纠错编码(海明码、奇偶检验码与CRC循环冗余码)

文章目录前言一. 差错控制简介二.补充知识三. 检错编码(1)奇偶检验码组成:构造方法:以奇检验码为例。举个例子:检验码求法:错误检测方法:特点(2)CRC循环冗余检验码三要素…

【学习笔记】数据链路层——流量控制:停止等待协议、后退N帧协议(GBN)、选择重传协议(SR)

文章目录一. 流量控制① 必要性② 数据链路层 VS 传输层③ 定义④ 方法1)停止等待协议2)滑动窗口协议关系:包括:3)协议对比二. 停止-等待协议必要性应用情况① 无差错情况② 有差错情况1)数据帧丢失&#x…

java线程唤醒与等待_Java线程的等待与唤醒

生产者和消费者必须使用同步代码块包裹起来,保证等待和唤醒只能有一个执行,同步使用的锁对象必须保证唯一Thread中重要方法void wait() 在其他线程调用此对象的notify()方法或notifyall()方法前,导致当前线程等待void notify() 唤醒在此对象监…

【学习笔记】数据链路层——信道划分访问控制(FDM、TDM、STDM、WDM、CDM CDMA)

PPT截自王道考研B站教程 一. 铺垫知识 ① 传输数据使用的两种链路 星型、总线型都是广播式结构。 星型更有容错率,总线型断一个则全断。 ② 介质访问控制 定义 采取一定措施,使得两对节点之间的通信不会发生互相干扰的情况。 分类 多路复用&…

【学习笔记】数据链路层——随机访问介质访问控制(ALOHA、CSMA、CSMA/CD、CSMA/CA),截断二进制指数规避算法

文章目录小前言一. ALOHA协议纯ALOHA协议时隙ALOHA协议ALOHA对比CSMA协议定义与分类① 1-坚持CSMA② 非坚持CSMA③ p-坚持CSMA总结CSMA/CD协议传播时延对载波监听的影响确定重传时机:截断二进制指数规避算法最小帧长问题CSMA/CA协议工作原理CSMA/CD 与 CSMA/CA的对比…

pca算法介绍及java实现_PCA算法原理及实现

众所周知,PCA(principal component analysis)是一种数据降维的方式,能够有效的将高维数据转换为低维数据,进而降低模型训练所需要的计算资源。以上是比较官方的说法,下面是人话(正常人讲的话)版。pca就是一种能够有效压缩数据的方…

【学习笔记】数据链路层——轮询访问介质控制(轮询协议、令牌传递协议)

文章目录一. 轮询访问介质控制二. 轮询协议三. 令牌传递协议结束语PPT截自王道考研B站教程 一. 轮询访问介质控制 结合了前面的信道划分访问控制、随机访问MAC协议的优点: 既要不产生冲突,又要发送时占全部带宽。 二. 轮询协议 轮询开销:…

【学习笔记】局域网基本概念和体系结构,以太网、无线局域网与PPP协议、HDLC协议

文章目录一. 局域网:特点与要素① 拓扑结构② 传输介质③ 介质访问控制方法④ 局域网的分类⑤ IEEE 802标准⑥ MAC子层和LLC子层二. 以太网① 概述② 提供无连接、不可靠的服务③ 传输介质与拓扑结构的发展④ 10BAST-T以太网⑤ 适配器与MAC地址⑥ 以太网MAC帧⑦ 高速…

【学习笔记】数据链路层——链路层设备:物理层拓展以太网、链路层拓展以太网与冲突域和广播域

文章目录一. 冲突域与广播域① 定义与对比图② 例子二. 物理层扩展以太网三. 链路层扩展以太网① 网桥定义透明网桥源路由网桥② 以太网交换机PPT截自B站王道考研教程 本文内容导图 一. 冲突域与广播域 可以先只是简单看看定义,然后看完二、三后再回来看对比图和…

【学习笔记】网络层——概述、数据交换方式:电路交换、报文交换与分组交换(数据报与虚电路)

文章目录一. 概述二. 数据交换方式① 电路交换② 报文交换③ 分组交换④ 分组交换 && 报文交换举例对比⑤ 三种数据交换方式比较总结三. 分组交换的两种方式① 定义传输单元名词辨析② 数据报③ 虚电路④ 数据报与虚电路的对比ppt截自王道考研B站教程 太不容易了&#…

【学习记录】网络层——IP数据报(格式与分片)

文章目录一. IP数据报格式二. IP数据报分片① 为什么要分片?② 标识、标志与片偏移③ 例题单位为nB小结PPT截自王道考研教程 tips:b是位,B是字节。 一. IP数据报格式 在本章节中,暂时不区分IP数据报与分组。 生存时间(Time To Live)&#xf…