我该如何回复你

请原谅我并没有去看webchat-api的文档,所以关于这个话题暂时没什么可说的。不过我也没闲着,作为一名能动手就不动口的暴力型男程序员,本着Definitely Re-invent the wheel的学习精神,自己动手做了公众号服务器比较简单的那一部分,被动回复消息

从上节啃文档啃出来的成果看,微信服务器(小龙出品)会给公众号服务器(小虫出品)的同一个url发两种请求,分别是getpost,我们要在公众号服务器中分别处理这两种请求:

  1. 处理get请求是为了让微信服务器和公众号服务器接头,说白了就是对暗号的过程。微信服务器发过来一个“天王盖地虎”,我们的公众号服务器回一个“宝塔镇河妖”,那肯定是不行的。完成这个过程要借助别人都不知道的token,如果请求中发过来的signature经过验证是有效的,就把echostr还给它,如果无效,就回它“认错人了吧!”。
  2. 处理post请求是为了回应用户发过来的消息或触发的事件,让用户能跟我们的公众号服务器愉快地玩耍。但因为这些消息和事件是放在xml里发过来的,而且响应的时候也要用xml格式封装好,所以除了业务逻辑,还要处理xml的解析和封装。

说到xml解析,因为有express-xml-bodyparser这样的middleware存在,并且这个轮子也不在我们的学习范围里,就拿过来直接用了。

除此之外,既然只有第二项的业务逻辑部分是不同的,那其他的部分我们就可以像webchat一样,搞一个共用的库。而我们对这个库的要求也很简单:

  1. 能验证signature
  2. 能提供json格式的消息给我们
  3. 能把json格式的返回消息封装成xml

而这个库的用法,我们希望是:

  1. 在get请求处理函数中把验证signature需要的数据给它,让它告诉我们true还是false
  2. 在post请求处理函数中把消息或事件给它,让它把要返回的xml数据给我们
  3. 它在处理消息或事件时,能调用我们提供的消息或事件处理函数,给我们json格式的消息,接收我们函数返回的json结果

综合上面这两种考虑,我想用ES 6的类实现模板方法模式。因为这个类干的是为微信服务器提供服务的工作,我决定管它叫Waiter。我们的Waiter类有三个方法:

  1. verifySignature:验证signature
  2. process:处理接收到的消息,调用业务逻辑,将返回结果封装成xml返回
  3. populateReply:由process调用,子类要实现的业务逻辑就放在这里

总体来说,完成后我们的应用大概是下图这个样子的:

具体实现以keystone为基础,首先来看我们的路由定义:

app.get('/api/weixin',routes.weixin.verify)
app.post('/api/weixin',
         xmlparser({
           trim: false, 
           explicitArray: false}), 
         routes.weixin.handle)

针对/api/weixin的post请求添加了中间件xmlparser。

verify的定义非常简单,只是调用waiter的verifySignature:

import keystone from 'keystone'

exports = module.exports = (req, res) => {
    const waiter = keystone.get('weixin waiter')

    const {
        signature,timestamp,nonce
    } = req.query

    console.log("get verify request:",req.query)

    if(waiter.verifySignature(signature,timestamp,nonce)) {
        console.log(`signature ${signature} is valid`)
        res.send(req.query.echostr)
    } else {
        res.send('error')
    }
}

handle的定义更简单,把req交给waiter去process,得到结果,将响应的Content-type设为xml,然后把reply send出去:

import keystone from 'keystone'

exports = module.exports = (req, res) => {
    const waiter = keystone.get('weixin waiter')
    const reply = waiter.process(req)
    res.set('Content-Type', 'text/xml')
    res.send(reply)
}

DWaiterWaiter的子类,只实现了populateReply方法:

class DWaiter extends Waiter {

    populateReply(message) {
        console.log(message)
        var reply = false
        switch(message.msgtype) {
            case 'text':                    
                reply = {
                    msgtype:'text',
                    content: message.content
                }
                break
            case 'image':
                reply = {
                    msgtype:'image',
                    mediaId: message.mediaid
                }
                break
            case 'voice':
                reply = {
                    msgtype:'voice',
                    mediaId: message.mediaid
                }    
                break
            case 'video':
            case 'shortvideo':
            case 'location':
            case 'link':
            case 'event':
            default:
            break         
        }
        return reply
    }
}

