1. 程式人生 > >公眾號第三方平臺開發流程詳解

公眾號第三方平臺開發流程詳解

準備工作

1. 註冊申請

2. 建立第三方平臺

進入‘管理中心->建立第三方平臺’
這裡寫圖片描述

2.1. 輸入基本資訊

!基本資訊將顯示在授權頁被使用者看到,請認真填寫
修改平臺稽核成功後,僅對測試公眾號生效,屆時再提交“覆蓋現網全網釋出狀態”並稽核成功後,才可影響現網

這裡寫圖片描述

2.2. 選擇許可權

根據第三方平臺需要的介面許可權進行選擇,需要注意:

  1. 修改平臺稽核成功後,僅對測試公眾號生效,屆時再提交“覆蓋現網全網釋出狀態”並稽核成功後,才可影響現網;
  2. 在修改平臺時,若有許可權的增加,則視為一次平臺升級,已授權使用者使用老許可權不受任何影響,但使用新許可權需要使用者再次進行授權頁進行升級授權,此時注意引導使用者重新授權

這裡寫圖片描述

2.3. 填寫開發資料

授權流程相關
這裡寫圖片描述

授權發起頁:即引導使用者進入授權頁面的入口頁面的域名(後面開發部分會詳細介紹)
授權測試公眾號列表:要填寫公眾號的原始ID,‘微信公眾平臺->公眾號設定->原始ID’
授權事件接收URL:用於接收微信的各種通知,包括授權成功、取消授權、授權更新以及每十分鐘推送的component_verify_ticket(後面開發部分會詳細介紹)

授權後代公眾號實現業務
這裡寫圖片描述

如果有過公眾號開發經驗這部分就相對比較容易理解了,基本上與公眾號開發的配置相同(‘微信公眾平臺->基本配置->伺服器配置’),可以參考

公眾號開發準備這篇文章。
公眾號訊息與事件接收URL 這一項要配置一個萬用字元$APPID$,不同的公眾號事件推送時,這個萬用字元會自動替換為對應的公眾號的AppID

其它
這裡寫圖片描述

這個IP列表要填寫公網IP,我使用的是阿里雲ESC雲伺服器

開發

使用 Node.js進行的服務端開發,資料庫使用 MysqlES7語法,使用Koa2框架。具體框架結構參考使用Koa2搭建web專案這篇文章(一定要看,不然後面的程式碼會看不懂)。下面會按流程逐步完成第三方平臺後臺的開發

為了篇幅工整,所有的工具類(XXXUtil.js)均放在最後列舉

1. 授權事件接收

<完成本文2.3中的 授權事件接收URL 的開發>
在公眾號第三方平臺建立稽核通過後,微信伺服器會向其“授權事件接收URL”每隔10分鐘定時推送component_verify_ticket(重要欄位類似於密碼)。將欄位存入資料庫定期重新整理,component_verify_ticket、component_access_token以及component_auth_code儲存在同一個表中,資料表component結構如下:

id component_ticket component_access_token component_auth_code

【component.js】

const xmlUtil = require("./../utils/xmlUtil.js");
const componentService = require('./../service/componentService.js');

//Be called every 10 minutes to refresh component_verify_ticket by wechat
//Be called when authorized
//Be called when unauthorized
//Be called when refresh
var handleComponentMessage = async (ctx, next) => {
    let requestString = ctx.data;
    let requestMessage = xmlUtil.formatMessage(requestString.xml);

    let query = ctx.query;
    /**
    * Checking whether access_token and pre_auth_code 
    * need to be refreshed according to component_verify_ticket 
    * access_token need to be refreshed every 2 hours
    * pre_auth_code need to be refreshed every 30 minutes
    */
    let result = await componentService.handleComponentMessage(requestMessage, query);

    ctx.response.body = 'success';
}

... ...

module.exports = {
    'POST /handleComponentMessage' :handleComponentMessage
};

【componentService.js】

