微信公衆號_訂閱號_被動回覆用戶消息

node.js 做爲服務器node

微信公衆號(訂閱號)git

給我的、企業、組織 提供業務服務用戶管理能力的全新服務平臺web

  • 企業微信: 無需開發,直接使用
  • 小程序
  • 服務號: 單獨的一條消息顯示;偏向信息查詢;一個月只能羣發消息 4 條;須要企業認證
  • 訂閱號: 收錄在 "訂閱號" 中;一天只能羣發 1 條消息

訂閱號 與 服務號 開發模式 是同樣的express

  • 常見功能:
  • 微信分享
  • 羣發
  • 自定義菜單
  • 打賞

註冊npm

  • 在線接口調試工具

接口: 網址 url小程序

  • web開發者工具
  • 公衆平臺測試帳號

無需申請公衆帳號便可在測試帳號中測試全部高級接口數組

 

  • 基本交互流程
  • 用戶 使用 微信 將消息發送到 微信公衆號 上
  • 微信客戶端 將消息發送到騰訊本身的微信服務器
  • 騰訊的微信服務器 會將用戶消息轉發到 開發者服務器(1. 搭建服務器____須要與服務器進行交互配置)
  • 開發者服務器 又將消息響應給 微信服務器器
  • 微信服務器 最後將消息響應給相應的用戶
  • 1. 搭建服務器

1、買域名服務器

2、使得 開發服務器地址 萬維網能訪問微信

開啓 node.js 寫好的服務器併發

雙擊啓動 ngrok 客戶端(ngrok 內網傳透)

ngrok http 3000

  • 填寫的 接口配置信息 url
  • Token 參與微信簽名加密的一個字段,越複雜越好
  • 確保 服務器開着的,點擊 提交
  • 微信的加密簽名 由 timestamp、nonce、token三個參數加密生成的簽名

2. 驗證消息來自於服務器(只有驗證成功了,才能繼續開發。若是步驟無誤,多提交幾回,總會成功)

// 1. 定義本身的 測試號及接口 配置信息

const myWeChat= {

token: 'atguiguHTML0920',
appID: 'wxc8e92f7aada0fbca0',
appsecret: 'b4054e90b7a8sdasdasd0af50bf7fc3e87' 

};

app.use((request, response, next)=>{

// 2. 將 timestamp、nonce、token 三個參數組合成 數組,按照字典序進行排序

const {token} = myWeChat;

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

const sortArr = [timestamp, nonce, token].sort();

// 3. 將排序後的參數拼接成一個新的字符串,進行 sha1 加密,獲得的就是 signature

const sha1Str = sha1(sortArr.join(""));

// 4. 將 加密後的簽名 和 微信發送來的 signature 進行對比

if(sha1Str === signature){

// 同樣,說明消息來自於服務器,返回 echostr 給微信服務器

response.end(echostr);

}else{

// 不同,說明消息不來自於微信服務器,返回 error 錯誤

response.end('error');

};

next();

});

/* 此時,再次點擊提交,顯示 "配置成功" */

/* 用手機掃描 測試帳號的二維碼 關注,併發送任意消息____能夠看到服務器接受到的數據*/

3. 驗證服務器有效性模塊化 源代碼

config/index.js

  • module.exports = {
        token: 'FinnKou',
        appID: 'wxba59db235745154d22cd32d',
        appsecret: '62a454d75919545d2f276680fcb6148d77b2e31'
    };

wechat/handleRequest.js

  • const sha1 = require('sha1');
    const {token} = require('../config');
    
    module.exports = ()=>{
        return (request, response, next)=>{
            const {signature, echostr, timestamp, nonce} = request.query;
            
            const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
            
            if(sha1Str === signature){
                response.end(echostr);
            }else{
                response.end("error");
            };
            
            next();
        };
    };

index.js

  • const express = require('express');
    const handleRequest = require('./handleRequest');
    
    const app = express();
    
    app.use(express.urlencoded({extended: true}));
    
    app.use(handleRequest());
    
    app.listen(
        3000,
        err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!')
    );
  • 正式開發功能模塊

1. 被動回覆用戶消息789(此時,有兩個公衆號,一個本人的,一個測試的)

驗證服務器 有效性實際上是 get 請求

用戶發送的消息是 post 請求

 

if(request.method === 'GET'){    // 非用戶發送的消息

if(sha1Str === signature){

response.end(echostr);

}else{

response.end('error');

};

}else if(request.method === 'POST'){    // 多是用戶發送的消息

 if(sha1Str !== signature)response.end('error');

// 接收用戶信息

const userXMLData = await getUserXMLData(request);

// 將 xml 數據轉化爲 js 對象____xml2js

}else{

response.end('error');

};

