前言

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

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

曲线救国的方式,利用小程序的在线功能可以打开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)等信息。

1.URL服务器地址

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

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

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

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

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

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

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

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: "验证失败"
        });
      }
      
    }
   }

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

接收消息和推送消息

当用户在客服会话发送消息、或由某些特定的用户操作引发事件推送时,微信服务器会将消息或事件的数据包发送到开发者填写的 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;

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

总结

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

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

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

微信小程序在线客服自动回复功能(基于node)的更多相关文章

  1. 利用Node实现HTML5离线存储的方法

    这篇文章主要介绍了利用Node实现HTML5离线存储的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. 微信小程序“圣诞帽”的实现思路详解

    这两天朋友圈被圣诞帽刷屏,下面通过本文给大家分享微信小程序“圣诞帽”的实现思路详解,需要的朋友参考下吧

  3. ios – 使用带有NodeJs HTTPS的certificates.cer

    我为IOS推送通知生成了一个.cer文件,我希望将它与NodeJSHTTPS模块一起使用.我发现HTTPS模块的唯一例子是使用.pem和.sfx文件,而不是.cer:有解决方案吗解决方法.cer文件可以使用两种不同的格式进行编码:PEM和DER.如果您的文件使用PEM格式编码,您可以像使用任何其他.pem文件一样使用它(有关详细信息,请参见Node.jsdocumentation):如果您的文件使

  4. 如何在XCode IDE中构建NodeJS?

    如何在XCodeIDE中将NodeJS构建为项目?NodeJS构建指令说它应该用以下内容构建:但是我希望在XCodeIDE中构建.我真正想要做的是在我的应用程序中嵌入NodeJS,所以我想如果我可以在XCode中构建NodeJS,那么我可以调整它以在我建立和运行NodeJS后添加我的应用程序.我想通过让V8在XCode中编译来取得一些进展,现在我正在尝试将NodeJS添加到V8项目中.解决方法在节点存储库根目录中运行./configure–xcode,您将获得所需的node.xcodeproj文件.

  5. 深入云存储系统Swift核心组件:Ring实现原理剖析

    它的目的是用于托管Rackspace的CloudFilesservice,原始项目代号是swift,所以沿用至今。Ring是Swift中最重要的组件,用于记录存储对象与物理位置间映射关系。先来看一下Swift文档中关于Ring的描述:Ring用来确定数据驻留在集群中的位置。有单独对应于Account数据库、container数据库和单个object的ring。Ring使用zone的概念来保证数据的隔离。每个partition的replica都确保放在了不同的zone中。本文逐步深入探讨了Swift如何通过

  6. Swift - 自定义UIActivity分享

    UIActivity可以十分方便地将文字、图片等内容进行分享,比如分享到微信、微博、发送邮件、短信等等。下面通过继承UIActivity实现定制按钮和功能,通过调用UIActivityViewController来展示分享框。,NSURL]//新建自定义的分享对象数组acts=[WeiXinActivity(),147)!important;font-family:Consolas,UIActivityTypePrintUIActivityTypeAssignToContactUIActivityType

  7. Swift开发:创建XML文件,包含节点,属性值

    .append;//3创建第二个节点数据letitem2:Item=Item;for{letnode=Node;node.id=i+1;node.attributes=["ID":"\","Name":"N-\","disp":"1","Appliance":"1","Icon":"ic_switch_4"]item2.addNode;}xml.items?

  8. 泛型 – 符合Swift中Comparable的泛型类

    我正在尝试创建一个符合Comparable协议的简单通用节点类,以便我可以轻松地比较节点而无需访问其密钥.当我试图写

  9. swift3 – 将SceneKit对象放在SCNCamera当前方向的前面

    >生成SCNVector4,它定向节点,使其“面向”相机?但是让我有点失落.我看到了许多类似的问题,比如thisone,但没有答案.嘿,如果要将对象放在相对于另一个节点的某个位置,并且与参考节点的方向相同,则可以使用这个更简单的函数:如果您想将’node’2m放在某个’cameraNode’前面,你可以这样称呼:

  10. 如何在Swift中继承NSOperation以将SKAction对象排队以进行串行执行?

    Rob为子类化NSOperation提供了agreatObjective-Csolution,以实现SKAction对象的串行排队机制.我在自己的Swift项目中成功实现了这一点.要使用Actionoperation,请在客户端类中实例化NSOperationQueue类成员:在init方法中添加以下重要行:然后当您准备好向其添加SKActions时,它们会连续运行:您是否需要在任何时候终止操作:希望有所帮助!

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部