const xml2js = require('xml2js');
const xmlUtil = require("./../utils/xmlUtil.js");
const wechatCrypto = require('./../utils/cryptoUtil.js');
const http = require('./../utils/httpUtils.js');
//analyze the decrypted message (xml => json)
var resolveMessage = messageXML => {
    var message = new Promise((resolve, reject) => {
            xml2js.parseString(messageXML, {trim: true}, (err, result) => {
            var message = xmlUtil.formatMessage(result.xml);
            resolve(message);
        });
    }).then(function(message) {
        return message;
    });

    return message;
};

var handleComponentMessage = async (requestMessage, query) => {
    let signature = query.msg_signature;
    let timestamp = query.timestamp;
    let nonce = query.nonce;
    logger.debug("Receive messasge from weixin \nsignature: " + signature + "\ntimestamp: " + timestamp + "\nnonce: " + nonce);

    //Create cryptor object for decrypt message
    let cryptor = new wechatCrypto(真實的token, 真實的encodingAESKey, 真實的第三方平臺appID);
    let encryptMessage = requestMessage.Encrypt;
    let decryptMessage = cryptor.decrypt(encryptMessage);

    logger.debug('Receive messasge from weixin decrypted :' + JSON.stringify(decryptMessage));

    var message = await resolveMessage(decryptMessage.message);
    let infoType = message.InfoType;
    if(infoType == 'component_verify_ticket') {
        let ticket = message.ComponentVerifyTicket;
         //query the component_verify_ticket, component_access_token and component_auth_code
        ... ...
        //TODO 將拿到的ticket更新儲存到資料表component中
    } else if(infoType == 'authorized') {
        //TODO authorized
    } else if(infoType == 'unauthorized') {
        //TODO unauthorized
    } else if(infoType == 'refresh') {
        //TODO refresh
    }

    return '';
}

2. 獲取component_access_token和pre_auth_code

由於兩個屬性都需要定期重新整理,所以我做了一個定時任務,定時重新整理這兩個欄位並存入資料表component

【refresh.js】

const schedule = require('node-schedule');
const http = require('./../utils/httpUtils.js');

//refresh component_access_token every 1 hour
var refreshComponentAccessToken = async function() {
    var component = 讀取表component;
    var ticket = component.component_ticket;

    if(ticket == null || ticket == undefined || ticket == '') {
        return;
    }
    var access_token = component.component_access_token;
    var auth_code = component.component_auth_code;

    var componentTokenPostData = {
        component_appid : 第三方appID,
        component_appsecret : 第三方appSecret,
        component_verify_ticket : ticket
    };
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/component/api_component_token',
        method : 'post'
    };

    var component_access_token_result = await http.doHttps_withdata(https_options, componentTokenPostData);
    var access_token_json = JSON.parse(component_access_token_result);
    logger.debug('Refresh component_access_token result: ' + component_access_token_result);

    if(access_token_json.errcode != undefined) {
        return;
    }

    access_token = access_token_json.component_access_token;

    //TODO 將拿到的access_token 更新儲存到資料表component中
}

//refresh pre_auth_code every 20 minutes
var refreshComponentAuthCode = async function() {
    var component = 讀取表component;
    var ticket = component.component_ticket;
    var access_token = component.component_access_token;

    if(access_token == null || access_token == undefined || access_token == '') {
        return;
    }

    var access_token_times = component.component_access_token_times;
    var auth_code = component.component_auth_code;
    var auth_code_times = component.component_auth_code_times;

    var componentAuthCodePostData = {
        component_appid : config.component.appID
    };
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/component/api_create_preauthcode?component_access_token=%ACCESS_TOKEN%',
        method : 'post'
    };

    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', access_token);
    var component_preauthcode_result = await http.doHttps_withdata(https_options, componentAuthCodePostData);
    var preauthcode_json = JSON.parse(component_preauthcode_result);
    logger.debug('Refresh pre_auth_code result: ' + component_preauthcode_result);

    if(preauthcode_json.errcode != undefined) {
        return;
    }

    auth_code = preauthcode_json.pre_auth_code;

    //TODO 將拿到的pre_auth_code 更新儲存到資料表component中
}

