├── .gitignore ├── .npmignore ├── README.md ├── index.js ├── lib ├── aes.js ├── middleware.js ├── parseXml.js └── payment.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test.js 3 | node_modules 4 | example 5 | *.p12 6 | *.pem 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | example 5 | .DS_Store 6 | coverage 7 | *.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信支付 for Nodejs 2 | 3 | ## 初始化 4 | ```js 5 | var Payment = require('wechat-pay').Payment; 6 | var initConfig = { 7 | partnerKey: "", 8 | appId: "", 9 | mchId: "", 10 | notifyUrl: "", 11 | pfx: fs.readFileSync("") 12 | }; 13 | var payment = new Payment(initConfig); 14 | ``` 15 | 所有参数都不是必须的,不过这样配置最省事。实际调用时候的参数若有同名会覆盖。 16 | 17 | ## 付个钱 18 | ```javascript 19 | var order = { 20 | body: '吮指原味鸡 * 1', 21 | attach: '{"部位":"三角"}', 22 | out_trade_no: 'kfc' + (+new Date), 23 | total_fee: 10 * 100, 24 | spbill_create_ip: req.ip, 25 | openid: req.user.openid, 26 | trade_type: 'JSAPI' 27 | }; 28 | 29 | payment.getBrandWCPayRequestParams(order, function(err, payargs){ 30 | res.json(payargs); 31 | }); 32 | 33 | // 也可以使用`async/await`形式 34 | // let payargs = await payment.getBrandWCPayRequestParams(order) 35 | ``` 36 | 37 | 注: 38 | 1. 页面的路径需要位于`支付授权目录`下 39 | 2. 由于每次呼出支付界面,无论用户是否支付成功,out_trade_no 都会失效(OUT_TRADE_NO_USED),所以这里使用timestamp保证每次的id不同。业务逻辑中应该自行维护之 40 | 41 | 42 | 前端通过 43 | 44 | ```javascript 45 | WeixinJSBridge.invoke('getBrandWCPayRequest', payargs, function(res){ 46 | if(res.err_msg == "get_brand_wcpay_request:ok"){ 47 | alert("支付成功"); 48 | // 这里可以跳转到订单完成页面向用户展示 49 | }else{ 50 | alert("支付失败,请重试"); 51 | } 52 | }); 53 | ``` 54 | 来呼出微信的支付界面 55 | 56 | ## 接收微信付款确认请求 57 | ```javascript 58 | var middleware = require('wechat-pay').middleware; 59 | app.use('', middleware(initConfig).getNotify().done(function(message, req, res, next) { 60 | var openid = message.openid; 61 | var order_id = message.out_trade_no; 62 | var attach = {}; 63 | try{ 64 | attach = JSON.parse(message.attach); 65 | }catch(e){} 66 | 67 | /** 68 | * 查询订单,在自己系统里把订单标为已处理 69 | * 如果订单之前已经处理过了直接返回成功 70 | */ 71 | res.reply('success'); 72 | 73 | /** 74 | * 有错误返回错误,不然微信会在一段时间里以一定频次请求你 75 | * res.reply(new Error('...')) 76 | */ 77 | })); 78 | ``` 79 | 80 | ## 退个款 81 | 82 | ```javascript 83 | payment.refund({ 84 | out_trade_no: "kfc001", 85 | out_refund_no: 'kfc001_refund', 86 | total_fee: 10 * 100, 87 | refund_fee: 10 * 100 88 | }, function(err, result){ 89 | /** 90 | * 微信收到正确的请求后会给用户退款提醒 91 | * 这里一般不用处理,有需要的话有err的时候记录一下以便排查 92 | */ 93 | }); 94 | ``` 95 | 96 | ### 接收退款确认请求 97 | 98 | ```javascript 99 | var middleware = require('wechat-pay').middleware; 100 | app.use('', middleware(initConfig).getRefundNotify().done(function(message, req, res, next) { 101 | var openid = message.openid; 102 | var refund_order_id = message.out_refund_no; 103 | var order_id = message.out_trade_no; 104 | var attach = {}; 105 | try{ 106 | attach = JSON.parse(message.attach); 107 | }catch(e){} 108 | 109 | /** 110 | * 查询订单,在自己系统里把订单标为已处理 111 | * 如果订单之前已经处理过了直接返回成功 112 | */ 113 | res.reply('success'); 114 | 115 | /** 116 | * 有错误返回错误,不然微信会在一段时间里以一定频次请求你 117 | * res.reply(new Error('...')) 118 | */ 119 | })); 120 | ``` 121 | 122 | ## 发红包 123 | 124 | ```javascript 125 | payment.sendRedPacket({ 126 | mch_billno: 'kfc002', 127 | send_name: '肯德基', 128 | re_openid: '', 129 | total_amount: 10 * 100, 130 | total_num: 1, 131 | wishing: '祝多多吃鸡', 132 | client_ip: '', 133 | act_name: '吃鸡大奖赛', 134 | remark: '记得吐骨头', 135 | scene_id: 'PRODUCT_1' 136 | }, (err, result) => { 137 | /** 138 | * 微信收到正确的请求后会给用户发红包,用户不必关注公众号也能收到。 139 | * 红包没有通知回调,有需要的话标记订单状态,和有err的时候记录一下以便排查 140 | */ 141 | }); 142 | }); 143 | ``` 144 | 145 | ### 查询红包状态 146 | 147 | ```javascript 148 | payment.redPacketQuery({ 149 | mch_billno: 'kfc002' 150 | }, (err, result) => { 151 | /** 152 | * 根据状态相应处理订单 153 | */ 154 | }); 155 | ``` 156 | 157 | ## 企业付款 158 | 159 | ```javascript 160 | payment.transfers({ 161 | partner_trade_no: 'kfc003', 162 | openid: '', 163 | check_name: 'NO_CHECK', 164 | amount: 10 * 100, 165 | desc: '', 166 | spbill_create_ip: '' 167 | }, (err, result) => { 168 | // 根据微信文档,当返回错误码为“SYSTEMERROR”时,一定要使用原单号重试,否则可能造成重复支付等资金风险。 169 | }); 170 | ``` 171 | 172 | ## 查询历史订单 173 | 174 | ```javascript 175 | payment.downloadBill({ 176 | bill_date: "20140913", 177 | bill_type: "ALL" 178 | }, function(err, data){ 179 | // 账单列表 180 | var list = data.list; 181 | // 账单统计信息 182 | var stat = data.stat; 183 | }); 184 | ``` 185 | 186 | ## 错误处理 187 | 188 | 在回调的Error上的以name做了区分,有需要可以拿来做判断 189 | 190 | * ProtocolError 协议错误,看看有没有必须要传的参数没传 191 | * BusinessError 业务错误,可以从返回的data里面看看错误细节 192 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.Payment = require('./lib/payment').Payment; 2 | exports.middleware = require('./lib/middleware'); 3 | -------------------------------------------------------------------------------- /lib/aes.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | function AES(key, algorithm) { 4 | if (!this instanceof AES) { 5 | return new AES(); 6 | } 7 | 8 | var iv = algorithm.indexOf('ecb') > -1 ? '' : key; 9 | this.cipher = crypto.createCipheriv(algorithm, key, iv); 10 | this.decipher = crypto.createDecipheriv(algorithm, key, iv); 11 | this.decipher.setAutoPadding(false); 12 | 13 | return this; 14 | } 15 | 16 | AES.prototype.decode = function(str, inputEncoding, outputEncoding) { 17 | inputEncoding = inputEncoding || 'base64'; 18 | outputEncoding = outputEncoding || 'utf8'; 19 | 20 | var decipherChunks = []; 21 | decipherChunks.push(this.decipher.update(str, inputEncoding, outputEncoding)); 22 | decipherChunks.push(this.decipher.final(outputEncoding)); 23 | return decipherChunks.join(''); 24 | } 25 | 26 | module.exports = AES; 27 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | var Payment = require('./payment').Payment; 2 | var AES = require('./aes'); 3 | var parseXml = require('./parseXml'); 4 | var md5 = require('md5'); 5 | var util = require('util'); 6 | 7 | var getRawBody = function (req, callback) { 8 | if (req.rawBody) { 9 | return callback(null, req.rawBody); 10 | } 11 | 12 | var data=''; 13 | req.setEncoding('utf8'); 14 | req.on('data', function(chunk) { 15 | data += chunk; 16 | }); 17 | 18 | req.on('end', function() { 19 | req.rawBody = data; 20 | callback(null, data); 21 | }); 22 | }; 23 | 24 | /** 25 | * 中间件基础类 26 | * @class Basic 27 | * @constructor 28 | * @param {String} partnerKey 29 | * @param {String} appId 30 | * @param {String} mchId 31 | * @param {String} notifyUrl 32 | * @param {String} pfx appkey 33 | * @chainable 34 | */ 35 | function Basic(config){ 36 | this.payment = new Payment(config); 37 | return this; 38 | } 39 | 40 | /** 41 | * 完成中间件配置,并返回中间件 42 | * @method done 43 | * @for Basic 44 | * @chainable 45 | * @param {Function} [handler] 默认处理方法 46 | */ 47 | Basic.prototype.done = function (handler) { 48 | var self = this; 49 | var payment = self.payment; 50 | return function (req, res, next) { 51 | if (req.method !== 'POST') { 52 | var error = new Error(); 53 | error.name = 'NotImplemented'; 54 | return self.fail(error, res); 55 | } 56 | getRawBody(req, function (err, rawBody) { 57 | if (err) { 58 | err.name = 'BadMessage' + err.name; 59 | return self.fail(err, res); 60 | } 61 | payment.validate(rawBody, function(err, message){ 62 | res.reply = function(data){ 63 | if(data instanceof Error){ 64 | self.fail(data, res); 65 | }else{ 66 | self.success(data, res); 67 | } 68 | }; 69 | 70 | if(err){ 71 | return self.fail(err, res); 72 | } 73 | 74 | handler(message, req, res, next); 75 | }); 76 | }); 77 | }; 78 | }; 79 | 80 | 81 | Basic.prototype.success = function(result, res){ 82 | return res.end(this.payment.buildXml({ 83 | return_code: 'SUCCESS' 84 | })); 85 | }; 86 | 87 | Basic.prototype.fail = function(err, res){ 88 | return res.end(this.payment.buildXml({ 89 | return_code: 'FAIL', 90 | return_msg: err.name 91 | })); 92 | }; 93 | 94 | function Notify(config){ 95 | if (!(this instanceof Notify)) { 96 | return new Notify(config); 97 | } 98 | Basic.call(this,config); 99 | return this; 100 | } 101 | 102 | util.inherits(Notify, Basic); 103 | 104 | /** 105 | * 中间件基础类 106 | * @class Refund 107 | * @constructor 108 | * @param {String} partnerKey 109 | * @param {String} appId 110 | * @param {String} mchId 111 | * @param {String} notifyUrl 112 | * @param {String} pfx appkey 113 | * @chainable 114 | */ 115 | function Refund(config){ 116 | this.key = md5(config.partnerKey).toLowerCase(); 117 | this.payment = new Payment(config); 118 | return this; 119 | } 120 | 121 | /** 122 | * 完成中间件配置,并返回中间件 123 | * @method done 124 | * @for Refund 125 | * @chainable 126 | * @param {Function} [handler] 默认处理方法 127 | */ 128 | Refund.prototype.done = function (handler) { 129 | var self = this; 130 | var payment = self.payment; 131 | var key = self.key; 132 | 133 | return function (req, res, next) { 134 | if (req.method !== 'POST') { 135 | var error = new Error(); 136 | error.name = 'NotImplemented'; 137 | return self.fail(error, res); 138 | } 139 | getRawBody(req, function (err, rawBody) { 140 | if (err) { 141 | err.name = 'BadMessage' + err.name; 142 | return self.fail(err, res); 143 | } 144 | 145 | payment.validate(rawBody, function(err, message){ 146 | res.reply = function(data){ 147 | if(data instanceof Error){ 148 | self.fail(data, res); 149 | }else{ 150 | self.success(data, res); 151 | } 152 | }; 153 | 154 | if(err){ 155 | return self.fail(err, res); 156 | } 157 | 158 | var refundResXml; 159 | try { 160 | var aes = new AES(key, 'aes-256-ecb'); 161 | refundResXml = aes.decode(message.req_info); 162 | } catch (e) { 163 | return self.fail(e, res); 164 | } 165 | 166 | try { 167 | parseXml(refundResXml, function(e, refundRes) { 168 | if (e) { 169 | return self.fail(e, res); 170 | } 171 | 172 | handler(refundRes, req, res, next); 173 | }); 174 | } catch (e) { 175 | return self.fail(e, res); 176 | } 177 | }); 178 | }); 179 | }; 180 | }; 181 | function RefundNotify(config){ 182 | if (!(this instanceof RefundNotify)) { 183 | return new RefundNotify(config); 184 | } 185 | Refund.call(this,config); 186 | 187 | return this; 188 | } 189 | 190 | util.inherits(Refund, Basic); 191 | util.inherits(RefundNotify, Refund); 192 | 193 | 194 | var middleware = function (config) { 195 | return { 196 | getNotify: function () { 197 | return new Notify(config); 198 | }, 199 | getRefundNotify: function () { 200 | return new RefundNotify(config); 201 | } 202 | }; 203 | }; 204 | 205 | middleware.Notify = Notify; 206 | 207 | module.exports = middleware; 208 | -------------------------------------------------------------------------------- /lib/parseXml.js: -------------------------------------------------------------------------------- 1 | var xml2js = require('xml2js'); 2 | 3 | function parseXml(xml, callback) { 4 | xml2js.parseString(xml, { 5 | trim: true, 6 | explicitArray: false 7 | }, function(err, json) { 8 | var error = null, 9 | data; 10 | if (err) { 11 | error = new Error(); 12 | err.name = 'XMLParseError'; 13 | return callback(err, xml); 14 | } 15 | 16 | data = json ? (json.xml || json.root) : {}; 17 | 18 | callback(error, data); 19 | }); 20 | } 21 | 22 | module.exports = parseXml; 23 | -------------------------------------------------------------------------------- /lib/payment.js: -------------------------------------------------------------------------------- 1 | var md5 = require('md5'); 2 | var sha1 = require('sha1'); 3 | var request = require('request'); 4 | var _ = require('underscore'); 5 | var xml2js = require('xml2js'); 6 | var https = require('https'); 7 | var url_mod = require('url'); 8 | 9 | var signTypes = { 10 | MD5: md5, 11 | SHA1: sha1 12 | }; 13 | 14 | var RETURN_CODES = { 15 | SUCCESS: 'SUCCESS', 16 | FAIL: 'FAIL' 17 | }; 18 | 19 | var URLS = { 20 | UNIFIED_ORDER: 'https://api.mch.weixin.qq.com/pay/unifiedorder', 21 | ORDER_QUERY: 'https://api.mch.weixin.qq.com/pay/orderquery', 22 | REFUND: 'https://api.mch.weixin.qq.com/secapi/pay/refund', 23 | REFUND_QUERY: 'https://api.mch.weixin.qq.com/pay/refundquery', 24 | DOWNLOAD_BILL: 'https://api.mch.weixin.qq.com/pay/downloadbill', 25 | SHORT_URL: 'https://api.mch.weixin.qq.com/tools/shorturl', 26 | CLOSE_ORDER: 'https://api.mch.weixin.qq.com/pay/closeorder', 27 | REDPACK_SEND: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack', 28 | REDPACK_QUERY: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo', 29 | TRANSFERS: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', 30 | TRANSFERS_QUERY: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo', 31 | }; 32 | 33 | var Payment = function(config) { 34 | this.appId = config.appId; 35 | this.partnerKey = config.partnerKey; 36 | this.mchId = config.mchId; 37 | this.subMchId = config.subMchId; 38 | this.notifyUrl = config.notifyUrl; 39 | this.passphrase = config.passphrase || config.mchId; 40 | this.pfx = config.pfx; 41 | return this; 42 | }; 43 | 44 | Payment.prototype.getBrandWCPayRequestParams = function(order, callback) { 45 | var self = this; 46 | var default_params = { 47 | appId: this.appId, 48 | timeStamp: this._generateTimeStamp(), 49 | nonceStr: this._generateNonceStr(), 50 | signType: 'MD5' 51 | }; 52 | 53 | order = this._extendWithDefault(order, [ 54 | 'notify_url' 55 | ]); 56 | 57 | this.unifiedOrder(order, function(err, data) { 58 | if (err) { 59 | return callback(err); 60 | } 61 | 62 | var params = _.extend(default_params, { 63 | package: 'prepay_id=' + data.prepay_id 64 | }); 65 | 66 | params.paySign = self._getSign(params); 67 | 68 | if (order.trade_type == 'NATIVE') { 69 | params.code_url = data.code_url; 70 | }else if(order.trade_type == 'MWEB'){ 71 | params.mweb_url = data.mweb_url; 72 | } 73 | 74 | params.timestamp = params.timeStamp; 75 | 76 | callback(null, params); 77 | }); 78 | }; 79 | 80 | Payment.prototype.sendRedPacket = function(order, callback) { 81 | var self = this; 82 | var default_params = { 83 | wxappid: this.appId 84 | }; 85 | 86 | order = _.extend(order, default_params); 87 | 88 | var requiredData = ['mch_billno', 'send_name', 're_openid', 'total_amount', 'total_num', 'wishing', 'client_ip', 'act_name', 'remark']; 89 | 90 | this._signedQuery(URLS.REDPACK_SEND, order, { 91 | https: true, 92 | required: requiredData 93 | }, callback); 94 | }; 95 | 96 | Payment.prototype.redPacketQuery = function(order, callback) { 97 | var self = this; 98 | var default_params = { 99 | bill_type: 'MCHT' 100 | }; 101 | 102 | order = _.extend(order, default_params); 103 | 104 | var requiredData = ['mch_billno']; 105 | 106 | this._signedQuery(URLS.REDPACK_QUERY, order, { 107 | https: true, 108 | required: requiredData 109 | }, callback); 110 | }; 111 | 112 | Payment.prototype.transfers = function(order, callback) { 113 | var self = this; 114 | var default_params = { 115 | mchid: this.mchId, 116 | mch_appid: this.appId 117 | }; 118 | 119 | order = _.extend(order, default_params); 120 | 121 | var requiredData = ['mch_appid', 'partner_trade_no', 'openid', 'check_name', 'amount', 'desc', 'spbill_create_ip']; 122 | 123 | this._signedQuery(URLS.TRANSFERS, order, { 124 | https: true, 125 | required: requiredData 126 | }, callback); 127 | }; 128 | 129 | Payment.prototype.transfersQuery = function(order, callback) { 130 | var self = this; 131 | var default_params = { 132 | mch_id: this.mchId, 133 | appid: this.appId 134 | }; 135 | 136 | order = _.extend(order, default_params); 137 | 138 | var requiredData = ['partner_trade_no']; 139 | 140 | this._signedQuery(URLS.TRANSFERS_QUERY, order, { 141 | https: true, 142 | required: requiredData 143 | }, callback); 144 | }; 145 | 146 | /** 147 | * Generate parameters for `WeixinJSBridge.invoke('editAddress', parameters)`. 148 | * 149 | * @param {String} data.url Referer URL that call the API. *Note*: Must contain `code` and `state` in querystring. 150 | * @param {String} data.accessToken 151 | * @param {Function} callback(err, params) 152 | * 153 | * @see https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_9 154 | */ 155 | Payment.prototype.getEditAddressParams = function(data, callback) { 156 | if (!(data.url && data.accessToken)) { 157 | var err = new Error('Missing url or accessToken'); 158 | return callback(err); 159 | } 160 | 161 | var params = { 162 | appId: this.appId, 163 | scope: 'jsapi_address', 164 | signType: 'SHA1', 165 | timeStamp: this._generateTimeStamp(), 166 | nonceStr: this._generateNonceStr(), 167 | }; 168 | var signParams = { 169 | appid: params.appId, 170 | url: data.url, 171 | timestamp: params.timeStamp, 172 | noncestr: params.nonceStr, 173 | accesstoken: data.accessToken, 174 | }; 175 | var string = this._toQueryString(signParams); 176 | params.addrSign = signTypes[params.signType](string); 177 | callback(null, params); 178 | }; 179 | 180 | Payment.prototype._httpRequest = function(url, data, callback) { 181 | request({ 182 | url: url, 183 | method: 'POST', 184 | body: data 185 | }, function(err, response, body) { 186 | if (err) { 187 | return callback(err); 188 | } 189 | 190 | callback(null, body); 191 | }); 192 | }; 193 | 194 | Payment.prototype._httpsRequest = function(url, data, callback) { 195 | var parsed_url = url_mod.parse(url); 196 | var req = https.request({ 197 | host: parsed_url.host, 198 | port: 443, 199 | path: parsed_url.path, 200 | pfx: this.pfx, 201 | passphrase: this.passphrase, 202 | method: 'POST' 203 | }, function(res) { 204 | var content = ''; 205 | res.on('data', function(chunk) { 206 | content += chunk; 207 | }); 208 | res.on('end', function() { 209 | callback(null, content); 210 | }); 211 | }); 212 | 213 | req.on('error', function(e) { 214 | callback(e); 215 | }); 216 | req.write(data); 217 | req.end(); 218 | }; 219 | 220 | Payment.prototype._signedQuery = function(url, params, options, callback) { 221 | var self = this; 222 | var required = options.required || []; 223 | 224 | if (url == URLS.REDPACK_SEND) { 225 | params = this._extendWithDefault(params, [ 226 | 'mch_id', 227 | 'nonce_str' 228 | ]); 229 | } else if (url == URLS.TRANSFERS) { 230 | params = this._extendWithDefault(params, [ 231 | 'nonce_str' 232 | ]); 233 | } else { 234 | params = this._extendWithDefault(params, [ 235 | 'appid', 236 | 'mch_id', 237 | 'sub_mch_id', 238 | 'nonce_str' 239 | ]); 240 | } 241 | 242 | params = _.extend({ 243 | 'sign': this._getSign(params) 244 | }, params); 245 | 246 | if (params.long_url) { 247 | params.long_url = encodeURIComponent(params.long_url); 248 | } 249 | 250 | for (var key in params) { 251 | if (params[key] !== undefined && params[key] !== null) { 252 | params[key] = params[key].toString(); 253 | } 254 | } 255 | 256 | var missing = []; 257 | required.forEach(function(key) { 258 | var alters = key.split('|'); 259 | for (var i = alters.length - 1; i >= 0; i--) { 260 | if (params[alters[i]]) { 261 | return; 262 | } 263 | } 264 | missing.push(key); 265 | }); 266 | 267 | if (missing.length) { 268 | return callback('missing params ' + missing.join(',')); 269 | } 270 | 271 | var request = (options.https ? this._httpsRequest : this._httpRequest).bind(this); 272 | request(url, this.buildXml(params), function(err, body) { 273 | if (err) { 274 | return callback(err); 275 | } 276 | self.validate(body, callback); 277 | }); 278 | 279 | }; 280 | 281 | Payment.prototype.unifiedOrder = function(params, callback) { 282 | var requiredData = ['body', 'out_trade_no', 'total_fee', 'spbill_create_ip', 'trade_type']; 283 | if (params.trade_type == 'JSAPI') { 284 | requiredData.push('openid|sub_openid'); 285 | } else if (params.trade_type == 'NATIVE') { 286 | requiredData.push('product_id'); 287 | } 288 | params.notify_url = params.notify_url || this.notifyUrl; 289 | this._signedQuery(URLS.UNIFIED_ORDER, params, { 290 | required: requiredData 291 | }, callback); 292 | }; 293 | 294 | Payment.prototype.orderQuery = function(params, callback) { 295 | this._signedQuery(URLS.ORDER_QUERY, params, { 296 | required: ['transaction_id|out_trade_no'] 297 | }, callback); 298 | }; 299 | 300 | Payment.prototype.refund = function(params, callback) { 301 | params = this._extendWithDefault(params, [ 302 | 'op_user_id' 303 | ]); 304 | 305 | this._signedQuery(URLS.REFUND, params, { 306 | https: true, 307 | required: ['transaction_id|out_trade_no', 'out_refund_no', 'total_fee', 'refund_fee'] 308 | }, callback); 309 | }; 310 | 311 | Payment.prototype.refundQuery = function(params, callback) { 312 | this._signedQuery(URLS.REFUND_QUERY, params, { 313 | required: ['transaction_id|out_trade_no|out_refund_no|refund_id'] 314 | }, callback); 315 | }; 316 | 317 | Payment.prototype.downloadBill = function(params, callback) { 318 | var self = this; 319 | this._signedQuery(URLS.DOWNLOAD_BILL, params, { 320 | required: ['bill_date', 'bill_type'] 321 | }, function(err, rawData) { 322 | if (err) { 323 | if (err.name == 'XMLParseError') { 324 | callback(null, self._parseCsv(rawData)); 325 | } else { 326 | callback(err); 327 | } 328 | } 329 | }); 330 | }; 331 | 332 | Payment.prototype.shortUrl = function(params, callback) { 333 | this._signedQuery(URLS.SHORT_URL, params, { 334 | required: ['long_url'] 335 | }, callback); 336 | }; 337 | 338 | Payment.prototype.closeOrder = function(params, callback) { 339 | this._signedQuery(URLS.CLOSE_ORDER, params, { 340 | required: ['out_trade_no'] 341 | }, callback); 342 | }; 343 | 344 | Payment.prototype._parseCsv = function(text) { 345 | var rows = text.trim().split(/\r?\n/); 346 | 347 | function toArr(rows) { 348 | var titles = rows[0].split(','); 349 | var bodys = rows.splice(1); 350 | var data = []; 351 | 352 | bodys.forEach(function(row) { 353 | var rowData = {}; 354 | row.split(',').forEach(function(cell, i) { 355 | rowData[titles[i]] = cell.split('`')[1]; 356 | }); 357 | data.push(rowData); 358 | }); 359 | return data; 360 | } 361 | 362 | return { 363 | list: toArr(rows.slice(0, rows.length - 2)), 364 | stat: toArr(rows.slice(rows.length - 2, rows.length))[0] 365 | }; 366 | }; 367 | 368 | Payment.prototype.buildXml = function(obj) { 369 | var builder = new xml2js.Builder({ 370 | allowSurrogateChars: true 371 | }); 372 | var xml = builder.buildObject({ 373 | xml: obj 374 | }); 375 | return xml; 376 | }; 377 | 378 | Payment.prototype.validate = function(xml, callback) { 379 | var self = this; 380 | xml2js.parseString(xml, { 381 | trim: true, 382 | explicitArray: false 383 | }, function(err, json) { 384 | var error = null, 385 | data; 386 | if (err) { 387 | error = new Error(); 388 | err.name = 'XMLParseError'; 389 | return callback(err, xml); 390 | } 391 | 392 | data = json ? json.xml : {}; 393 | 394 | if (data.return_code == RETURN_CODES.FAIL) { 395 | error = new Error(data.return_msg); 396 | error.name = 'ProtocolError'; 397 | } else if (data.result_code == RETURN_CODES.FAIL) { 398 | error = new Error(data.err_code); 399 | error.name = 'BusinessError'; 400 | } else if (data.appid && self.appId !== data.appid) { 401 | error = new Error(); 402 | error.name = 'InvalidAppId'; 403 | } else if (data.mch_id && self.mchId !== data.mch_id) { 404 | error = new Error(); 405 | error.name = 'InvalidMchId'; 406 | } else if (data.mchid && self.mchId !== data.mchid) { 407 | error = new Error(); 408 | error.name = 'InvalidMchId'; 409 | } else if (self.subMchId && self.subMchId !== data.sub_mch_id) { 410 | error = new Error(); 411 | error.name = 'InvalidSubMchId'; 412 | } else if (data.sign && self._getSign(data) !== data.sign) { 413 | error = new Error(); 414 | error.name = 'InvalidSignature'; 415 | } 416 | 417 | callback(error, data); 418 | }); 419 | }; 420 | 421 | /** 422 | * 使用默认值扩展对象 423 | * @param {Object} obj 424 | * @param {Array} keysNeedExtend 425 | * @return {Object} extendedObject 426 | */ 427 | Payment.prototype._extendWithDefault = function(obj, keysNeedExtend) { 428 | var defaults = { 429 | appid: this.appId, 430 | mch_id: this.mchId, 431 | sub_mch_id: this.subMchId, 432 | nonce_str: this._generateNonceStr(), 433 | notify_url: this.notifyUrl, 434 | op_user_id: this.mchId, 435 | pfx: this.pfx 436 | }; 437 | var extendObject = {}; 438 | keysNeedExtend.forEach(function(k) { 439 | if (defaults[k]) { 440 | extendObject[k] = defaults[k]; 441 | } 442 | }); 443 | return _.extend(extendObject, obj); 444 | }; 445 | 446 | Payment.prototype._getSign = function(pkg, signType) { 447 | pkg = _.clone(pkg); 448 | delete pkg.sign; 449 | signType = signType || 'MD5'; 450 | var string1 = this._toQueryString(pkg); 451 | var stringSignTemp = string1 + '&key=' + this.partnerKey; 452 | var signValue = signTypes[signType](stringSignTemp).toUpperCase(); 453 | return signValue; 454 | }; 455 | 456 | Payment.prototype._toQueryString = function(object) { 457 | return Object.keys(object).filter(function(key) { 458 | return object[key] !== undefined && object[key] !== ''; 459 | }).sort().map(function(key) { 460 | return key + '=' + object[key]; 461 | }).join('&'); 462 | }; 463 | 464 | Payment.prototype._generateTimeStamp = function() { 465 | return parseInt(+new Date() / 1000, 10) + ''; 466 | }; 467 | 468 | /** 469 | * [_generateNonceStr description] 470 | * @param {[type]} length [description] 471 | * @return {[type]} [description] 472 | */ 473 | Payment.prototype._generateNonceStr = function(length) { 474 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 475 | var maxPos = chars.length; 476 | var noceStr = ''; 477 | var i; 478 | for (i = 0; i < (length || 32); i++) { 479 | noceStr += chars.charAt(Math.floor(Math.random() * maxPos)); 480 | } 481 | return noceStr; 482 | }; 483 | 484 | /** 485 | * Promisify for public functions 486 | */ 487 | if (global.Promise) { 488 | for (let key in Payment.prototype) { 489 | let func = Payment.prototype[key] 490 | let syncFuncs = ['buildXml'] 491 | if (typeof func == 'function' && key.indexOf('_') !== 0 && syncFuncs.indexOf(key) === -1) { 492 | Payment.prototype[key] = function () { 493 | let args = Array.prototype.slice.call(arguments) 494 | let originCallback = args[args.length - 1] 495 | return new Promise((resolve, reject) => { 496 | let handleResult = function (err, result) { 497 | if (err) { 498 | reject(err) 499 | } else { 500 | resolve(result) 501 | } 502 | } 503 | if (typeof originCallback !== 'function') { 504 | args.push(handleResult) 505 | } else { 506 | args[args.length - 1] = function (err, result) { 507 | handleResult(err, result) 508 | originCallback(err, result) 509 | } 510 | } 511 | func.apply(this, args) 512 | }) 513 | } 514 | } 515 | } 516 | } 517 | 518 | exports.Payment = Payment; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-pay", 3 | "version": "0.3.2", 4 | "description": "wechat payment api for document v3.3.5", 5 | "main": "index.js", 6 | "scripts": 7 | { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": 11 | { 12 | "type": "git", 13 | "url": "git://github.com/supersheep/wechat-pay.git" 14 | }, 15 | "keywords": [ 16 | "wechat", 17 | "payment" 18 | ], 19 | "author": "spud", 20 | "license": "ISC", 21 | "bugs": 22 | { 23 | "url": "https://github.com/supersheep/wechat-pay/issues" 24 | }, 25 | "homepage": "https://github.com/supersheep/wechat-pay", 26 | "dependencies": 27 | { 28 | "md5": "^2.0.0", 29 | "request": "^2.45.0", 30 | "sha1": "^1.1.0", 31 | "underscore": "^1.7.0", 32 | "xml2js": "^0.4.4" 33 | } 34 | } 35 | --------------------------------------------------------------------------------