小程序打怪之在線客服自動回復功能(node版)

前言

我們知道H5頁面經常需要將用戶導流到APP,通過下載安裝包或者跳轉至應用寶市場/Appstore等方式進行導流。但是由于小程序嵌套webview時需要校驗域名,因此跳轉到第三方應用市場和Appstroe無法實現導流。那怎么辦呢?
只能說道高一尺魔高一丈,看看微博小程序是怎么導流的:

01.gif

曲線救國的方式,利用小程序的在線功能可以打開H5的方式,去進行下載引導。
于是,就引出了這次文檔的主題,小程序在線客服自動回復功能。??

閱讀本文檔之前,最好已經了解過小程序客服信息官方的相關文檔:

1.客服消息使用指南

2.小程序客服消息服務端接口

3.客服消息開發文檔

這次開發做在線客服功能也踩了不少坑,網上也查閱不少資料,但大部分的后臺都是基于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)等信息。

o2.png

  1. URL服務器地址

URL: 開發者用來接收微信消息和事件的接口 URL。開發者所填寫的URL 必須以 http:// 或 https:// 開頭,分別支持 80 端口和 443 端口。

務必要記住,服務器地址必須是線上地址,因為需要微信服務器去訪問。localhost,IP,內網地址都不行的。

不然會提示 '解析失敗,請檢查信息是否填寫正確'。

那么問題來了,不同的公司都有一套上線流程,總不能為了調試URL是否可用要上到線上去測試,成本太大,也不方便。

這就要引出內網穿透了,簡單來說就是配置一個線上域名,但是這個域名可以穿透到你配置的本地開發地址上,這樣可以方便你去調試看日志。
推薦一個可以實現內網穿透的工具。(非廣告 ??)

NATAPP 具體不詳細介紹,免得廣告嫌疑。

簡單說,NATAPP有免費和付費兩種模式,免費的是域名不定時更換,對于微信的推送消息配置一個月只有3次更改機會來說,有點奢侈。不定什么時候配置的域名就不能訪問,得重新配置。而付費的則是固定域名,映射的內網地址也可以隨時更改。樓主從免費切到付費模式,一個月的VIP使用大概十幾塊錢吧。

03.png

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: "驗證失敗"
                });
            }
            
        }
     }

驗證接口開發完畢,后臺配置可以去點提交了。配置成功會提示如下:

04.png

接收消息和推送消息

當用戶在客服會話發送消息、或由某些特定的用戶操作引發事件推送時,微信服務器會將消息或事件的數據包發送到開發者填寫的 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;

呼~ 代碼終于碼完,來看看效果:

05.gif

總結

開發并不是一帆風順的,也遇到了一些值得留意的坑,強調一下:

  • 后臺配置URL地址一定外網可訪問(可以通過內網穿透解決)
  • 文件上傳接口uploadTempMedia media參數要用 FormData數據格式 (用node的request庫很容易實現。urllib這個庫有坑有坑 都是淚T_T)
  • 切記接收消息不論成功失敗都要返回success,不然即使成功接收返回消息,日志沒有報錯的情況下,還是出現IOS提示該小程序提供的服務出現故障 請稍后再試。

參考資料

koa接入微信小程序客服消息

request文檔

上一篇: 小程序踩坑指南

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 點擊查看原文 Web SDK 開發手冊 SDK 概述 網易云信 SDK 為 Web 應用提供一個完善的 IM 系統...
    layjoy閱讀 13,867評論 0 15
  • 一直在做小程序,可以對于后端還是一知半解。近些天在看node相關的內容,于是想嘗試用node寫寫接口,全當自己學習...
    前端開發小匠閱讀 2,447評論 0 4
  • 客服功能: 會話入口: 小程序內:開發者在小程序內添加客服消息按鈕組件,用戶可以在小陳需內喚起客服會話頁面,給小程...
    佩佩216閱讀 10,900評論 1 9
  • (愿無論經歷什么 璐璐同學都擁有一顆包容心 內心澄澈善良 對未來充滿希望 就像那年剛畢業的時候 給自己許下的小小愿...
    冰棍她姐閱讀 155評論 0 0
  • 記憶中的三和面,白面三分之一,豆面三分之一,小粉三分之一,如果是黑豆,搟出來的面黑黑心,煮出來硬硬的,吃起來口感不...
    靜夜思今閱讀 554評論 0 2