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

前言

我們知道H5頁面經(jīng)常需要將用戶導(dǎo)流到APP,通過下載安裝包或者跳轉(zhuǎn)至應(yīng)用寶市場(chǎng)/Appstore等方式進(jìn)行導(dǎo)流隘膘。但是由于小程序嵌套webview時(shí)需要校驗(yàn)域名,因此跳轉(zhuǎn)到第三方應(yīng)用市場(chǎng)和Appstroe無法實(shí)現(xiàn)導(dǎo)流缚窿。那怎么辦呢?
只能說道高一尺魔高一丈棘幸,看看微博小程序是怎么導(dǎo)流的:

01.gif

曲線救國的方式焰扳,利用小程序的在線功能可以打開H5的方式倦零,去進(jìn)行下載引導(dǎo)误续。
于是,就引出了這次文檔的主題扫茅,小程序在線客服自動(dòng)回復(fù)功能蹋嵌。??

閱讀本文檔之前,最好已經(jīng)了解過小程序客服信息官方的相關(guān)文檔:

1.客服消息使用指南

2.小程序客服消息服務(wù)端接口

3.客服消息開發(fā)文檔

這次開發(fā)做在線客服功能也踩了不少坑葫隙,網(wǎng)上也查閱不少資料栽烂,但大部分的后臺(tái)都是基于php或者python,java開發(fā),node.js開發(fā)的較少恋脚,因此將這次開發(fā)的流程記錄一下腺办,供大家參考,避免大家踩坑糟描』澈恚可能會(huì)有一些錯(cuò)誤地方歡迎指正交流。
另外船响,我們用的node框架是基于koa自行封裝的躬拢,在一些細(xì)節(jié)實(shí)現(xiàn)上和其他框架會(huì)有區(qū)別,不必糾結(jié)见间。

需求描述

小程序中點(diǎn)按鈕跳轉(zhuǎn)在線客服界面聊闯,根據(jù)關(guān)鍵詞自動(dòng)回復(fù)
客服回復(fù)判斷條件,支持cms配置key米诉,及 respond
respond 支持配置以下類型菱蔬,及回復(fù)內(nèi)容:

type 內(nèi)容
text text=文本回復(fù)內(nèi)容
link title=標(biāo)題 description=描述 url=跳轉(zhuǎn)鏈接 thumb_url=圖片地址
image imageurl=圖片地址
  • 配置后用戶需要精準(zhǔn)匹配回復(fù)條件才可收到自動(dòng)回復(fù)
  • 可支持配置多個(gè)key,及對(duì)應(yīng)respond
  • 除了配置的key以外的回復(fù)史侣,可配置默認(rèn)的自動(dòng)回復(fù)

開發(fā)流程

寫個(gè)跳轉(zhuǎn)客服的按鈕吧

index.wxml

<button open-type="contact">轉(zhuǎn)在線客服</button>

后臺(tái)配置

登錄小程序后臺(tái)后汗销,在「開發(fā)」-「開發(fā)設(shè)置」-「消息推送」中,管理員掃碼啟用消息服務(wù)抵窒,填寫服務(wù)器地址(URL)弛针、令牌(Token) 和 消息加密密鑰(EncodingAESKey)等信息。

o2.png

  1. URL服務(wù)器地址

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

務(wù)必要記住掉房,服務(wù)器地址必須是線上地址茧跋,因?yàn)樾枰⑿欧?wù)器去訪問。localhost卓囚,IP瘾杭,內(nèi)網(wǎng)地址都不行的。

不然會(huì)提示 '解析失敗哪亿,請(qǐng)檢查信息是否填寫正確'粥烁。

那么問題來了贤笆,不同的公司都有一套上線流程,總不能為了調(diào)試URL是否可用要上到線上去測(cè)試讨阻,成本太大芥永,也不方便。

這就要引出內(nèi)網(wǎng)穿透了钝吮,簡(jiǎn)單來說就是配置一個(gè)線上域名埋涧,但是這個(gè)域名可以穿透到你配置的本地開發(fā)地址上,這樣可以方便你去調(diào)試看日志奇瘦。
推薦一個(gè)可以實(shí)現(xiàn)內(nèi)網(wǎng)穿透的工具棘催。(非廣告 ??)