//refresh component_access_token every hour
var refreshComponentAccessTokenJob = schedule.scheduleJob('0 0 */1 * * *', refreshComponentAccessToken);

//refresh pre_auth_code every 20 minutes
var refreshComponentAuthCodeJob = schedule.scheduleJob('10 */20 * * * *', refreshComponentAuthCode);

3. 獲取授權

引導使用者進入授權頁,拿到授權碼,再通過授權碼獲取授權令牌(authorizer_access_token)

【component.js】

const xmlUtil = require("./../utils/xmlUtil.js");
const componentService = require('./../service/componentService.js');

var componentAuthorize = async (ctx, next) => {
    let url = await componentService.getAuthorizeUrl();
    ctx.redirect(url);
}

//授權後跳轉到的頁面
var queryAuthorizeInfo =  async (ctx, next) => {
    let query = ctx.query;
    let auth_code = query.auth_code;
    let expires_in = query.expires_in;

    let authorization_info = await componentService.queryAuthorizeInfo(auth_code);

    let html = '<html><body>'
        + '<p>auth_code = ' + query.auth_code + '</p>'
        + '<p>authorizer_appid = ' + authorization_info.authorizer_appid + '</p>'
        + '<p>access_token = ' + authorization_info.authorizer_access_token + '</p>'
        + '<p>refresh_token = ' + authorization_info.authorizer_refresh_token + '</p>'
        + '<p>func_info = ' + JSON.stringify(authorization_info.func_info) + '</p>'
        + '<p>expires_in = ' + query.expires_in + '</p>'
        + '</body></html>';

    ctx.response.type ='text/html';
    ctx.response.body = html;
};

module.exports = {
    'GET /componentAuthorize' :componentAuthorize,
    'GET /queryAuthorizeInfo' :queryAuthorizeInfo
};

【componentService.js】

const xml2js = require('xml2js');
const xmlUtil = require("./../utils/xmlUtil.js");
const wechatCrypto = require('./../utils/cryptoUtil.js');
const http = require('./../utils/httpUtils.js');

let getAuthorizeUrl = async function() {
    let component = 讀取表component;
    let url = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%APPID%&pre_auth_code=%AUTH_CODE%&redirect_uri=%REDIRECT_URI%'
        .replace('%APPID%', 第三方平臺appID)
        .replace('%AUTH_CODE%', component.component_auth_code)
        .replace('%REDIRECT_URI%', /*要跳轉到的頁面*/'http://www.xxxxxxx.com/queryAuthorizeInfo');
    return url;
}

