微信小程序在线客服自动回复功能(基于node)


Posted in Javascript onJuly 03, 2019

前言

我们知道H5页面经常需要将用户导流到APP,通过下载安装包或者跳转至应用宝市场/Appstore等方式进行导流。但是由于小程序嵌套webview时需要校验域名,因此跳转到第三方应用市场和Appstroe无法实现导流。那怎么办呢?

只能说道高一尺魔高一丈,看看微博小程序是怎么导流的:

微信小程序在线客服自动回复功能(基于node)

曲线救国的方式,利用小程序的在线功能可以打开H5的方式,去进行下载引导。

于是,就引出了这次文档的主题,小程序在线客服自动回复功能。

阅读本文档之前,最好已经了解过小程序客服信息官方的相关文档:

  • 客服消息使用指南
  • 小程序客服消息服务端接口
  • 客服消息开发文档

这次开发做在线客服功能也踩了不少坑,网上也查阅不少资料,但大部分的后台都是基于php或者python,java开发,node.js开发的较少,因此将这次开发的流程记录一下,供大家参考,避免大家踩坑。可能会有一些错误地方欢迎指正交流。

另外,我们用的node框架是基于koa自行封装的,在一些细节实现上和其他框架会有区别,不必纠结。

需求描述

小程序中点按钮跳转在线客服界面,根据关键词自动回复

客服回复判断条件,支持cms配置key,及 respond

respond 支持配置以下类型,及回复内容:

type 内容
text text=文本回复内容
link title=标题 description=描述 url=跳转链接 thumb_url=图片地址
image imageurl=图片地址
  • 配置后用户需要精准匹配回复条件才可收到自动回复
  • 可支持配置多个key,及对应respond
  • 除了配置的key以外的回复,可配置默认的自动回复

开发流程

写个跳转客服的按钮吧

index.wxml

<button open-type="contact">转在线客服</button>

后台配置

登录小程序后台后,在「开发」-「开发设置」-「消息推送」中,管理员扫码启用消息服务,填写服务器地址(URL)、令牌(Token) 和 消息加密密钥(EncodingAESKey)等信息。

微信小程序在线客服自动回复功能(基于node)

1.URL服务器地址

URL: 开发者用来接收微信消息和事件的接口 URL。开发者所填写的URL 必须以 http:// 或 https:// 开头,分别支持 80 端口和 443 端口。

务必要记住,服务器地址必须是线上地址,因为需要微信服务器去访问。localhost,IP,内网地址都不行的。

不然会提示 '解析失败,请检查信息是否填写正确'。

那么问题来了,不同的公司都有一套上线流程,总不能为了调试URL是否可用要上到线上去测试,成本太大,也不方便。
这就要引出内网穿透了,简单来说就是配置一个线上域名,但是这个域名可以穿透到你配置的本地开发地址上,这样可以方便你去调试看日志。

推荐一个可以实现内网穿透的工具。(非广告)

NATAPP 具体不详细介绍,免得广告嫌疑。

简单说,NATAPP有免费和付费两种模式,免费的是域名不定时更换,对于微信的推送消息配置一个月只有3次更改机会来说,有点奢侈。不定什么时候配置的域名就不能访问,得重新配置。而付费的则是固定域名,映射的内网地址也可以随时更改。楼主从免费切到付费模式,一个月的VIP使用大概十几块钱吧。

微信小程序在线客服自动回复功能(基于node)

2.Token

Token自己随便写就行了,但是要记住它,因为你在接口中要用的。

3.EncodingAESKey

随机生成即可。

4.加密方式和数据格式

根据自己喜欢选择,楼主选择的安全模式和JSON格式。
不同的模式和数据格式,在开发上会有不同,自己衡量。
既然这些配置都清楚,那开始码代码。

验证消息的确来自微信服务器

配置提交前,需要把验证消息来自微信服务器的接口写好。

server.js