这个实现也很简单,只处理了文本、图片和语音三种消息,收到什么就回复什么;其它的全不理。

最后是Waiter类:

class Waiter {
    constructor(options) {
        this.appId = options.appId
        this.appSecret = options.appSecret
        this.token = options.token    
    }
    /**
    * Verify Signature in request from Weixin 
    * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
    * @param signature
    * @param timestamp
    * @param nonce
    */
    verifySignature(signature,timestamp,nonce) {

        console.log('verify siganture',signature)
        const token = this.token

        let shasum = crypto.createHash('sha1');
        let arr = [token, timestamp, nonce].sort();
        shasum.update(arr.join(''));

        return shasum.digest('hex') === signature;
    }
    /**
    * http://mp.weixin.qq.com/wiki/14/89b871b5466b19b3efa4ada8e577d45e.html    
    */
    process(req) {
        const {
            signature,timestamp,nonce
        } = req.query
        var reply = 'success'

        if(this.verifySignature(signature,timestamp,nonce)) {
            const message = req.body.xml
            const _reply = this.populateReply(message) 

            var replyContent = false

            switch(_reply && _reply.msgtype) {
                case 'text':                    
                    replyContent = `<Content><![CDATA[${_reply.content}]]></Content>`    
                    break
                case 'image':
                    replyContent = `<Image>
                                    <MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
                                    </Image>`    
                    break
                case 'voice':
                    replyContent = `<Voice>
                                    <MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
                                    </Voice>`    
                    break
                case 'video':
                    replyContent = `<Video>
                                        <MediaId><![CDATA[${_reply.mediaId}]]></MediaId>
                                        <Title><![CDATA[${_reply.title}]]></Title>
                                        <Description><![CDATA[${_reply.description}]]></Description>
                                    </Video>`    
                    break
                case 'music':
                    replyContent = `<Music>
                                        <Title><![CDATA[${_reply.title}]]]></Title>
                                        <Description><![CDATA[${_reply.description}]]></Description>
                                        <MusicUrl><![CDATA[${_reply.musicUrl}]]></MusicUrl>
                                        <HQMusicUrl><![CDATA[${_reply.hqMusicUrl}]]></HQMusicUrl>
                                        <ThumbMediaId><![CDATA[${_reply.mediaId}]]></ThumbMediaId>
                                    </Music>`    
                    break
                case 'news':
                    var articleItems = _reply.articles.map((article) => {
                        return `<item>
                                <Title><![CDATA[${article.title}]]></Title> 
                                <Description><![CDATA[${article.description}]]></Description>
                                <PicUrl><![CDATA[${article.picurl}]]></PicUrl>
                                <Url><![CDATA[${article.url}]]></Url>
                                </item>`
                    })

                    replyContent = `<ArticleCount>${_reply.articles.length}</ArticleCount>
                                    <Articles>
                                        ${articleItems.join('')}
                                    </Articles>`    
                    break
                default:
                break
            }
            replay = replyContent ? 
                     `<xml>
                        <ToUserName><![CDATA[${message.fromusername}]]></ToUserName>
                        <FromUserName><![CDATA[${message.tousername}]]></FromUserName>
                        <CreateTime><![CDATA[${Date.now()}]]></CreateTime>
                        <MsgType><![CDATA[${_reply.msgtype}]]></MsgType>
                        ${replayContent}
                      </xml>`        
                      :
                      false    
        }
        return reply
    }

    populateReply(message) {
        const reply = {
            msgtype : 'text',
            content: '已收到您的消息!'
        }
        return reply
    }

}

代码也非常简单直白,verifySignature跟webchat的是一样一样一样的,process在签名验证通过后,从req.body.xml中获得解析好的消息或事件,交给populateReply,然后根据populateReply返回的消息类型封装成不同的xml数据。

就酱!

results matching ""

    No results matching ""