let queryAuthorizeInfo = async (auth_code) => {
    var component = 讀取表component;
    var access_token = component.component_access_token;

    let queryAuthorizePostData = {
        component_appid : 第三方平臺appID,
        authorization_code : auth_code
    };
    let https_options = {
        hostname : 'api.weixin.qq.com',
        path : config.'/cgi-bin/component/api_query_auth?component_access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };

    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', access_token);
    let queryAuthorizeResult = await http.doHttps_withdata(https_options, queryAuthorizePostData);
    let queryAuthorize_json = JSON.parse(queryAuthorizeResult);
    console.log(queryAuthorize_json);

    let authorization_info = queryAuthorize_json.authorization_info;

    let appid = authorization_info.authorizer_appid;
    access_token = authorization_info.authorizer_access_token;
    let expires_in = authorization_info.expires_in;
    let refresh_token = authorization_info.authorizer_refresh_token;
    let func_info = JSON.stringify(authorization_info.func_info);

    let authorizers = await 授權資料表操作類.queryAuthorizerByAppID(appid);
    //存在則更新,不存在則新建表項
    if(authorizers.length == 0) {
        let authorizerResult = await 授權資料表操作類.saveAuthorizer(appid, access_token, refresh_token, expires_in, func_info);
    } else {
        let authorizerResult = await 授權資料表操作類.updateAuthorizer(appid, access_token, refresh_token, expires_in, func_info);
    }

    return authorization_info;
}

module.exports = {
    getAuthorizeUrl : getAuthorizeUrl,
    queryAuthorizeInfo : queryAuthorizeInfo
}

同component_access_token和pre_auth_code一樣,authorizer_access_token也需要定時重新整理,所以在refresh.js中新加一個重新整理任務

【refresh.js】

const schedule = require('node-schedule');
const http = require('./../utils/httpUtils.js');

//refresh authorizer_access_token
var refreshAuthorizerAccessToken = async (component_access_token, authorizer) => {
    let appid = authorizer.authorizer_appid;
    let authorizer_refresh_token = authorizer.authorizer_refresh_token;
    let func_info = authorizer.authorizer_func_info;

    var authorizerAccessTokenPostData = {
        component_appid : 第三方平臺appID,
        authorizer_appid : appid,
        authorizer_refresh_token : authorizer_refresh_token
    };
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/component/api_authorizer_token?component_access_token=%ACCESS_TOKEN%',
        method : 'post'
    };

    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', component_access_token);
    var authorizer_token_result = await http.doHttps_withdata(https_options, authorizerAccessTokenPostData);
    var authorizer_token_json = JSON.parse(authorizer_token_result);
    logger.debug('Refresh authorizer_access_token result: ' + authorizer_token_result);

    if(authorizer_token_json.errcode != undefined) {
        return;
    }

    let authorizer_access_token = authorizer_token_json.authorizer_access_token;
    let expires_in = authorizer_token_json.expires_in;
    authorizer_refresh_token = authorizer_token_json.authorizer_refresh_token;
    var authorizerResult = await 資料表authorizer操作類.updateAuthorizer(appid, authorizer_access_token, authorizer_refresh_token, expires_in, func_info);
    return authorizerResult;
}

//refresh authorizer_access_tokens
var refreshAuthorizerAccessTokens = async function() {
    var component = await 資料表component操作類.queryComponent();
    var component_access_token = component.component_access_token;

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    for(let authorizer of authorizers) {
        let authorizerResult = await refreshAuthorizerAccessToken(component_access_token, authorizer);
    }
}

//refresh authorizer_access_token(s) every hour
var refreshAuthorizerAccessTokensJob = schedule.scheduleJob('0 0 */1 * * *', refreshAuthorizerAccessTokens);

5. 介面測試

使用獲取到的authorizer_access_token 測試各個授權介面呼叫情況

【unitTest.js】

//本部分內容用於測試使用
const mysql = require('../utils/mysqlUtil.js');
const config = require('./../../config/config.local.js');
const 資料表authorizer操作類 = require('./../dao/資料表authorizer操作類.js');
const http = require('./../utils/httpUtils.js');
const wechatCrypto = require('./../utils/cryptoUtil.js');

var testMenuCreate = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/menu/create?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };
    var button = [{
        type:'view',
        name:'百度',
        url:'http://www.baidu.com/'
    }];

    var postData = {
        button : button
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = menuResult;
}

var testMenuGet = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/menu/get?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttps(https_options);

    ctx.response.body = menuResult;
}

var testMenuDelete = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/menu/delete?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttps(https_options);

    ctx.response.body = menuResult;
}

var testSelfMenuGet = async (ctx, next) => {
    var http_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/get_current_selfmenu_info?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttp(http_options);

    ctx.response.body = menuResult;
}

var testConditionalMenuCreate = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/menu/addconditional?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };
    var button = [{
        type:'view',
        name:'soso',
        url:'http://www.soso.com/'
    }];

    var postData = {
        button : button,
        matchrule:{
            tag_id : '130'
        }
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = menuResult;
}