/*
   * https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
   * 验证消息的确来自微信服务器
   * 开发者通过检验 signature 对请求进行校验(下面有校验方式)。
   * 若确认此次 GET 请求来自微信服务器,请原样返回 echostr 参数内容,
   * 则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
   * 将token、timestamp、nonce三个参数进行字典序排序
   * 将三个参数字符串拼接成一个字符串进行sha1加密
   * 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
   */
   const crypto = require('crypto');
   async wxCallbackAction(){
    const ctx = this.ctx;
    const method = ctx.method;
    //微信服务器签名验证,确认请求来自微信
    if(method === 'GET') {
      // 1.获取微信服务器Get请求的参数 signature、timestamp、nonce、echostr
      const {
        signature,
        timestamp,
        nonce,
        echostr
      } = ctx.query;
      
      // 2.将token、timestamp、nonce三个参数进行字典序排序
      let array = ['yourToken', timestamp, nonce];
      array.sort();
      
      // 3.将三个参数字符串拼接成一个字符串进行sha1加密
      const tempStr = array.join('');
      const hashCode = crypto.createHash('sha1'); //创建加密类型
      const resultCode = hashCode.update(tempStr, 'utf8').digest('hex');
      
      // 4.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
      if (resultCode === signature) {
        console.log('验证成功,消息是从微信服务器转发过来');
        return this.json(echostr);
      }else {
        console.log('验证失败!!!');
        return this.json({
          status: -1,
          message: "验证失败"
        });
      }
      
    }
   }

验证接口开发完毕,后台配置可以去点提交了。配置成功会提示如下:

微信小程序在线客服自动回复功能(基于node)

接收消息和推送消息

当用户在客服会话发送消息、或由某些特定的用户操作引发事件推送时,微信服务器会将消息或事件的数据包发送到开发者填写的 URL。开发者收到请求后可以使用 发送客服消息 接口进行异步回复。

本文以接收文本消息为例开发:

server.js

const WXDecryptContact = require('./WXDecryptContact');
  async wxCallbackAction(){
    const ctx = this.ctx;
    const method = ctx.method;
    //接收信息时 为POST请求;(完整代码自行与上面验证时的合并即可)
    if(method === 'POST'){
      const { Encrypt } = ctx.request.body;
      //配置时选的安全模式 因此需要解密
      if(!Encrypt){
        return this.json('success');
      }
      const decryptData = WXDecryptContact(Encrypt);
      await this._handleWxMsg(decryptData);
      return this.json('success');
    }else{
      return this.json('success');
    }
  }
  
  //处理微信回调消息的总入口 (只处理了文本类型,其他类型自行添加)
  async _handleWxMsg(msgJson){
    if(!msgJson){
      return this.json('success');
    }

    const { MsgType } = msgJson;
    if(MsgType === 'text'){
      await this._sendTextMessage(msgJson);
    }
  }
  
  //微信文本信息关键字自动回复
  async _sendTextMessage(msgJson){
    //获取CMS客服关键词回复配置
    const result = await this.callService('cms.getDataByName', 'wxApplet.contact');
    
    let keyWordObj = result.data || {};
  
    //默认回复default
    let options = keyWordObj.default;
    for(let key in keyWordObj){
      //查看是否命中配置的关键词
      if(msgJson.Content === key){
        //CMS配置项
        options = keyWordObj[key];
        }
      }
    }
    
    //获取access_token
    const accessToken = await this._getAccessToken();
    
    /*
    * 先判断配置回复的消息类型是不是image类型
    * 如果是 则需要先通过 新增素材接口 上传图片文件获得 media_id
    */
    
    let media_id = '';
    if(options.type === 'image'){
      //获取图片地址(相对路径)
      let url = options.url;
      const file = fs.createReadStream(url);
      
      //调用微信 uploadTempMedia接口 具体实现见 service.js
      const mediaResult = await this.callService('wxApplet.uploadTempMedia',
        {
          access_token: accessToken,
          type: 'image'
        },
        {
          media: file
        }
      );
      
      if(mediaResult.status === 0){
        media_id = mediaResult.data.media_id;
      }else {
        //如果图片id获取失败 则按默认处理
        options = keyWordObj.default;
      }
    }
    
    //回复信息给用户
    const sendMsgResult = await this.callService('wxApplet.sendMessageToCustomer',
      {
        access_token: accessToken,
        touser: msgJson.FromUserName,
        msgtype: options.type || 'text',
        text: {
          content: options.description || '',
        },
        link: options.type === "link" ? 
          {
            title: options.title,
            description: options.description,
            url: options.url,
            thumb_url: options.thumb_url
          }
          :
          {},
        image: {
          media_id
        }
      }
    );    
  }

