├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | example 5 | .DS_Store 6 | coverage 7 | *.sublime* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wechat corp service callback 2 | ======================================= 3 | 4 | 微信公共平台企业号版(第三方企业套件)SDK-回调接口 5 | 6 | 7 | ## 功能说明 8 | 9 | 用来接收企业第三方应用套件发送过来的回调消息。 10 | 11 | ## 安装方法 12 | 13 | ```sh 14 | $ npm install wechat-corp-service-callback 15 | ``` 16 | 17 | ## 使用方法 18 | 19 | ### 前提 20 | 21 | 首先,你要有一个企业号。 22 | 然后,你要申请成为第三方企业套件的供应商。 23 | 接下来才可以创建套件,并且设置套件应用。 24 | 25 | ### 用法 26 | 27 | 其中的token,encodingAESKey,suite_id可以在套件的信息配置界面获取。 28 | 29 | ```js 30 | var wechat_cs = require('wechat-corp-service-callback'); 31 | 32 | var app_suite = function(req, res, next) { 33 | var _config = { 34 | token: sc.token, 35 | encodingAESKey: sc.encodingAESKey, 36 | suiteid: sc.suite_id, 37 | }; 38 | var _route = function(message, req, res, next) { 39 | 40 | if (message.InfoType == 'suite_ticket') { //微信服务器发过来的票,每10分钟发一次 41 | //更新到数据库 42 | var suite_ticket = message.SuiteTicket; 43 | var suite_ticket_tm = new Date(parseInt(message.TimeStamp) * 1000); 44 | //将最新的ticket放到数据库中, 调用用户自己定义的 save_ticket(callback) 方法。 45 | save_ticket(function(err, ret) { 46 | res.reply('success'); 47 | }); 48 | } else if (message.InfoType == 'change_auth') { //变更授权的通知 49 | //更新到数据库 50 | res.reply('success'); 51 | 52 | } else if (message.InfoType == 'cancel_auth') { //取消授权的通知 53 | //更新到数据库 54 | res.reply('success'); 55 | } else { 56 | res.reply('success'); 57 | }; 58 | } 59 | if (req.method == 'POST') { 60 | wechat_cs(_config, _route)(req, res, next); 61 | } else if (req.method == 'GET') { 62 | res.send('这个接口不适合GET'); 63 | }; 64 | } 65 | 66 | app.get(__base_path + '/app_suite_callback', app_suite); 67 | app.post(__base_path + '/app_suite_callback', app_suite); 68 | ``` 69 | 70 | ## 相关文档 71 | - [微信企业号-第三方应用授权](http://qydev.weixin.qq.com/wiki/index.php?title=%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%94%E7%94%A8%E6%8E%88%E6%9D%83) 72 | 73 | 74 | ## License 75 | The MIT license. 76 | 77 | ## 交流群 78 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 79 | 80 | ## 感谢 81 | 感谢以下贡献者: 82 | 83 | 84 | ## 捐赠 85 | 如果您觉得Wechat企业号版本对您有帮助,欢迎请作者一杯咖啡 86 | 87 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var xml2js = require('xml2js'); 4 | var WXBizMsgCrypt = require('wechat-crypto'); 5 | 6 | var load = function (stream, callback) { 7 | var buffers = []; 8 | stream.on('data', function (trunk) { 9 | buffers.push(trunk); 10 | }); 11 | stream.on('end', function () { 12 | callback(null, Buffer.concat(buffers)); 13 | }); 14 | stream.once('error', callback); 15 | }; 16 | 17 | /*! 18 | * 将xml2js解析出来的对象转换成直接可访问的对象 19 | */ 20 | var formatMessage = function (result) { 21 | var message = {}; 22 | if (typeof result === 'object') { 23 | for (var key in result) { 24 | if (result[key].length === 1) { 25 | var val = result[key][0]; 26 | if (typeof val === 'object') { 27 | message[key] = formatMessage(val); 28 | } else { 29 | message[key] = (val || '').trim(); 30 | } 31 | } else { 32 | message = result[key].map(formatMessage); 33 | } 34 | } 35 | } 36 | return message; 37 | }; 38 | 39 | var respond = function (handler) { 40 | return function (req, res, next) { 41 | res.reply = function () { 42 | res.writeHead(200); 43 | res.end('success'); 44 | }; 45 | 46 | handler.callback(req.suiteMessage, req, res, next); 47 | }; 48 | }; 49 | 50 | /** 51 | * 微信自动回复平台的内部的Handler对象 52 | * @param {Object} config 企业号的开发者配置对象 53 | * @param {Function} handle handle对象 54 | * 55 | * config: 56 | * ``` 57 | * { 58 | * token: '', // 公众平台上,开发者设置的Token 59 | * encodingAESKey: '', // 公众平台上,开发者设置的EncodingAESKey 60 | * suiteid: '', // 企业号的CorpId 61 | * } 62 | * ``` 63 | */ 64 | var Handler = function (config, handle) { 65 | this.config = config; 66 | this.callback = handle; 67 | }; 68 | 69 | /** 70 | * 根据Handler对象生成响应方法,并最终生成中间件函数 71 | */ 72 | Handler.prototype.middlewarify = function () { 73 | var that = this; 74 | var config = this.config; 75 | that.cryptor = new WXBizMsgCrypt(config.token, config.encodingAESKey, config.suiteid); 76 | var _respond = respond(this); 77 | 78 | return function (req, res, next) { 79 | var method = req.method; 80 | var signature = req.query.msg_signature; 81 | var timestamp = req.query.timestamp; 82 | var nonce = req.query.nonce; 83 | var cryptor = req.cryptor || that.cryptor; 84 | 85 | load(req, function (err, buf) { 86 | if (err) { 87 | return next(err); 88 | } 89 | var xml = buf.toString('utf8'); 90 | if (!xml) { 91 | var emptyErr = new Error('body is empty'); 92 | emptyErr.name = 'Wechat'; 93 | return next(emptyErr); 94 | } 95 | xml2js.parseString(xml, {trim: true}, function (err, result) { 96 | if (err) { 97 | err.name = 'BadMessage' + err.name; 98 | return next(err); 99 | } 100 | var xml = formatMessage(result.xml); 101 | var encryptMessage = xml.Encrypt; 102 | if (signature !== cryptor.getSignature(timestamp, nonce, encryptMessage)) { 103 | res.writeHead(401); 104 | res.end('Invalid signature'); 105 | return; 106 | } 107 | var decrypted = cryptor.decrypt(encryptMessage); 108 | var messageWrapXml = decrypted.message; 109 | if (messageWrapXml === '') { 110 | res.writeHead(401); 111 | res.end('Invalid suiteid'); 112 | return; 113 | } 114 | req.weixin_xml = messageWrapXml; 115 | xml2js.parseString(messageWrapXml, {trim: true}, function (err, result) { 116 | if (err) { 117 | err.name = 'BadMessage' + err.name; 118 | return next(err); 119 | } 120 | req.suiteMessage = formatMessage(result.xml); 121 | _respond(req, res, next); 122 | }); 123 | }); 124 | }); 125 | }; 126 | }; 127 | 128 | /** 129 | * 根据口令 130 | * 131 | * Examples: 132 | * 使用wechat作为自动回复中间件的三种方式 133 | * ``` 134 | * wechat(config, function (message, req, res, next) {}); 135 | * ``` 136 | * @param {Object} config 企业号的开发者配置对象 137 | * @param {Function} handle 生成的回调函数,参见示例 138 | */ 139 | var middleware = function (config, handle) { 140 | return new Handler(config, handle).middlewarify(); 141 | }; 142 | 143 | module.exports = middleware; 144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-corp-service-callback", 3 | "version": "0.0.1", 4 | "description": "A callback interface for wechat 3rd corp service.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/node-webot/wechat-corp-service-callback.git" 12 | }, 13 | "author": "NickMa", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/node-webot/wechat-corp-service-callback/issues" 17 | }, 18 | "homepage": "https://github.com/node-webot/wechat-corp-service-callback", 19 | "dependencies": { 20 | "xml2js": "0.4.4", 21 | "ejs": ">=1.0.0", 22 | "wechat-crypto": "0.0.2" 23 | }, 24 | "devDependencies": { 25 | "supertest": "*", 26 | "mocha": "*", 27 | "expect.js": "*", 28 | "connect": "2.5.*", 29 | "travis-cov": "*", 30 | "coveralls": "*", 31 | "mocha-lcov-reporter": "*", 32 | "muk": "*", 33 | "rewire": "*", 34 | "istanbul": "*" 35 | } 36 | } 37 | --------------------------------------------------------------------------------