var testConditionalMenuDelete = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/menu/delconditional?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };
    var postData = {
        menuid : '417320051'
    }
    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    var menuResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = menuResult;
}

var testMaterialGetApi = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/material/batchget_material?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };
    var postData = {
        type : 'image',
        offset : 0,
        count : 30
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(https_options.path);
    var materialResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = materialResult;
}

var testMaterialCount = async (ctx, next) => {
    var http_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/material/get_materialcount?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(http_options.path);
    var materialResult = await http.doHttps(http_options);

    ctx.response.body = materialResult;
}

var testCurrentAutoReplyInfo = async (ctx, next) => {
    var http_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/get_current_autoreply_info?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(http_options.path);
    var currentAutoReplyInfoResult = await http.doHttps(http_options);

    ctx.response.body = currentAutoReplyInfoResult;
}

var testTagGet = async (ctx, next) => {
    var http_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/tags/get?access_token=%ACCESS_TOKEN%',
        method : 'GET'
    };

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(http_options.path);
    var tagResult = await http.doHttps(http_options);

    ctx.response.body = tagResult;
}

var testUserTagGet = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/user/tag/get?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    };

    var postData = {
        tagid : 130
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(https_options.path);
    var tagResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = tagResult;
}

var testTagCreate = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/tags/create?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    }

    var postData = {
        tag : {
            name : 'china'
        }
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(https_options.path);
    var tagResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = tagResult;
}

var testTagUpdate = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/tags/update?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    }

    var postData = {
        tag : {
            id : 131,
            name : 'English'
        }
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(https_options.path);
    var tagResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = tagResult;
}

var testTagDelete = async (ctx, next) => {
    var https_options = {
        hostname : 'api.weixin.qq.com',
        path : '/cgi-bin/tags/delete?access_token=%ACCESS_TOKEN%',
        method : 'POST'
    }

    var postData = {
        tag : {
            id : 131
        }
    }

    var authorizers = await 資料表authorizer操作類.queryAuthorizer();
    https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
    console.log(https_options.path);
    var tagResult = await http.doHttps_withdata(https_options, postData);

    ctx.response.body = tagResult;
}


var testMsgDecrypt = async (ctx, next) => {
    let encryptMessage = '0yGuDILyxl2GQeyIyGwDyN0JR5CBEctVkZAMyxbsCy' +
        '+ZM9eVEZCf+erMzZhh0tC8Ucc4KcUtzXye4DeJ6vLgCX/gUEKWsmGOMXqO5k7kYYNpFpEzAtXWxFjqtr' +
        'TbYs27gkoUefUoRvEOke5aTHxQeYwvQoPg8GLKudFehlhsSvii0J90C2cgq4HW5tYTfSAxEIEK8wV37s' +
        'tf13JRfCTQWsutpKg1G3Z/Rg1dad/8bLgB+jOeY3NDSPgfkfjn5e7oq7vz/WqQqpmLIOi3zyvahsO6Od' +
        'v0VGPo0eaYSpy0Rz1Kh94sunav+XxRAdrAPe1YrGpOX9XTMtBH36bFPrTX82kmjT6/AOYUNUIISVxYaq' +
        'ESiIMdB/Fx+3Fn0vX9jp6GIQaRh4mWcvhZFr1aO6E/xNXWsxWN/CjB43ST3FuEk6R9f5+RaOJ8lamhw6' +
        'bGTuN7BaTU0qtWgHovtgEEvSZMfw==';
    let cryptor = new wechatCrypto(config.validate.token, config.validate.encodingAESKey, config.system.appID);
    let decryptMessage = cryptor.decrypt(encryptMessage);
    console.log(decryptMessage);
    ctx.response.body = decryptMessage;
}