NATAPP 具體不詳細(xì)介紹,免得廣告嫌疑耳标。

簡(jiǎn)單說巧鸭,NATAPP有免費(fèi)和付費(fèi)兩種模式,免費(fèi)的是域名不定時(shí)更換麻捻,對(duì)于微信的推送消息配置一個(gè)月只有3次更改機(jī)會(huì)來說纲仍,有點(diǎn)奢侈。不定什么時(shí)候配置的域名就不能訪問贸毕,得重新配置郑叠。而付費(fèi)的則是固定域名,映射的內(nèi)網(wǎng)地址也可以隨時(shí)更改明棍。樓主從免費(fèi)切到付費(fèi)模式乡革,一個(gè)月的VIP使用大概十幾塊錢吧。

03.png

2.Token

Token自己隨便寫就行了摊腋,但是要記住它沸版,因?yàn)槟阍诮涌谥幸玫摹?/p>

3.EncodingAESKey

隨機(jī)生成即可。

4.加密方式和數(shù)據(jù)格式

根據(jù)自己喜歡選擇兴蒸,樓主選擇的安全模式和JSON格式视粮。
不同的模式和數(shù)據(jù)格式,在開發(fā)上會(huì)有不同橙凳,自己衡量蕾殴。
既然這些配置都清楚,那開始碼代碼岛啸。

驗(yàn)證消息的確來自微信服務(wù)器

配置提交前钓觉,需要把驗(yàn)證消息來自微信服務(wù)器的接口寫好。

server.js

    /*
     * https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
     * 驗(yàn)證消息的確來自微信服務(wù)器
     * 開發(fā)者通過檢驗(yàn) signature 對(duì)請(qǐng)求進(jìn)行校驗(yàn)(下面有校驗(yàn)方式)坚踩。
     * 若確認(rèn)此次 GET 請(qǐng)求來自微信服務(wù)器荡灾,請(qǐng)?jiān)瓨臃祷?echostr 參數(shù)內(nèi)容,
     * 則接入生效,成為開發(fā)者成功批幌,否則接入失敗础锐。加密/校驗(yàn)流程如下:
     * 將token、timestamp逼裆、nonce三個(gè)參數(shù)進(jìn)行字典序排序
     * 將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
     * 開發(fā)者獲得加密后的字符串可與signature對(duì)比,標(biāo)識(shí)該請(qǐng)求來源于微信
     */
     const crypto = require('crypto');
     async wxCallbackAction(){
        const ctx = this.ctx;
        const method = ctx.method;
        //微信服務(wù)器簽名驗(yàn)證赦政,確認(rèn)請(qǐng)求來自微信
        if(method === 'GET') {
            // 1.獲取微信服務(wù)器Get請(qǐng)求的參數(shù) signature胜宇、timestamp、nonce恢着、echostr
            const {
                signature,
                timestamp,
                nonce,
                echostr
            } = ctx.query;
            
            // 2.將token桐愉、timestamp、nonce三個(gè)參數(shù)進(jìn)行字典序排序
            let array = ['yourToken', timestamp, nonce];
            array.sort();
            
            // 3.將三個(gè)參數(shù)字符串拼接成一個(gè)字符串進(jìn)行sha1加密
            const tempStr = array.join('');
            const hashCode = crypto.createHash('sha1'); //創(chuàng)建加密類型
            const resultCode = hashCode.update(tempStr, 'utf8').digest('hex');
            
            // 4.開發(fā)者獲得加密后的字符串可與signature對(duì)比掰派,標(biāo)識(shí)該請(qǐng)求來源于微信
            if (resultCode === signature) {
                console.log('驗(yàn)證成功从诲,消息是從微信服務(wù)器轉(zhuǎn)發(fā)過來');
                return this.json(echostr);
            }else {
                console.log('驗(yàn)證失敗C蚁邸O德濉!');
                return this.json({
                    status: -1,
                    message: "驗(yàn)證失敗"
                });
            }
            
        }
     }

