使用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包括: textActionimageActionvoiceActionvideoActionshortvideoActionlocationActionlinkActioneventActiondeviceTextActiondeviceEventAction

公众号后台配置

注:后面跟的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,去中控查询这次批量发送的状态结果。

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

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

(0)

相关推荐

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

    对于刚入手thinkjs项目的新手来说,时常会犯的一个错误就是"混用"各种代码逻辑,比如:我们经常在做后台管理系统的时候用到的登录框, 其实它原本是有一个路由专门存放自己的代码逻辑,而在点击提交按钮的时候,要达到的效果便是账号密码正确的时候,正常跳转页面,而错误的时候给出提示:为了发现问题,就先把源代码贴出来吧: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <h

  • ThinkJS中如何使用MongoDB的CURD操作

    前言 众所周知目前使用Node.js + mongodb已经成为很多公司的技术栈.ThinkJS其实也提供了对mongo的支持,虽然官方文档较少,但是保证了ORM的API的一致性,所以用起来需要查看基本的>Model api 基本的模型文件放在common/model下 获取列表 getList(q, page) { return this.select(); } 分页加条件搜索 search(q, page) { if(q) { q = new RegExp(q,'i'); } return

  • Thinkjs3新手入门之如何使用静态资源目录

    静态资源访问 项目开发时,一般都需要在模版里引用静态资源. 使用 thinkjs 命令创建项目时,会自动创建 www/static 目录,该目录下专门用来存放 JS.CSS.图片等静态资源. 0x0 听说new的项目自动就有www/static!? 传说默认创建的项目结构如下: |--- development.js //开发环境下的入口文件 |--- nginx.conf //nginx 配置文件 |--- package.json |--- pm2.json //pm2 配置文件 |---

  • thinkjs 文件上传功能实例代码

    介绍 ThinkJS 是一款面向未来开发的 Node.js 框架,整合了大量的项目最佳实践,让企业级开发变得如此简单.高效.从 3.0 开始,框架底层基于 Koa 2.x 实现,兼容 Koa 的所有功能. 特性 基于 Koa 2.x,兼容 middleware 内核小巧,支持 Extend.Adapter 等插件方式 性能优异,单元测试覆盖程度高 内置自动编译.自动更新机制,方便快速开发 使用更优雅的 async/await 处理异步问题,不再支持 */yield 从 3.2 开始支持 Type

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

    前言 ThinkJS 是一款拥抱未来的 Node.js Web 框架,致力于集成项目最佳实践,规范项目让企业级团队开发变得更加简单,更加高效. 目前该版本已经在线上多个项目中使用,简单一个命令就可以让你下载使用它: npm install -g think-cli 本文将帮助大家往Thinkjs3中添加一个新的页面,没有原理描述,只提供给入门者一个感性认识. 0x0 首先需要一个Thinkjs3项目 我们创建一个叫做1uck的项目 $ thinkjs new 1uck $ cd ./1uck $

  • 使用ThinkJs搭建微信中控服务的实现方法

    本人前端渣渣一枚,这篇文章是第一次写,如果有硬核bug,请大佬们轻喷.指出... 另外,本文不涉及任何接口安全.参数校验之类的东西,默认对调用方无脑级的信任:joy: 目前自用的接口包括但不限于以下这些 |--- 微信相关 | |--- 0. 处理微信推过来的一些消息 | |--- 1. 获取微信SDK配置参数 | |--- 2. 微信鉴权登陆 | |--- 3. 获取微信用户信息 | |--- 4. 获取AccessToken | |--- 5. 批量发送模版消息 | |--- 6. 获取模版

  • thinkjs微信中控之微信鉴权登陆的实现代码

    前言 上一篇文章大概写了一下如何搭一个微信中控服务: [thinkjs搭建微信中控服务] . 接下来这篇,专门写一下如何在此基础上扩展出来一个比较好用的微信鉴权登陆的方案. 由于这一段的逻辑着实有点绕,所以就单独拿出来写了. 有时候,调用方甚至可以通过这个方案,进行多公众号openid的之间的关联. 官方说明 开发文档 微信文档地址:传送门 鉴权逻辑 前端跳转到以下url,重定向或者代码跳转都可以:https://open.weixin.qq.com/connect/oauth2/authori

  • 一个小时快速搭建微信小程序的方法步骤

    「小程序」这个划时代的产品发布快一周了,互联网技术人都在摩拳擦掌,跃跃欲试.可是小程序目前还在内测,首批只发放了 200 个内测资格(泪流满面).本以为没有 AppID 这个月就与小程序无缘了,庆幸的是微信这两天发布了正式版开发者工具,无需内测邀请也可以尝鲜了. 因此也就有了我与「小程序」的初体验,而我的感受只有一个字--爽! 选择哪个「小程序」Demo? 在知名同性交友网站 Github 上,「小程序」的 Demo 不少,但是大多只是简单的 API 演示,有的甚至直接把页面数据写在了 json

  • 半小时实现基于ChatGPT搭建微信机器人

    目录 ChatGPT刷屏了 流程 注册 运行机器人程序 思考 ChatGPT刷屏了 相信大家最近被 ChatGPT 刷屏了,其实在差不多一个月前就火过一次,不会那会好像只在程序员的圈子里面火起来了,并没有被大众认知到,不知道最近是因为什么又火起来了,而且这次搞的人尽皆知. 想着这么火的 AI 完全可以好好玩一玩呀,于是就尝试着将 ChatGPT 接入到了个人微信中,实现在微信中调戏 AI. 先看几个聊天截图 还可以拉入到一个群,进行群聊,可以看到 ChatGPT 的训练数据相当还是比较旧的,有些

  • 使用Node.js搭建静态资源服务详细教程

    对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如果有使用express框架,用express.static一行代码就可以达到目的了: app.use(express.static('public')) 这里我们要实现的正是express.static背后所做工作的一部分,建议同步阅读该模块源码. 基本功能 不急着写下第一行代码,而是先梳理一下就基

  • PHP:微信小程序 微信支付服务端集成实例详解及源码下载

    微信小程序 微信支付服务端集 理论上集成微信支付的全部工作可以在小程序端完成,因为小程序js有访问网络的能力,但是为了安全,不暴露敏感key,而且可以使用官方提供的现成php demo更省力,于是在服务端完成签名与发起请求,小程序端只做一个wx.requestPayment(OBJECT)接口的对接. 整体集成过程与JSAPI.APP类似,先统一下单,然后拿返回的结果来请求支付. 一共三步: 1.小程序端通过wx.login的返回的code换取openid 2.服务端向微信统一下单 3.小程序端

  • 本地搭建微信小程序服务器的实现方法

    本地搭建微信小程序服务器的实现方法 现在开发需要购买服务器,价格还是有点贵的,可以花费小代价就可以搭建一个服务器,可以用来开发小程序,博客等. 1.域名(备案过的)  2.阿里云注册免费的https证书  3.配置本地的nginx  4.内网映射(本地安装wampserver 服务器) 一.域名 注册花生壳,开通内网映射需要8元(我开通时需要,现在不清楚还要不要),里面可以注册2个免费的域名,都是免备案的.具体的请自行百度,花生壳注册地址 二.申请阿里云免费的https证书 阿里云免费的http

  • 轻松实现python搭建微信公众平台

    本文主要是一步一步教大家如何利用python搭建微信公众平台,有兴趣的朋友可以参考一下 使用的工具,python 新浪SAE平台,微信的公众平台 你需要先在微信的公众平台与新浪SAE平台上各种注册,微信平台注册的时候需要你拍张手持身份证的照片,还有几天的审核期 微信公众平台:http://mp.weixin.qq.com 新浪SAE:http://sae.sina.com.cn/ 等待微信公众审核通过后,登录公众平台后,点击高级功能.将会看到需要提供一个接入信息: 微信接口配置 那么我们需要一个

  • CentOS搭建FTP文件服务的步骤

    基于 CentOS 搭建 FTP 文件服务,供大家参考,具体内容如下 系统要求:CentOS 7.2 64 位操作系统 一.安装VSFTPD (vsftpd 是在 Linux 上被广泛使用的 FTP 服务器,根据其[官网介绍][https://security.appspot.com/vsftpd.html],它可能是 UNIX-like 系统下最安全和快速的 FTP 服务器软件.) yum install vsftpd -y 启动 VSFTPD(安装完成后,启动 FTP 服务) service

  • CentOS7搭建gerrit 代码审查服务方法

    步骤一:创建专用账户和工作目录 adduser gerrit:passwd gerrit:创建gerrit专用工作目录:mkdir /home/gerrit 步骤二:配置Java环境 1) 从官网下载JDK:http://download.oracle.com/otn-pub/java/jdk-nb/8u161-8.2/jdk-8u161-nb-8_2-linux-x64.sh 2) 添加执行权限chmod a+x jdk-8u161-nb-8_2-linux-x64.sh,然后运行该脚本安装

随机推荐