module.exports = {
    'GET /testMenuCreate' : testMenuCreate,
    'GET /testMenuGet' : testMenuGet,
    'GET /testMenuDelete' : testMenuDelete,
    'GET /testSelfMenuGet' : testSelfMenuGet,
    'GET /testConditionalMenuCreate' : testConditionalMenuCreate,
    'GET /testConditionalMenuDelete' : testConditionalMenuDelete,
    'GET /testMaterialGetApi' : testMaterialGetApi,
    'GET /testMaterialCount' : testMaterialCount,
    'GET /testCurrentAutoReplyInfo' : testCurrentAutoReplyInfo,
    'GET /testTagCreate' : testTagCreate,
    'GET /testTagUpdate' : testTagUpdate,
    'GET /testTagDelete' : testTagDelete,
    'GET /testTagGet' : testTagGet,
    'GET /testUserTagGet' : testUserTagGet,
    'GET /testMsgDecrypt' : testMsgDecrypt
};

6. 工具類

加解密工具【cryptoUtil.js】

var crypto = require('crypto');

/**
 * 提供基於PKCS7演算法的加解密介面
 *
 */
var PKCS7Encoder = {};

/**
 * 刪除解密後明文的補位字元
 *
 * @param {String} text 解密後的明文
 */
PKCS7Encoder.decode = function (text) {
    var pad = text[text.length - 1];

    if (pad < 1 || pad > 32) {
        pad = 0;
    }

    return text.slice(0, text.length - pad);
};

/**
 * 對需要加密的明文進行填充補位
 *
 * @param {String} text 需要進行填充補位操作的明文
 */
PKCS7Encoder.encode = function (text) {
    var blockSize = 32;
    var textLength = text.length;
    //計算需要填充的位數
    var amountToPad = blockSize - (textLength % blockSize);

    var result = new Buffer(amountToPad);
    result.fill(amountToPad);

    return Buffer.concat([text, result]);
};

/**
 * 微信企業平臺加解密資訊建構函式
 *
 * @param {String} token          公眾平臺上,開發者設定的Token
 * @param {String} encodingAESKey 公眾平臺上,開發者設定的EncodingAESKey
 * @param {String} id         企業號的CorpId或者AppId
 */
var wechatCrypto = function (token, encodingAESKey, id) {
    if (!token || !encodingAESKey || !id) {
        throw new Error('please check arguments');
    }
    this.token = token;
    this.id = id;
    var AESKey = new Buffer(encodingAESKey + '=', 'base64');
    if (AESKey.length !== 32) {
        throw new Error('encodingAESKey invalid');
    }
    this.key = AESKey;
    this.iv = AESKey.slice(0, 16);
};

/**
 * 獲取簽名
 *
 * @param {String} timestamp    時間戳
 * @param {String} nonce        隨機數
 * @param {String} encrypt      加密後的文字
 */
wechatCrypto.prototype.getSignature = function(timestamp, nonce, encrypt) {
    var shasum = crypto.createHash('sha1');
    var arr = [this.token, timestamp, nonce, encrypt].sort();
    shasum.update(arr.join(''));

    return shasum.digest('hex');
};

/**
 * 對密文進行解密
 *
 * @param {String} text 待解密的密文
 */
wechatCrypto.prototype.decrypt = function(text) {
    // 建立解密物件,AES採用CBC模式,資料採用PKCS#7填充;IV初始向量大小為16位元組,取AESKey前16位元組
    var decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv);
    decipher.setAutoPadding(false);
    var deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()]);

    deciphered = PKCS7Encoder.decode(deciphered);
    // 演算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
    // 去除16位隨機數
    var content = deciphered.slice(16);
    var length = content.slice(0, 4).readUInt32BE(0);

    return {
        message: content.slice(4, length + 4).toString(),
        id: content.slice(length + 4).toString()
    };
};

/**
 * 對明文進行加密
 *
 * @param {String} text 待加密的明文
 */
