├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── jsconfig.json ├── lib └── payment.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # webStorm 40 | .idea 41 | 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | example 5 | .DS_Store 6 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 jerrywu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # co-wechat-payment 2 | 微信支付 for node.js 3 | 4 | [![npm version](https://badge.fury.io/js/co-wechat-payment.svg)](http://badge.fury.io/js/co-wechat-payment) 5 | 6 | ## Installation 7 | ``` 8 | npm install co-wechat-payment 9 | ``` 10 | 11 | ## Usage 12 | 13 | 创建统一支付订单 14 | ```js 15 | var WXPay = require('co-wechat-payment'); 16 | 17 | var wxpay = new WXPay({ 18 | appId: 'xxxxxxxx', 19 | mchId: '1234567890', 20 | partnerKey: 'xxxxxxxxxxxxxxxxx', //微信商户平台API密钥 21 | pfx: fs.readFileSync('./wxpay_cert.p12'), //微信商户平台证书 22 | }); 23 | 24 | var result = yield wxpay.unifiedOrder({ 25 | body: 'js H5支付', 26 | out_trade_no: '20160701'+Math.random().toString().substr(2, 10), 27 | total_fee: 1, // 1分钱 28 | spbill_create_ip: '10.10.10.10', 29 | notify_url: 'http://xx.xx.xx/wxpay/notify/', 30 | trade_type: 'JSAPI', 31 | product_id: '1234567890' 32 | }); 33 | ``` 34 | 35 | 查询订单 36 | https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2 37 | ```js 38 | // 通过微信订单号查 39 | var result = yield wxpay.queryOrder({ transaction_id:"xxxxxx" }); 40 | 41 | // 通过商户订单号查 42 | var result = yield wxpay.queryOrder({ out_trade_no:"xxxxxx" }); 43 | ``` 44 | 45 | 关闭订单 46 | https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3 47 | ```js 48 | var result = yield wxpay.closeOrder({ out_trade_no:"xxxxxx"}); 49 | ``` 50 | 退款接口 51 | ```js 52 | var params = { 53 | appid: 'xxxxxxxx', 54 | mch_id: '1234567890', 55 | op_user_id: '商户号即可', 56 | out_refund_no: '20140703'+Math.random().toString().substr(2, 10), 57 | total_fee: '1', //原支付金额 58 | refund_fee: '1', //退款金额 59 | transaction_id: '微信订单号' 60 | }; 61 | 62 | var result = yield wxpay.refund(params); 63 | ``` 64 | 65 | ### 原生支付 (NATIVE) 66 | 67 | 68 | ### 公众号支付 (JS API) 69 | 70 | 生成JS API支付参数,发给页面 71 | ```js 72 | var result = yield wxpay.getBrandWCPayRequestParams({ 73 | openid: '微信用户 openid', 74 | body: '公众号支付测试', 75 | detail: '公众号支付测试', 76 | out_trade_no: '20150331'+Math.random().toString().substr(2, 10), 77 | total_fee: 1, 78 | spbill_create_ip: '192.168.2.210', 79 | notify_url: 'http://wxpay_notify_url' 80 | }); 81 | 82 | yield this.render('/wechat/pay',{payArgs:result}); 83 | ``` 84 | 85 | 网页调用参数(以ejs为例) 86 | ```js 87 | WeixinJSBridge.invoke( 88 | "getBrandWCPayRequest", <%-JSON.stringify(payArgs)%>, function(res){ 89 | if(res.err_msg == "get_brand_wcpay_request:ok" ) { 90 | // success 91 | } 92 | }); 93 | ``` 94 | 95 | 根据之前预创建订单接口返回的prepare_id参数,生成JS API支付参数,发给页面 96 | ```js 97 | var result = yield wxpay.getJsPayRequestParams('xxxxx'); 98 | 99 | yield this.render('/wechat/pay',{payArgs:result}); 100 | ``` 101 | 102 | ### 中间件 103 | 104 | 商户服务端处理微信的回调(koa为例) 105 | ```js 106 | var wechatBodyParser = require('co-wechat-body'); 107 | app.use(wechatBodyParser(options)); 108 | 109 | // 支付结果异步通知 110 | router.post('/wechat/payment/notify', wechatPayment.middleware(), function* (){ 111 | var message = this.request.body; 112 | // 处理你的订单状态更新逻辑. [warn] 注意,分布式并发情况下需要加锁处理. 113 | // do something... 114 | 115 | // 向微信返回处理成功信息 116 | this.body = 'OK'; 117 | 118 | // 如果业务逻辑处理异常, 向微信返回错误信息,微信服务器会继续通知. 119 | // this.body = new Error('server error'); 120 | }); 121 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/payment'); -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // 请访问 https://go.microsoft.com/fwlink/?LinkId=759670 3 | // 参阅有关 jsconfig.json 格式的文档 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /lib/payment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var md5 = require('md5'); 4 | var sha1 = require('sha1'); 5 | var urllib = require('urllib'); 6 | var _ = require('underscore'); 7 | var xml2js = require('xml2js'); 8 | var https = require('https'); 9 | var url_mod = require('url'); 10 | var thunkify = require('thunkify'); 11 | 12 | var signTypes = { 13 | MD5: md5, 14 | SHA1: sha1 15 | }; 16 | 17 | var RETURN_CODES = { 18 | SUCCESS: 'SUCCESS', 19 | FAIL: 'FAIL' 20 | }; 21 | 22 | var URLS = { 23 | UNIFIED_ORDER: 'https://api.mch.weixin.qq.com/pay/unifiedorder', 24 | ORDER_QUERY: 'https://api.mch.weixin.qq.com/pay/orderquery', 25 | REFUND: 'https://api.mch.weixin.qq.com/secapi/pay/refund', 26 | REFUND_QUERY: 'https://api.mch.weixin.qq.com/pay/refundquery', 27 | DOWNLOAD_BILL: 'https://api.mch.weixin.qq.com/pay/downloadbill', 28 | SHORT_URL: 'https://api.mch.weixin.qq.com/tools/shorturl', 29 | CLOSE_ORDER: 'https://api.mch.weixin.qq.com/pay/closeorder' 30 | }; 31 | 32 | var Payment = function (config) { 33 | this.appId = config.appId; 34 | this.partnerKey = config.partnerKey; 35 | this.mchId = config.mchId; 36 | this.subMchId = config.subMchId; 37 | this.notifyUrl = config.notifyUrl; 38 | this.passphrase = config.passphrase || config.mchId; 39 | this.pfx = config.pfx; 40 | return this; 41 | }; 42 | 43 | Payment.prototype.getBrandWCPayRequestParams = function* getBrandWCPayRequestParams(order) { 44 | var default_params = { 45 | appId: this.appId, 46 | timeStamp: this._generateTimeStamp(), 47 | nonceStr: this._generateNonceStr(), 48 | signType: 'MD5' 49 | }; 50 | 51 | order = this._extendWithDefault(order, [ 52 | 'notify_url' 53 | ]); 54 | 55 | var data = yield this.unifiedOrder(order); 56 | 57 | var params = _.extend(default_params, { 58 | package: 'prepay_id=' + data.prepay_id 59 | }); 60 | 61 | params.paySign = this._getSign(params); 62 | 63 | if(order.trade_type == 'NATIVE'){ 64 | params.code_url = data.code_url; 65 | } 66 | 67 | params.timestamp = params.timeStamp; 68 | delete params.timeStamp; 69 | 70 | return params; 71 | }; 72 | 73 | Payment.prototype.getJsPayRequestParams = function* getJsPayRequestParams( prepayId ) { 74 | var default_params = { 75 | appId: this.appId, 76 | timeStamp: this._generateTimeStamp(), 77 | nonceStr: this._generateNonceStr(), 78 | signType: 'MD5' 79 | }; 80 | 81 | var params = _.extend(default_params, { 82 | package: 'prepay_id=' + prepayId 83 | }); 84 | 85 | params.paySign = this._getSign(params); 86 | 87 | // see: https://mp.weixin.qq.com/wiki?action=doc&id=mp1421141115&t=0.5826723026485408&token=&lang=zh_CN#wxzf1 88 | params.timestamp = params.timeStamp; 89 | delete params.timeStamp; 90 | 91 | return params; 92 | }; 93 | 94 | /** 95 | * Generate parameters for `WeixinJSBridge.invoke('editAddress', parameters)`. 96 | * 97 | * @param {String} data.url Referer URL that call the API. *Note*: Must contain `code` and `state` in querystring. 98 | * @param {String} data.accessToken 99 | * 100 | * @see https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_9 101 | */ 102 | Payment.prototype.getEditAddressParams = function* getEditAddressParams(data) { 103 | if (!(data.url && data.accessToken)) { 104 | var err = new Error('Missing url or accessToken'); 105 | throw err; 106 | } 107 | 108 | var params = { 109 | appId: this.appId, 110 | scope: 'jsapi_address', 111 | signType: 'SHA1', 112 | timeStamp: this._generateTimeStamp(), 113 | nonceStr: this._generateNonceStr(), 114 | }; 115 | var signParams = { 116 | appid: params.appId, 117 | url: data.url, 118 | timestamp: params.timeStamp, 119 | noncestr: params.nonceStr, 120 | accesstoken: data.accessToken, 121 | }; 122 | var string = this._toQueryString(signParams); 123 | params.addrSign = signTypes[params.signType](string); 124 | 125 | return params; 126 | }; 127 | 128 | Payment.prototype._httpRequest = function(url, data){ 129 | return urllib.request(url, { 130 | method: 'POST', 131 | data: data 132 | }); 133 | }; 134 | 135 | Payment.prototype._httpsRequest = function(url, data){ 136 | var parsed_url = url_mod.parse(url); 137 | 138 | return new Promise((resolve,reject) => { 139 | var req = https.request({ 140 | host: parsed_url.host, 141 | port: 443, 142 | path: parsed_url.path, 143 | pfx: this.pfx, 144 | passphrase: this.passphrase, 145 | method: 'POST' 146 | }, function(res) { 147 | var content = ''; 148 | res.on('data', function(chunk) { 149 | content += chunk; 150 | }); 151 | res.on('end', function(){ 152 | resolve(content); 153 | }); 154 | }); 155 | 156 | req.on('error', function(e) { 157 | reject(e); 158 | }); 159 | req.write(data); 160 | req.end(); 161 | }); 162 | }; 163 | 164 | Payment.prototype._signedQuery = function* _signedQuery(url, params, options){ 165 | var self = this; 166 | var required = options.required || []; 167 | params = this._extendWithDefault(params, [ 168 | 'appid', 169 | 'mch_id', 170 | 'sub_mch_id', 171 | 'nonce_str' 172 | ]); 173 | 174 | params = _.extend({ 175 | 'sign': this._getSign(params) 176 | }, params); 177 | 178 | if(params.long_url){ 179 | params.long_url = encodeURIComponent(params.long_url); 180 | } 181 | 182 | for(var key in params){ 183 | if(params[key] !== undefined && params[key] !== null){ 184 | params[key] = params[key].toString(); 185 | } 186 | } 187 | 188 | var missing = []; 189 | required.forEach(function(key) { 190 | var alters = key.split('|'); 191 | for (var i = alters.length - 1; i >= 0; i--) { 192 | if (params[alters[i]]) { 193 | return; 194 | } 195 | } 196 | missing.push(key); 197 | }); 198 | 199 | if(missing.length){ 200 | throw new Error('missing params ' + missing.join(',')); 201 | } 202 | var request = (options.https ? this._httpsRequest : this._httpRequest).bind(this); 203 | var response = yield request(url, this.buildXml(params)); 204 | 205 | var that = this; 206 | 207 | var parseString = thunkify(xml2js.parseString); 208 | var result = yield parseString([response.data, response.headers], {explicitArray: false}); 209 | result = result.xml; 210 | var err = that.check(result); 211 | if(err){ 212 | throw err; 213 | } 214 | return result; 215 | 216 | }; 217 | 218 | Payment.prototype.unifiedOrder = function* unifiedOrder(params) { 219 | var requiredData = ['body', 'out_trade_no', 'total_fee', 'spbill_create_ip', 'trade_type']; 220 | if(params.trade_type == 'JSAPI'){ 221 | requiredData.push('openid'); 222 | }else if (params.trade_type == 'NATIVE'){ 223 | requiredData.push('product_id'); 224 | } 225 | params.notify_url = params.notify_url || this.notifyUrl; 226 | 227 | return yield this._signedQuery(URLS.UNIFIED_ORDER, params, { 228 | required:requiredData, 229 | https: true 230 | }); 231 | }; 232 | 233 | Payment.prototype.queryOrder = function* queryOrder(params){ 234 | return yield this._signedQuery(URLS.ORDER_QUERY, params, { 235 | required: ['transaction_id|out_trade_no'] 236 | }); 237 | }; 238 | 239 | Payment.prototype.refund = function* refund(params){ 240 | params = this._extendWithDefault(params, [ 241 | 'op_user_id' 242 | ]); 243 | 244 | return yield this._signedQuery(URLS.REFUND, params, { 245 | https: true, 246 | required: ['transaction_id|out_trade_no', 'out_refund_no', 'total_fee', 'refund_fee'] 247 | }); 248 | }; 249 | 250 | Payment.prototype.refundQuery = function* refundQuery(params){ 251 | return yield this._signedQuery(URLS.REFUND_QUERY, params, { 252 | required: ['transaction_id|out_trade_no|out_refund_no|refund_id'] 253 | }); 254 | }; 255 | 256 | Payment.prototype.downloadBill = function* downloadBill(params){ 257 | return yield this._signedQuery(URLS.DOWNLOAD_BILL, params, { 258 | required: ['bill_date', 'bill_type'] 259 | }); 260 | }; 261 | 262 | Payment.prototype.shortUrl = function* shortUrl(params){ 263 | return yield this._signedQuery(URLS.SHORT_URL, params, { 264 | required: ['long_url'] 265 | }); 266 | }; 267 | 268 | Payment.prototype.closeOrder = function* closeOrder(params) { 269 | return yield this._signedQuery(URLS.CLOSE_ORDER, params, { 270 | required: ['out_trade_no'] 271 | }); 272 | }; 273 | 274 | Payment.prototype.parseCsv = function(text){ 275 | var rows = text.trim().split(/\r?\n/); 276 | 277 | function toArr(rows){ 278 | var titles = rows[0].split(','); 279 | var bodys = rows.splice(1); 280 | var data = []; 281 | 282 | bodys.forEach(function(row){ 283 | var rowData = {}; 284 | row.split(',').forEach(function(cell,i){ 285 | rowData[titles[i]] = cell.split('`')[1]; 286 | }); 287 | data.push(rowData); 288 | }); 289 | return data; 290 | } 291 | 292 | return { 293 | list: toArr(rows.slice(0, rows.length - 2)), 294 | stat: toArr(rows.slice(rows.length - 2, rows.length))[0] 295 | }; 296 | }; 297 | 298 | Payment.prototype.buildXml = function (obj) { 299 | var builder = new xml2js.Builder(); 300 | var xml = builder.buildObject({xml:obj}); 301 | return xml; 302 | }; 303 | 304 | Payment.prototype.check = function ( message ) { 305 | 306 | let error = null; 307 | if (message.return_code == RETURN_CODES.FAIL) { 308 | error = new Error(message.return_msg); 309 | error.name = 'ProtocolError'; 310 | } else if (message.result_code == RETURN_CODES.FAIL) { 311 | error = new Error(message.err_code); 312 | error.name = 'BusinessError'; 313 | } else if (this.appId !== message.appid) { 314 | error = new Error(); 315 | error.name = 'InvalidAppId'; 316 | } else if (this.mchId !== message.mch_id) { 317 | error = new Error(); 318 | error.name = 'InvalidMchId'; 319 | } else if (this.subMchId && this.subMchId !== message.sub_mch_id) { 320 | error = new Error(); 321 | error.name = 'InvalidSubMchId'; 322 | } else if (this._getSign(message) !== message.sign) { 323 | error = new Error(); 324 | error.name = 'InvalidSignature'; 325 | } 326 | 327 | return error; 328 | }; 329 | 330 | 331 | /** 332 | * 使用默认值扩展对象 333 | * @param {Object} obj 334 | * @param {Array} keysNeedExtend 335 | * @return {Object} extendedObject 336 | */ 337 | Payment.prototype._extendWithDefault = function (obj, keysNeedExtend) { 338 | var defaults = { 339 | appid: this.appId, 340 | mch_id: this.mchId, 341 | sub_mch_id: this.subMchId, 342 | nonce_str: this._generateNonceStr(), 343 | notify_url: this.notifyUrl, 344 | op_user_id: this.mchId 345 | }; 346 | var extendObject = {}; 347 | keysNeedExtend.forEach(function (k) { 348 | if (defaults[k]) { 349 | extendObject[k] = defaults[k]; 350 | } 351 | }); 352 | return _.extend(extendObject, obj); 353 | }; 354 | 355 | Payment.prototype._getSign = function (pkg, signType) { 356 | pkg = _.clone(pkg); 357 | delete pkg.sign; 358 | signType = signType || 'MD5'; 359 | var string1 = this._toQueryString(pkg); 360 | var stringSignTemp = string1 + '&key=' + this.partnerKey; 361 | var signValue = signTypes[signType](stringSignTemp).toUpperCase(); 362 | return signValue; 363 | }; 364 | 365 | Payment.prototype._toQueryString = function (object) { 366 | return Object.keys(object).filter(function (key) { 367 | return object[key] !== undefined && object[key] !== ''; 368 | }).sort().map(function (key) { 369 | return key + '=' + object[key]; 370 | }).join('&'); 371 | }; 372 | 373 | Payment.prototype._generateTimeStamp = function () { 374 | return parseInt(+new Date() / 1000, 10) + ''; 375 | }; 376 | 377 | /** 378 | * [_generateNonceStr description] 379 | * @param {[type]} length [description] 380 | * @return {[type]} [description] 381 | */ 382 | Payment.prototype._generateNonceStr = function (length) { 383 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 384 | var maxPos = chars.length; 385 | var noceStr = ''; 386 | var i; 387 | for (i = 0; i < (length || 32); i++) { 388 | noceStr += chars.charAt(Math.floor(Math.random() * maxPos)); 389 | } 390 | return noceStr; 391 | }; 392 | 393 | /** 394 | * Pay notify middleware for koa. 395 | * 396 | * first use co-wechat-body parse xml body 397 | * 398 | * ``` 399 | * var wechatBodyParser = require('co-wechat-body'); 400 | * app.use(wechatBodyParser(options)) 401 | * ``` 402 | * 403 | * then by koa-router: 404 | * 405 | * ``` 406 | * router.post('/wechat/payment/notify', wechatPayment.middleware(), payNotifyChangeOrderStatus); 407 | * ``` 408 | */ 409 | Payment.prototype.middleware = function middleware() { 410 | var self = this; 411 | 412 | function success(){ 413 | this.body = self.buildXml({ 414 | return_code: 'SUCCESS', 415 | return_msg: 'OK' 416 | }); 417 | } 418 | 419 | function fail(err) { 420 | if(typeof err === 'string'){ 421 | err = new Error(err); 422 | } 423 | 424 | this.body = self.buildXml({ 425 | return_code: 'FAIL', 426 | return_msg: err.message || err.name || '' 427 | }); 428 | } 429 | 430 | return function* wechatPayNotify(next) { 431 | // 这里面的this指针指向的是 koa context 432 | if (this.method !== 'POST') { 433 | return fail.call(this, 'NotImplemented'); 434 | } 435 | 436 | // through co-wechat-body parse middleware 437 | var body = this.request.body; 438 | 439 | if(!body){ 440 | return fail.call(this, 'Invalid body'); 441 | } 442 | 443 | var err = self.check(body); 444 | if(err){ 445 | return fail.call(this, err); 446 | } 447 | 448 | this.reply = (data) => { 449 | if(data instanceof Error){ 450 | fail.call(this, data); 451 | }else{ 452 | success.call(this, data); 453 | } 454 | }; 455 | 456 | // order pay status deal by yourself. 457 | yield next; 458 | } 459 | } 460 | 461 | module.exports = Payment; 462 | 463 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "co-wechat-payment", 3 | "version": "0.2.0", 4 | "description": "Wechat pay lib for koa", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/perzy/co-wechat-payment.git" 12 | }, 13 | "keywords": [ 14 | "wechat", 15 | "pay", 16 | "co", 17 | "jssdk" 18 | ], 19 | "author": "jerrywu", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/perzy/co-wechat-payment/issues" 23 | }, 24 | "homepage": "https://github.com/perzy/co-wechat-payment#readme", 25 | "dependencies": { 26 | "md5": "^2.1.0", 27 | "sha1": "^1.1.1", 28 | "thunkify": "^2.1.2", 29 | "underscore": "^1.8.3", 30 | "urllib": "^2.11.0", 31 | "xml2js": "^0.4.17" 32 | } 33 | } 34 | --------------------------------------------------------------------------------