微信小程序在线客服自动回复功能(基于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 相关文章推荐
arguments对象
Nov 20 Javascript
javascript中自定义对象的属性方法分享
Jul 12 Javascript
理运用命名空间让js不产生冲突避免全局变量的泛滥
Jun 15 Javascript
js实现正方形颜色从下往上升的效果
Aug 04 Javascript
Node.js和MongoDB实现简单日志分析系统
Apr 25 Javascript
JavaScript常用本地对象小结
Mar 28 Javascript
Angularjs实现多个页面共享数据的方式
Mar 29 Javascript
jQuery实现鼠标悬停3d菜单展开动画效果
Jan 19 Javascript
Bootstrap框架安装使用详解
Jan 21 Javascript
JavaScript 字符串数字左补位,右补位,取固定长度,截位扩展函数代码
Mar 25 Javascript
父组件中vuex方法更新state子组件不能及时更新并渲染的完美解决方法
Apr 25 Javascript
vue将后台数据时间戳转换成日期格式
Jul 31 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
数据库中排序的对比及使用条件详解
2012/02/23 PHP
PHP使用pcntl_fork实现多进程下载图片的方法
2014/12/16 PHP
Codeigniter实现发送带附件的邮件
2015/03/19 PHP
PHP面向对象五大原则之依赖倒置原则(DIP)详解
2018/04/08 PHP
浅析PHP7 的垃圾回收机制
2019/09/06 PHP
Javascript JSQL,SQL无处不在,
2010/05/05 Javascript
面向对象Javascript核心支持代码分享
2012/05/23 Javascript
JQuery给元素添加/删除节点比如select
2013/04/02 Javascript
基于NodeJS的前后端分离的思考与实践(五)多终端适配
2014/09/26 NodeJs
JS实现的简单鼠标跟随DiV层效果完整实例
2015/10/31 Javascript
JS控制静态页面传递参数并获取参数应用
2016/08/10 Javascript
js自调用匿名函数的三种写法(推荐)
2016/08/19 Javascript
详解使用angular的HttpClient搭配rxjs
2017/09/01 Javascript
jQuery替换节点元素的操作方法
2018/03/18 jQuery
微信小程序开发的基本流程步骤
2019/01/31 Javascript
基于layui实现高级搜索(筛选)功能
2019/07/26 Javascript
JavaScript设计模式--桥梁模式引入操作实例分析
2020/05/23 Javascript
解决ant-design-vue中menu菜单无法默认展开的问题
2020/10/31 Javascript
[03:07]【DOTA2亚洲邀请赛】我们,梦开始的地方
2017/03/07 DOTA
[03:37]2016完美“圣”典 风云人物:Mikasa专访
2016/12/07 DOTA
利用Celery实现Django博客PV统计功能详解
2017/05/08 Python
浅谈Python中的私有变量
2018/02/28 Python
python中利用h5py模块读取h5文件中的主键方法
2018/06/05 Python
python扫描线填充算法详解
2020/02/19 Python
Python os模块常用方法和属性总结
2020/02/20 Python
Python基于execjs运行js过程解析
2020/11/27 Python
俄罗斯旅游网站:Tripadvisor俄罗斯
2017/03/21 全球购物
美国照明、家居装饰和家具购物网站:Bellacor
2017/09/20 全球购物
2019年分享net面试的经历和题目
2016/08/07 面试题
工作表现自我评价
2014/02/08 职场文书
培训楼经理岗位责任制
2014/02/10 职场文书
大学生自我评价范文分享
2014/02/21 职场文书
企业法人授权委托书
2014/04/03 职场文书
圣诞节活动策划方案
2014/06/09 职场文书
企业优秀团员事迹材料
2014/08/20 职场文书
授权委托书样本
2014/09/25 职场文书