驗(yàn)證接口開發(fā)完畢略步,后臺(tái)配置可以去點(diǎn)提交了描扯。配置成功會(huì)提示如下:

04.png

接收消息和推送消息

當(dāng)用戶在客服會(huì)話發(fā)送消息、或由某些特定的用戶操作引發(fā)事件推送時(shí)趟薄,微信服務(wù)器會(huì)將消息或事件的數(shù)據(jù)包發(fā)送到開發(fā)者填寫的 URL绽诚。開發(fā)者收到請(qǐng)求后可以使用 發(fā)送客服消息 接口進(jìn)行異步回復(fù)。

本文以接收文本消息為例開發(fā):

server.js

    const WXDecryptContact = require('./WXDecryptContact');
    async wxCallbackAction(){
        const ctx = this.ctx;
        const method = ctx.method;
        //接收信息時(shí) 為POST請(qǐng)求杭煎;(完整代碼自行與上面驗(yàn)證時(shí)的合并即可)
        if(method === 'POST'){
            const { Encrypt } = ctx.request.body;
            //配置時(shí)選的安全模式 因此需要解密
            if(!Encrypt){
                return this.json('success');
            }
            const decryptData = WXDecryptContact(Encrypt);
            await this._handleWxMsg(decryptData);
            return this.json('success');
        }else{
            return this.json('success');
        }
    }
    
    //處理微信回調(diào)消息的總?cè)肟?(只處理了文本類型恩够,其他類型自行添加)
    async _handleWxMsg(msgJson){
        if(!msgJson){
            return this.json('success');
        }

        const { MsgType } = msgJson;
        if(MsgType === 'text'){
            await this._sendTextMessage(msgJson);
        }
    }
    
    //微信文本信息關(guān)鍵字自動(dòng)回復(fù)
    async _sendTextMessage(msgJson){
        //獲取CMS客服關(guān)鍵詞回復(fù)配置
        const result = await this.callService('cms.getDataByName', 'wxApplet.contact');
        
        let keyWordObj = result.data || {};
    
        //默認(rèn)回復(fù)default
        let options = keyWordObj.default;
        for(let key in keyWordObj){
            //查看是否命中配置的關(guān)鍵詞
            if(msgJson.Content === key){
                //CMS配置項(xiàng)
                options = keyWordObj[key];
                }
            }
        }
        
        //獲取access_token
        const accessToken = await this._getAccessToken();
        
        /*
        * 先判斷配置回復(fù)的消息類型是不是image類型
        * 如果是 則需要先通過 新增素材接口 上傳圖片文件獲得 media_id
        */
        
        let media_id = '';
        if(options.type === 'image'){
            //獲取圖片地址(相對(duì)路徑)
            let url = options.url;
            const file = fs.createReadStream(url);
            
            //調(diào)用微信 uploadTempMedia接口 具體實(shí)現(xiàn)見 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獲取失敗 則按默認(rèn)處理
                options = keyWordObj.default;
            }
        }
        
        //回復(fù)信息給用戶
        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客服關(guān)鍵詞回復(fù)配置
* 這個(gè)接口只是為了回去CMS配置的字段回復(fù)關(guān)鍵字配置 返回的data數(shù)據(jù)結(jié)構(gòu)如下
*/
async contact(){
    return {
        data: {
            "1": {
                "type": "link",
                "title": "點(diǎn)擊下載[****]APP",
                "description": "注冊(cè)領(lǐng)取領(lǐng)***元注冊(cè)紅包禮",
                "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": "再見"
              }
        }
    }
}