// 當服務器沒有返回響應時,微信服務器會發送 3 次請求到開發者服務器

// 每次請求都會佔用使用接口的使用次數

// 爲了保證後續開發有次數,返回一個響應 resquest.end('');

/*

若是node 服務器 發送一個不正確的 xml 消息,會在用戶微信報錯

 

*/

實例源代碼:

index.js

  • const express = require('express'); const handleRequest = require('./handleRequest'); const app = express(); app.use(express.urlencoded({extended: true})); app.use(handleRequest()); // 使用中間件的方式 激活自定義模塊  app.listen( 3000, err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!') );

config/index.js

  • module.exports = { // 微信公衆號 配置信息 token: 'FinnKou', appID: 'wxba554569dbd1127d22cd32d', appsecret: '62ad759915d251f276680fc153b618d77b2e31' };

handleRequest/index.js

  • const sha1 = require('sha1');  // sha1 加密庫 const {token} = require('../config'); const {getUserXMLData} = require('../utils/getUserDataAsync'); const {XML2JSON, JSON2Obj} = require("../utils/tools"); module.exports = ()=>{ return async (request, response, next)=>{ const {signature, echostr, timestamp, nonce} = request.query; const sha1Str = sha1([timestamp, nonce, token].sort().join("")); if(request.method === 'GET'){ // 服務器發過來的消息 if(sha1Str === signature){ response.end(echostr); }else{ response.end("error"); }; }else if(request.method === 'POST'){ if(sha1Str !== signature){ // 非微信用戶發過來的消息 response.end("error"); }; // 用戶微信客戶端 發過來的消息 /**** <xml> <ToUserName><![CDATA[gh_d9bf45407d2a]]></ToUserName> <FromUserName><![CDATA[oSX3Z1aufrhsCwuEKXbVRfqOC1Wo]]></FromUserName> <CreateTime>1545741162</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[1]]></Content> <MsgId>6638907739305821698</MsgId> </xml> ****/ const XMLData = await getUserXMLData(request); const JSONData = XML2JSON(XMLData); const objData = JSON2Obj(JSONData); /* { ToUserName: 'gh_d9bf45407d2a', FromUserName: 'oSX3Z1aufrhsCwuEKXbVRfqOC1Wo', CreateTime: '1545744183', MsgType: 'text', Content: '1', MsgId: '6638920714402022972' } */ if(objData.Content === '1'){ response.send(`<xml> <ToUserName><![CDATA[${objData.FromUserName}]]></ToUserName> <FromUserName><![CDATA[${objData.ToUserName}]]></FromUserName> <CreateTime>${Date.now()}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[聖誕節快樂]]></Content> </xml>`  ); }else{ response.send( `<xml> <ToUserName><![CDATA[${objData.FromUserName}]]></ToUserName> <FromUserName><![CDATA[${objData.ToUserName}]]></FromUserName> <CreateTime>${Date.now()}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[hello world!]]></Content> </xml>`  ); }; }else{ response.end("error"); }; }; };

utils/tools.js

  • const {parseString} = require('xml2js'); // xml2js 是第三方庫,XML 字符串 轉 JSON 對象 module.exports = { XML2JSON(originXML){ // XML 字符串 轉 JSON 對象 let JSONData = null; parseString(originXML, {"trim": true}, (err, result)=>{ if(!err){ JSONData = result; }; }); return JSONData; }, JSON2Obj({xml}){ // JSON 對象 轉成 普通對象 let Obj = {}; for (let attr in xml){ const value = xml[attr]; Obj[attr] = value[0]; }; return Obj; } };

utils/getUserDataAsync.js

  • module.exports = { getUserXMLData(request){ // 獲取用戶微信客戶端 發送到 服務器的 XML 字符串 return new Promise((resolve, reject)=>{ let resultStr = ''; request.on("data", browser_info=>{ // 流方式 接受 resultStr += browser_info.toString(); }).on("end", ()=>{ resolve(resultStr); }); }); } };

2. 看手冊,根據用戶的內容,類型,回覆你想要回復的內容

源代碼:

index.js

  • const express = require('express');
    const handleRequest = require('./handleRequest');
    
    const app = express();
    
    app.use(express.urlencoded({extended: true}));
    
    app.use(handleRequest());
    
    app.listen(
        3000,
        err=>console.log(err?err:'\n\n服務器已啓動\n\t\tHunting Happy!')
    );

config/index.js

  • module.exports = {
        token: 'FinnKou',
        appID: 'wxba59d182bd7d2245cd32d',
        appsecret: '62ad7594595d2f276680f454cb618d77b2e31'
    };

handleRequest/index.js

  • const sha1 = require('sha1');
    const {token} = require('../config');
    const {getUserXMLData} = require('../utils/getUserDataAsync');
    const {XML2JSON, JSON2Obj} = require('../utils/tools');
    const {autoReply} = require('../autoReply');
    
    module.exports = ()=>{
        return async (request, response, next)=>{
            const {signature, echostr, timestamp, nonce} = request.query;
            
            const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
            
            if(request.method === 'GET'){    // 服務器發過來的消息
                if(sha1Str === signature){
                    response.end(echostr);
                }else{
                    response.end("error");
                };
            }else if(request.method === 'POST'){
                if(sha1Str !== signature){    // 非微信用戶發過來的消息
                    response.end("error");
                };
                // 用戶發過來的消息
                const XMLData = await getUserXMLData(request);
                const JSONData = XML2JSON(XMLData);
                const objData = JSON2Obj(JSONData);
        
                console.log(objData);
                response.send(autoReply(objData));    // 封裝 自動回覆用戶消息
                
            }else{
                response.end("error");
            };
        };
    };

utils/getUserDataAsync.js

  • module.exports = {
        getUserXMLData(request){
            return new Promise((resolve, reject)=>{
                let resultStr = '';
                request.on("data", browser_info=>{
                    resultStr += browser_info.toString();
                }).on("end", ()=>{
                    resolve(resultStr);
                });
            });
        }
    };
    /****    用戶發過來的消息
        <xml>
        <ToUserName><![CDATA[gh_d9bf45407d2a]]></ToUserName>
        <FromUserName><![CDATA[oSX3Z1aufrhsCwuEKXbVRfqOC1Wo]]></FromUserName>
        <CreateTime>1545741162</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[1]]></Content>
        <MsgId>6638907739305821698</MsgId>
        </xml>
    ****/

utils/tools.js

  • const {parseString} = require('xml2js');
    
    module.exports = {
        XML2JSON(originXML){
            let JSONData = null;
            parseString(originXML, {"trim": true}, (err, result)=>{
                if(!err){
                    JSONData = result;
                };
            });
            return JSONData;
        },
        JSON2Obj({xml}){
            let Obj = {};
            for (let attr in xml){
                const value = xml[attr];
                Obj[attr] = value[0];
            };
            
            return Obj;
        }
    };
    // 用戶微信客戶端 發過來的消息
    /****
     {
        ToUserName: 'gh_d9bf45407d2a',
        FromUserName: 'oSX3Z1aufrhsCwuEKXbVRfqOC1Wo',
        CreateTime: '1545744183',
        MsgType: 'text',
        Content: '1',
        MsgId: '6638920714402022972'
     }
    ****/

autoReply/index.js

  • const {getReplyInfo} = require('./getReplyInfo');
    
    module.exports = {
        autoReply(objData){
            
            const options = getReplyInfo(objData);
        
            let replyXML = `<xml>
                              <ToUserName><![CDATA[${options.toUserName}]]></ToUserName>
                              <FromUserName><![CDATA[${options.fromUserName}]]></FromUserName>
                              <CreateTime>${options.createTime}</CreateTime>
                              <MsgType><![CDATA[${options.msgType}]]></MsgType>
                           `;
            switch (options.msgType) {
                case 'text':{
                    replyXML += `<Content><![CDATA[${options.content}]]></Content>`;
                }break;
                
                case 'image':{
                    replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`;
                    replyXML += `<Image><MediaId><![CDATA[${options.image}]]></MediaId></Image>`;
                }break;
        
                case 'voice':{
                    replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`;
                    replyXML += `<Voice><MediaId><![CDATA[${options.voice}]]></MediaId></Voice>`;
                }break;
        
                case 'video':{
                    replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`;
                    replyXML += `<Video>
                                    <MediaId><![CDATA[${options.video}]]></MediaId>
                                    <Title><![CDATA[${options.videoTitle}]]></Title>
                                    <Description><![CDATA[${options.videoDescription}]]></Description>
                                 </Video>`;
                }break;
        
                case 'music':{
                    replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`;
                    replyXML += `<Music>
                                    <Title><![CDATA[${options.musicTitle}]]></Title>
                                    <Description><![CDATA[${options.musicDescription}]]></Description>
                                    <MusicUrl><![CDATA[${options.musicUrl}]]></MusicUrl>
                                    <HQMusicUrl><![CDATA[${options.hqMusicUrl}]]></HQMusicUrl>
                                    <ThumbMediaId><![CDATA[${options.musicMediaId}]]></ThumbMediaId>
                                 </Music>`;
                }break;
        
                case 'news':{
                    replyXML += `<MsgType><![CDATA[${options.msgType}]]></MsgType>`;
                    replyXML += `<ArticleCount>${options.content.length}</ArticleCount><Articles>`;
        
                    options.content.forEach(item => {
                        replyXML += `<item>
                                          <Title><![CDATA[${item.title}]]></Title>
                                          <Description><![CDATA[${item.description}]]></Description>
                                          <PicUrl><![CDATA[${item.picUrl}]]></PicUrl>
                                          <Url><![CDATA[${item.url}]]></Url>
                                     </item>`;
                    });
        
                    replyXML += `</Articles>`;
                }break;
            };
            
            replyXML += '</xml>';
            return replyXML;
        }
    };

autoReply/getReplyInfo.js

  • module.exports = {
        getReplyInfo(objData){
            let options = {
                toUserName: objData.FromUserName,
                fromUserName: objData.ToUserName,
                createTime: Date.now(),
                msgType: objData.MsgType,
                
                /**** 文本 ****/
                content: objData.Content,
                
                /**** 圖片 ****/
                image: objData.MediaId,    // MediaId
                
                /**** 語音 ****/
                voice: objData.MediaId,    // MediaId
                recognition: objData.Recognition,    // MediaId
                
                /**** 視頻 ****/
                video: objData.MediaId,    // MediaId
                videoTitle: objData.Title,
                videoDescription: objData.Description,
                
                /**** 音樂 ****/
                musicTitle: objData.Title,
                musicDescription:objData.Description,
                musicUrl: objData.MusicUrl,
                hqMusicUrl: objData.HQMusicUrl,
                musicMediaId: objData.MediaId,    // MediaId
                
                /**** 地理位置 ****/
                location_X: objData.Location_X,
                location_Y: objData.Location_Y,
                scale: objData.Scale,
                label: objData.Label,
        
                /**** +位置 ****/
                latitude: objData.Latitude,
                longitude: objData.Longitude,
                precision: objData.Precision,
        
                /**** 菜單 CLICK ****/
                eventKey: objData.EventKey
            };
            
            switch (options.msgType) {
                case 'text':{    // 文本
                    switch (objData.Content) {
                        case 'news':{
                            options.msgType = 'news';
                            options.content = [{
                                title: '微信公衆號開發',
                                description: '這裏有最新的公衆號教程',
                                picUrl: 'https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2136740882,3271518133&fm=58&bpow=630&bpoh=630',
                                url: 'http://www.atguigu.com'
                            }];
                        }break;
                        
                        case '1':{
                            options.content = '我會對你 1 心 1 意~';
                        }break;
                        
                        case '2':{
                            options.content = '其實你一點都不 2 ~';
                        }break;
                        
                        case '3':{
                            options.content = '可能也會 3 心 二 意~';
                        }break;
                    };
                }break;
        
                case 'voice':{    // 語音
                    options.msgType = 'text';
                    options.content = `語音識別結果爲:${options.recognition}`;
                }break;
            
                case 'location':{    // + 位置
                    options.msgType = 'text';
                    options.content = ` 地理位置緯度:${options.location_X}
                                        地理位置經度:${options.location_Y}
                                        地圖縮放大小: ${options.scale}
                                        地理位置信息: ${options.label}`;
                }break;
                
                case 'event':{    // 事件推送
                    switch (objData.Event) {
                        case 'subscribe':{    // 訂閱
                            options.msgType = 'text';
                            options.content = '歡迎訂閱 FinnKou 的內容!'
                            if (objData.EventKey) {
                                //掃描帶參數二維碼關注的。 通常應用在活動上
                                options.content = '掃描帶參數二維碼關注, 歡迎您關注公衆號~';
                            }
                        }break;
                        
                        case 'unsubscribe':{    // 取消訂閱
                            console.log('無情取關~');
                        }break;
        
                        case 'LOCATION':{    // 用戶上報地理位置
                            // 當用戶關注公衆號時,它會問你是否容許上報地理位置,若是容許纔會觸發當前事件
                            options.msgType = 'text';
                            options.content = ` 地理位置緯度:${options.latitude}
                                                地理位置經度:${options.longitude}
                                                位置信息: ${options.precision}`;
                        }break;
        
                        case 'CLICK':{    // 用戶點擊菜單按鈕
                            options.msgType = 'text';
                            options.content = `菜單 key:${options.eventKey}`;
                        }break;
                    };
                }break;
        
                case 'link':{    // 連接消息
            
                }break;
                
                case 'image':{    // 圖片
                
                }break;
                
                case 'video':{    // 視頻
                
                }break;
        
                case 'shortvideo':{    // 小視頻
            
                }break;
                
                case 'music':{    // 音樂
                
                }break;
                
                case 'news':{    // 圖文
                
                }break;
            };
            
            return options;
        }
    };
相關文章
相關標籤/搜索