wechatCrypto.prototype.encrypt = function (text) {
    // 演算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
    // 獲取16B的隨機字串
    var randomString = crypto.pseudoRandomBytes(16);

    var msg = new Buffer(text);

    // 獲取4B的內容長度的網路位元組序
    var msgLength = new Buffer(4);
    msgLength.writeUInt32BE(msg.length, 0);

    var id = new Buffer(this.id);

    var bufMsg = Buffer.concat([randomString, msgLength, msg, id]);

    // 對明文進行補位操作
    var encoded = PKCS7Encoder.encode(bufMsg);

    // 建立加密物件,AES採用CBC模式,資料採用PKCS#7填充;IV初始向量大小為16位元組,取AESKey前16位元組
    var cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv);
    cipher.setAutoPadding(false);

    var cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()]);

    // 返回加密資料的base64編碼
    return cipheredMsg.toString('base64');
};

module.exports = wechatCrypto;

xml解析工具【xmlUtil.js】

var formatMessage = result => {
    var message = {};
    if (typeof result === 'object') {
        for (var key in result) {
            if (!(result[key] instanceof Array) || result[key].length === 0) {
                continue;
            }
            if (result[key].length === 1) {
                var val = result[key][0];
                if (typeof val === 'object') {
                    message[key] = formatMessage(val);
                } else {
                    message[key] = (val || '').trim();
                }
            } else {
                message[key] = [];
                result[key].forEach(function (item) {
                    message[key].push(formatMessage(item));
                });
            }
        }
        return message;
    } else {
        return result;
    }
};

module.exports = {
    formatMessage : formatMessage
};

資料庫工具【mysqlUtil.js】

const mysql = require('mysql');
const config = require('./../../config/config.local.js');

var connectionPool = mysql.createPool({
    'host' : config.database.host,
    'port':config.database.port,
    'user' : config.database.user,
    'password' : config.database.password,
    'database' : config.database.database,
    'charset': config.database.charset,
    'connectionLimit': config.database.connectionLimit,
    'supportBigNumbers': true,
    'bigNumberStrings': true
});

var release = connection => {
    connection.end(function(error) {
        if(error) {
            console.log('Connection closed failed.');
        } else {
            console.log('Connection closed succeeded.');
        }
    });
};

var execQuery = sqlOptions => {
    var results = new Promise((resolve, reject) => {
            connectionPool.getConnection((error,connection) => {
            if(error) {
                console.log("Get connection from mysql pool failed !");
                throw error;
            }

            var sql = sqlOptions['sql'];
            var args = sqlOptions['args'];

            if(!args) {
                var query = connection.query(sql, (error, results) => {
                    if(error) {
                        console.log('Execute query error !');
                        throw error;
                    }

                    resolve(results);
                });
            } else {
                var query = connection.query(sql, args, function(error, results) {
                    if(error) {
                        console.log('Execute query error !');
                        throw error;
                    }

                    resolve(results);
                });
            }

            connection.release(function(error) {
                if(error) {
                    console.log('Mysql connection close failed !');
                    throw error;
                }
            });
        });
    }).then(function (chunk) {
        return chunk;
    });

    return results;
};

module.exports = {
    release : release,
    execQuery : execQuery
}

http工具【httpUtils.js】

const http = require('http');
const https = require('https');
var querystring=require('querystring');

//傳送HTTP請求獲取資料方法
var doHttp_withdata = (http_options, data) => {
    var message = new Promise(function(resolve, reject) {
        var postData = JSON.stringify(data);
        http_options.headers = {
            "Content-Type": 'application/json, charset=UTF-8',
            "Content-Length": new Buffer(postData).length
        };
        var req = http.request(http_options, function(res) {
            var data = '';
            res.setEncoding('utf8');
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });

        req.on('error', function(e) {
            console.log('problem with request: ' + e.message);
        });

        req.write(postData + "\n");
        req.end();
    }).then(function (data) {
        return data;
    });

    return message;
};

//傳送HTTP請求獲取資料方法
var doHttp = http_options => {
    var message = new Promise(function(resolve, reject) {
        var req = http.request(http_options, function(res) {
            var data = '';
            res.setEncoding('utf8');
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });

        req.on('error'