/*
 * 把媒體文件上傳到微信服務(wù)器。目前僅支持圖片羡铲。用于發(fā)送客服消息或被動(dòng)回復(fù)用戶消息蜂桶。
 * 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
                });
            }
        });
    }
 }
 
 /*
 * 發(fā)送客服消息給用戶
 * 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);
};

// 微信轉(zhuǎn)發(fā)客服消息解密
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;
};

// 解密微信返回給配置的消息服務(wù)器的信息
const decryptWXContact = (wechatData) => {
    if(!wechatData){
        wechatData = '';
    }
    //EncodingAESKey 為后臺(tái)配置時(shí)隨機(jī)生成的
    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

總結(jié)

開發(fā)并不是一帆風(fēng)順的也切,也遇到了一些值得留意的坑屎飘,強(qiáng)調(diào)一下:

  • 后臺(tái)配置URL地址一定外網(wǎng)可訪問(可以通過內(nèi)網(wǎng)穿透解決)
  • 文件上傳接口uploadTempMedia media參數(shù)要用 FormData數(shù)據(jù)格式 (用node的request庫很容易實(shí)現(xiàn)。urllib這個(gè)庫有坑有坑 都是淚T_T)
  • 切記接收消息不論成功失敗都要返回success贾费,不然即使成功接收返回消息钦购,日志沒有報(bào)錯(cuò)的情況下,還是出現(xiàn)IOS提示該小程序提供的服務(wù)出現(xiàn)故障 請(qǐng)稍后再試褂萧。

參考資料

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

request文檔

上一篇: 小程序踩坑指南

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末押桃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子导犹,更是在濱河造成了極大的恐慌唱凯,老刑警劉巖羡忘,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異磕昼,居然都是意外死亡卷雕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門票从,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漫雕,“玉大人,你說我怎么就攤上這事峰鄙〗洌” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵吟榴,是天一觀的道長(zhǎng)魁蒜。 經(jīng)常有香客問我,道長(zhǎng)吩翻,這世上最難降的妖魔是什么兜看? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮狭瞎,結(jié)果婚禮上铣减,老公的妹妹穿的比我還像新娘。我一直安慰自己脚作,他們只是感情好葫哗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著球涛,像睡著了一般劣针。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亿扁,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天捺典,我揣著相機(jī)與錄音,去河邊找鬼从祝。 笑死襟己,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的牍陌。 我是一名探鬼主播擎浴,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼毒涧!你這毒婦竟也來了贮预?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仿吞,沒想到半個(gè)月后滑频,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唤冈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年峡迷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片你虹。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绘搞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出售葡,到底是詐尸還是另有隱情看杭,我是刑警寧澤忠藤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布挟伙,位于F島的核電站,受9級(jí)特大地震影響模孩,放射性物質(zhì)發(fā)生泄漏尖阔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一榨咐、第九天 我趴在偏房一處隱蔽的房頂上張望介却。 院中可真熱鬧,春花似錦块茁、人聲如沸齿坷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽永淌。三九已至,卻和暖如春佩耳,著一層夾襖步出監(jiān)牢的瞬間遂蛀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國打工干厚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留李滴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓蛮瞄,卻偏偏與公主長(zhǎng)得像所坯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挂捅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 點(diǎn)擊查看原文 Web SDK 開發(fā)手冊(cè) SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,758評(píng)論 0 15
  • 一直在做小程序包竹,可以對(duì)于后端還是一知半解。近些天在看node相關(guān)的內(nèi)容,于是想嘗試用node寫寫接口周瞎,全當(dāng)自己學(xué)習(xí)...
    前端開發(fā)小匠閱讀 2,428評(píng)論 0 4
  • 客服功能: 會(huì)話入口: 小程序內(nèi):開發(fā)者在小程序內(nèi)添加客服消息按鈕組件,用戶可以在小陳需內(nèi)喚起客服會(huì)話頁面,給小程...
    佩佩216閱讀 10,866評(píng)論 1 9
  • (愿無論經(jīng)歷什么 璐璐同學(xué)都擁有一顆包容心 內(nèi)心澄澈善良 對(duì)未來充滿希望 就像那年剛畢業(yè)的時(shí)候 給自己許下的小小愿...
    冰棍她姐閱讀 151評(píng)論 0 0
  • 記憶中的三和面苗缩,白面三分之一,豆面三分之一声诸,小粉三分之一酱讶,如果是黑豆,搟出來的面黑黑心彼乌,煮出來硬硬的泻肯,吃起來口感不...
    靜夜思今閱讀 544評(píng)論 0 2