service.js

const request = require('request');
/*
* 获取CMS客服关键词回复配置
* 这个接口只是为了回去CMS配置的字段回复关键字配置 返回的data数据结构如下
*/
async contact(){
  return {
    data: {
      "1": {
        "type": "link",
        "title": "点击下载[****]APP",
        "description": "注册领取领***元注册红包礼",
        "url": "https://m.renrendai.com/mo/***.html",
        "thumb_url": "https://m.we.com/***/test.png"
       },
       "2": {
        "url": "http://m.renrendai.com/cms/****/test.jpg",
        "type": "image"
       },
       "3": {
        "url": "/cms/***/test02.png",
        "type": "image"
       },
       "default": {
        "type": "text",
        "description": "再见"
       }
    }
  }
}

/*
 * 把媒体文件上传到微信服务器。目前仅支持图片。用于发送客服消息或被动回复用户消息。
 * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.uploadTempMedia.html
 */
 
 async uploadTempMedia(data,formData){
  const url = `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${data.access_token}&type=${data.type}`;
  return new Promise((resolve, reject) => {
    request.post({url, formData: formData}, (err, response, body) => {
      try{
        const out = JSON.parse(body);
        let result = {
          data: out,
          status: 0,
          message: "ok"
        }
        
        return resolve(result);
      
      }catch(err){
        return reject({
          status: -1,
          message: err.message
        });
      }
    });
  }
 }
 
 /*
 * 发送客服消息给用户
 * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-message/customerServiceMessage.send.html
 */
 
 async sendMessageToCustomer(data){
  const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${data.access_token}`;
  return new Promise((resolve, reject) => {
    request.post({url, data}, (err, response, body) => {
      ...
    });
  }
 }

WXDecryptContact.js

消息加密解密文档

const crypto = require('crypto'); // 加密模块

const decodePKCS7 = function (buff) {
  let pad = buff[buff.length - 1];
  if (pad < 1 || pad > 32) {
    pad = 0;
  }
  return buff.slice(0, buff.length - pad);
};

// 微信转发客服消息解密
const decryptContact = (key, iv, crypted) => {
  const aesCipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
  aesCipher.setAutoPadding(false);
  let decipheredBuff = Buffer.concat([aesCipher.update(crypted, 'base64'), aesCipher.final()]);
  decipheredBuff = decodePKCS7(decipheredBuff);
  const lenNetOrderCorpid = decipheredBuff.slice(16);
  const msgLen = lenNetOrderCorpid.slice(0, 4).readUInt32BE(0);
  const result = lenNetOrderCorpid.slice(4, msgLen + 4).toString();
  return result;
};

// 解密微信返回给配置的消息服务器的信息
const decryptWXContact = (wechatData) => {
  if(!wechatData){
    wechatData = '';
  }
  //EncodingAESKey 为后台配置时随机生成的
  const key = Buffer.from(EncodingAESKey + '=', 'base64');
  const iv = key.slice(0, 16);
  const result = decryptContact(key, iv, wechatData);
  const decryptedResult = JSON.parse(result);
  console.log(decryptedResult);
  return decryptedResult;
};
module.exports = decryptWXContact;

呼~ 代码终于码完,来看看效果:

微信小程序在线客服自动回复功能(基于node)

总结

开发并不是一帆风顺的,也遇到了一些值得留意的坑,强调一下:

  • 后台配置URL地址一定外网可访问(可以通过内网穿透解决)
  • 文件上传接口uploadTempMedia media参数要用 FormData数据格式 (用node的request库很容易实现。urllib这个库有坑有坑 都是泪T_T)
  • 切记接收消息不论成功失败都要返回success,不然即使成功接收返回消息,日志没有报错的情况下,还是出现IOS提示该小程序提供的服务出现故障 请稍后再试。

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

Javascript 相关文章推荐
JQuery对表格进行操作的常用技巧总结
Apr 23 Javascript
JavaScript的Polymer框架中dom-repeat与VM的相关操作
Jul 29 Javascript
概述jQuery的元素筛选
Nov 23 Javascript
EasyUI学习之DataGird分页显示数据
Dec 29 Javascript
vue router路由嵌套不显示问题的解决方法
Jun 17 Javascript
Easyui使用Dialog行内按钮布局的实例
Jul 27 Javascript
详解Vuejs2.0 如何利用proxyTable实现跨域请求
Aug 03 Javascript
微信小程序调用微信支付接口的实现方法
Apr 29 Javascript
通过jQuery学习js类型判断的技巧
May 27 jQuery
使用JavaScrip模拟实现仿京东搜索框功能
Oct 16 Javascript
Vue中jsx不完全应用指南小结
Nov 01 Javascript
原生js中运算符及流程控制示例详解
Jan 05 Javascript
解决vue打包后vendor.js文件过大问题
Jul 03 #Javascript
Vue.js递归组件实现组织架构树和选人功能案例分析
Jul 03 #Javascript
vuex 动态注册方法 registerModule的实现
Jul 03 #Javascript
微信小程序中如何使用flyio封装网络请求
Jul 03 #Javascript
JQuery+Bootstrap 自定义全屏Loading插件的示例demo
Jul 03 #jQuery
使用vue实现各类弹出框组件
Jul 03 #Javascript
elementUI vue this.$confirm 和el-dialog 弹出框 移动 示例demo
Jul 03 #Javascript
You might like
《星际争霸重制版》兵种对比图鉴
2020/03/02 星际争霸
如何实现给定日期的若干天以后的日期
2006/10/09 PHP
Php中用PDO查询Mysql来避免SQL注入风险的方法
2013/04/25 PHP
探讨PHP使用eAccelerator的API开发详解
2013/06/09 PHP
ThinkPHP的模版中调用session数据的方法
2014/07/01 PHP
php set_include_path函数设置 include_path 配置选项
2016/10/30 PHP
php类自动装载、链式操作、魔术方法实现代码
2017/07/23 PHP
thinkPHP5框架中widget的功能与用法详解
2018/06/11 PHP
Javascript Global对象
2009/08/13 Javascript
8个超棒的学习 jQuery 的网站 推荐收藏
2011/04/02 Javascript
Jquery 的扩展方法总结
2011/10/01 Javascript
js在数组中删除重复的元素自保留一个(两种实现思路)
2014/08/22 Javascript
Javascript实现的简单右键菜单类
2015/09/23 Javascript
一道JS前端闭包面试题解析
2015/12/25 Javascript
JavaScript的Backbone.js框架入门学习指引
2016/05/07 Javascript
基于jQuery实现Accordion手风琴自定义插件
2020/10/13 Javascript
babel之配置文件.babelrc入门详解
2018/02/22 Javascript
微信小程序实现团购或秒杀批量倒计时
2020/11/01 Javascript
Vue搭建后台系统需要注意的问题
2019/11/08 Javascript
在Mac OS上部署Nginx和FastCGI以及Flask框架的教程
2015/05/02 Python
Python判断文本中消息重复次数的方法
2016/04/27 Python
python书籍信息爬虫实例
2018/03/19 Python
python使用Plotly绘图工具绘制气泡图
2019/04/01 Python
一个可以套路别人的python小程序实例代码
2019/04/09 Python
基于TensorFlow中自定义梯度的2种方式
2020/02/04 Python
python 发送邮件的四种方法汇总
2020/12/02 Python
MIS软件工程师的面试题
2016/04/22 面试题
What is EJB
2016/07/22 面试题
介绍一下如何优化MySql
2016/12/20 面试题
《罗布泊,消逝的仙湖》教学反思
2014/03/01 职场文书
文化活动实施方案
2014/03/28 职场文书
2014年大学生就业规划书
2014/04/04 职场文书
离职报告格式
2014/11/04 职场文书
银行优秀员工推荐信
2015/03/24 职场文书
幼儿体育课教学反思
2016/02/16 职场文书
《角的度量》教学反思
2016/02/18 职场文书