├── .gitignore ├── .idea ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── node-wxpay.iml ├── vcs.xml ├── watcherTasks.xml └── workspace.xml ├── README.md ├── lib ├── index.js └── test.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | /test.js -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | true 8 | 9 | false 10 | true 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/node-wxpay.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | true 89 | DEFINITION_ORDER 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 121 | 122 | 125 | 126 | 127 | 128 | 131 | 132 | 135 | 136 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 206 | 207 | project 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | project 224 | 225 | 226 | true 227 | 228 | 229 | 230 | DIRECTORY 231 | 232 | false 233 | 234 | 235 | 236 | 237 | 239 | 240 | 241 | 242 | 1615426221422 243 | 255 | 256 | 257 | 258 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 286 | 287 | 289 | 290 | 291 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-wxpay 2 | 微信支付APIv3 for nodejs 3 | 4 | 5 | ## 功能概述 6 | - `完成模块` jsapi,native,h5,app统一下单,付款交易查询,退款,退款交易查询,解密通知参数,公钥获取,验证签名,交易账单,资金账单,下载账单 7 | - `支付模式支持` 付款码/公众号/小程序/APP/H5/扫码支付 8 | 9 | ## 交流 10 | 微信:yangfuhe036,如有任何问题欢迎加微信交流。喜欢或对你有帮助,欢迎右上角 star,非常感谢! 11 | 12 | ## 使用前必读 13 | #### 版本要求 14 | nodejs >= 8.3.0 15 | 16 | ## 安装 17 | ```Bash 18 | npm i wxpay-v3 --save 19 | 20 | # 如已安装旧版, 重新安装最新版 21 | npm i wxpay-v3@latest 22 | ``` 23 | 24 | ## 实例化 25 | ```javascript 26 | const Payment = require('wxpay-v3'); 27 | const paymnet = new Payment({ 28 | appid: '公众号ID', 29 | mchid: '微信商户号', 30 | private_key: require('fs').readFileSync('*_key.pem证书文件路径').toString(),//或者直接复制证书文件内容 31 | serial_no:'证书序列号', 32 | apiv3_private_key:'api v3密钥', 33 | notify_url: '支付退款结果通知的回调地址', 34 | }) 35 | ``` 36 | 37 | #### config说明: 38 | - `appid` - 公众号ID(必填) 39 | - `mchid` - 微信商户号(必填) 40 | - `private_key` - 商户API证书*_key.pem中内容 可在微信支付平台获取(必填, 在微信商户管理界面获取) 41 | - `serial_no` - 证书序列号(必填, 证书序列号,可在微信支付平台获取 也可以通过此命令获取(*_cert.pem为你的证书文件) openssl x509 -in *_cert.pem -noout -serial ) 42 | - `apiv3_private_key` - apiv3密钥 在创建实例时通过apiv3密钥会自动获取平台证书的公钥,以便于验证签名(必填) 43 | - `notify_url` - 支付退款结果通知的回调地址(选填) 44 | - 可以在初始化的时候传入设为默认值, 不传则需在调用相关API时传入 45 | - 调用相关API时传入新值则使用新值 46 | 47 | 48 | ### jsapi统一下单 49 | ```javascript 50 | let result = await payment.jsapi({ 51 | description:'点存云-测试支付', 52 | out_trade_no:Date.now().toString(), 53 | amount:{ 54 | total:1 55 | }, 56 | payer:{ 57 | openid:'ouEJk65CZr8_7eb95RIPDNWZKrvI' 58 | }, 59 | 60 | }) 61 | console.log(result) 62 | ``` 63 | ### app统一下单 64 | ```javascript 65 | let result = await payment.app({ 66 | description:'点存云-测试支付', 67 | out_trade_no:Date.now().toString(), 68 | amount:{ 69 | total:1 70 | } 71 | }) 72 | console.log(result) 73 | ``` 74 | 75 | ### h5统一下单 76 | ```javascript 77 | let result = await payment.h5({ 78 | description:'点存云-测试支付', 79 | out_trade_no:Date.now().toString(), 80 | amount:{ 81 | total:1 82 | }, 83 | scene_info:{ 84 | payer_client_ip:'203.205.219.187' 85 | } 86 | }) 87 | console.log(result) 88 | ``` 89 | 90 | ### native统一下单 91 | ```javascript 92 | let result = await payment.native({ 93 | description:'点存云-测试支付', 94 | out_trade_no:Date.now().toString(), 95 | amount:{ 96 | total:1 97 | } 98 | }) 99 | console.log(result) 100 | ``` 101 | 102 | ### 通过transaction_id查询订单 103 | ```javascript 104 | let result = await payment.getTransactionsById({ 105 | transaction_id:'4200000928202103013162567337' 106 | }) 107 | console.log(result) 108 | ``` 109 | 110 | ### 通过out_trade_no查询订单 111 | ```javascript 112 | let result = await payment.getTransactionsByOutTradeNo({ 113 | out_trade_no:'1614602083807' 114 | }) 115 | console.log(result) 116 | ``` 117 | 118 | ### 关闭订单 119 | ```javascript 120 | let result = await payment.close({ 121 | out_trade_no:'1614602083807' 122 | }) 123 | console.log(result) 124 | ``` 125 | 126 | ### 退款 127 | ```javascript 128 | let result = await payment.refund({ 129 | transaction_id:'4200000902202103026804947229', 130 | //out_trade_no:'1614602083807', 131 | out_refund_no:Date.now().toString(), 132 | amount:{ 133 | refund:1, 134 | total:1, 135 | currency:'CNY' 136 | } 137 | }) 138 | console.log(result) 139 | ``` 140 | 141 | ### 查询单笔退款订单 142 | ```javascript 143 | let result = await payment.getRefund({ 144 | out_refund_no:'1614757507992', 145 | }) 146 | console.log(result) 147 | ``` 148 | 149 | ### 获取平台证书列表 150 | ```javascript 151 | let result = await payment.getCertificates() 152 | console.log(result) 153 | ``` 154 | 155 | ### 解密支付退款通知参数 156 | ```javascript 157 | let result = payment.decodeResource({ 158 | "original_type":"refund", 159 | "algorithm":"AEAD_AES_256_GCM", 160 | "ciphertext":"d2Zi2VToOGXqB3K6bgQaFKktgA3AHm+cJg0vGZPcD22OUZ+CBymtrFJsFtaKKEwebSDN8Habic7NJVpKJpAxZd8ejm32v4UePg139/gj+X7vJtqB39ZkjZXLH973LT5R5yZQ351R3onlpx9JILN2+FNEbrUNenjgEufuQn45b9jwGSBX/sU6n/+gsCdt8+sSkbMy37sSX1bjMicHzte27fR0QSuO1TDjZjjDqP2ou0j7Jb+x9RRtWlbZ1hOYe7AhSTFzOXvkdCq0M6P6ja1cc2olV9xG8UzKxZN0JLnoqIGWwPzTVOPqmt/N3/MrzCK3TT1mNagBnhqEvSXhL9KUjpAIY8J6tkjfoG+9QwnJA8kW48C3nGsgePvNYvikJooQii7rx78Y2paR7cS8Pn8+sxKg4q91DiovBSdW2/ePDruI6SH/FWFrPmLQCG11fCjz/C9o6bqjaSsHKMaSVSAW9e/et04MP6GcZIDweG5AN9FgOXMI", 161 | "associated_data":"refund", 162 | "nonce":"AqfRSFm7h9Sa" 163 | }) 164 | console.log(result) 165 | ``` 166 | 167 | 168 | ### 验证签名 169 | ```javascript 170 | setTimeout(async ()=>{ 171 | //timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数 172 | let result = await payment.verifySign({ 173 | timestamp:'1614829763', 174 | nonce:'Eeumuhd3zA5TirWeJUCLCpkENYM8PSUA', 175 | serial:'3DEA336346E96C002B7B0D514D424C8DEDBF9145', 176 | signature:'ame3lX1y6FeXrlBN973M1Dhg5n77M1wVsD3VgeyZlb8c3dz/hpQ+9vNOMBBHGdv8kDIfZUxKDdfoeUaVJhfqAEn9ZV4x112ntEzCHpJtIXQ3rr8fScY7cO71EN/QyQHtY1Ovt8U2Yr891iYaLujUrBHtWrhiR6UKecRA+/RgsUBYh4D10rrqW5ywNrLVN+PSuG4QB85bz3jSslMvRrSG7HP/Xwo3e2sWMDuQ2Uadefu+8/FK1P3KDLDO2fq5teSaaqs7oof2WpV6zrVtyQ+P4p5t8NJ0ExlOSAs2xGJ0+xi+U996tq3VYZXf/4nVsfGW9rn0m/mOrYTmiST9PF+q1g==', 177 | body:'{"id":"3b66121d-c9b9-5d61-9d92-eeec248e993d","create_time":"2021-03-04T11:49:23+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"PB305U6jR6TN8mBzbzGts5TaKnDXQt/7C+uJpGnvOT1SyCvI18L4f42eTZtrZv+5XUdOkxwEHGWDVl2MwbvpgLjLdjyisaHc+uRQCDoYlusiaeDJzd515Rl36nqmdPD8xFKZahWZBBkAlCgXLuW3qcdxSISTk/pyqPziwUtFKfMeq3LEEm4z8DfBM9cVXJrN8EiY2WaQsm+lGnZAV4+pxCELj67xmccXs3JgJwHSKE4exqW919atQWTwJHzuP3WNd+Xvp0zwm9RtDPTvZ8egehqqBw+DARC5jg8MmDtlMR2sTgH2xq6b4+QqLXPPIooOyvEZKMOteSI4FmSfPNwDfZ26D4ga9yGRIxSQKkWDq3QRNhOzvmSkCax08t2hdq12NxBSE9y7aZkjKIr4/uMEtKDU/3wcSoVKlawfN1hlCKo2nWbdKH1avRvc6FAFxXHtXRw0Y0MRnSk8gPMF/T+QqEMRJniXbrylt21xR0AEKbIVk0xK9jvhXex0AvST4x3eKM0r4DXkmL/pCjo1XmZLZIMc2uJ1jJEyqWcURXirrxADCATIAEWOu1hNL6PE","associated_data":"transaction","nonce":"KcsMoPx5UW1i"}}' 178 | }) 179 | console.log(result) 180 | },2000) 181 | ``` 182 | 183 | ### 申请交易账单 184 | ```javascript 185 | let result = await payment.tradebill({ 186 | bill_date:'2021-03-03' 187 | }) 188 | console.log(result) 189 | ``` 190 | 191 | ### 申请资金账单 192 | ```javascript 193 | let result = await payment.fundflowbill({ 194 | bill_date:'2021-03-03' 195 | }) 196 | console.log(result) 197 | ``` 198 | 199 | ### 下载账单 200 | ```javascript 201 | let result = await payment.downloadbill('https://api.mch.weixin.qq.com/v3/billdownload/file?token=ktWgOuBvGNvmCk0NaOTMF41tG3yWsZrdM4zdgl10r1GRRNo4tG5V9mPi04ku-PY8&tartype=gzip') 202 | console.log(result) 203 | ``` 204 | 205 | ## 交流 206 | 微信:yangfuhe036,如有任何问题欢迎加微信交流。喜欢或对你有帮助,欢迎右上角 star,非常感谢! 207 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const urllib = require('urllib'); 2 | const {KJUR, hextob64} = require('jsrsasign') 3 | const assert = require('assert') 4 | //const nodeAesGcm = require('node-aes-gcm') 5 | const crypto = require("crypto"); 6 | const x509 = require('@peculiar/x509'); 7 | class Payment { 8 | constructor({appid, mchid, private_key, serial_no,apiv3_private_key,notify_url} = {}) { 9 | assert(appid, 'appid is required') 10 | assert(mchid, 'mchid is required') 11 | assert(private_key, 'private_key is required') 12 | assert(serial_no, 'serial_no is required') 13 | assert(apiv3_private_key, 'apiv3_private_key is required') 14 | 15 | this.appid = appid; 16 | this.mchid = mchid; 17 | this.private_key = private_key; 18 | this.serial_no = serial_no; 19 | this.apiv3_private_key = apiv3_private_key; 20 | this.notify_url = notify_url; 21 | 22 | this.urls = { 23 | jsapi:() => { 24 | return { 25 | url:'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi', 26 | method:'POST', 27 | pathname:'/v3/pay/transactions/jsapi', 28 | } 29 | }, 30 | app:() => { 31 | return { 32 | url:'https://api.mch.weixin.qq.com/v3/pay/transactions/app', 33 | method:'POST', 34 | pathname:'/v3/pay/transactions/app', 35 | } 36 | }, 37 | h5:() => { 38 | return { 39 | url:'https://api.mch.weixin.qq.com/v3/pay/transactions/h5', 40 | method:'POST', 41 | pathname:'/v3/pay/transactions/h5', 42 | } 43 | }, 44 | native:() => { 45 | return { 46 | url:'https://api.mch.weixin.qq.com/v3/pay/transactions/native', 47 | method:'POST', 48 | pathname:'/v3/pay/transactions/native', 49 | } 50 | }, 51 | getTransactionsById:({pathParams}) => { 52 | return { 53 | url:`https://api.mch.weixin.qq.com/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`, 54 | method:`GET`, 55 | pathname:`/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`, 56 | } 57 | }, 58 | getTransactionsByOutTradeNo:({pathParams}) => { 59 | return { 60 | url:`https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`, 61 | method:`GET`, 62 | pathname:`/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`, 63 | } 64 | }, 65 | close:({pathParams}) => { 66 | return { 67 | url:`https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`, 68 | method:`POST`, 69 | pathname:`/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`, 70 | } 71 | }, 72 | refund:() => { 73 | return { 74 | url:`https://api.mch.weixin.qq.com/v3/refund/domestic/refunds`, 75 | method:`POST`, 76 | pathname:`/v3/refund/domestic/refunds`, 77 | } 78 | }, 79 | getRefund:({pathParams}) => { 80 | return { 81 | url:`https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${pathParams.out_refund_no}`, 82 | method:`GET`, 83 | pathname:`/v3/refund/domestic/refunds/${pathParams.out_refund_no}`, 84 | } 85 | }, 86 | getCertificates:() => { 87 | return { 88 | url:`https://api.mch.weixin.qq.com/v3/certificates`, 89 | method:`GET`, 90 | pathname:`/v3/certificates`, 91 | } 92 | }, 93 | tradebill:({queryParams}) => { 94 | let {bill_date,bill_type,tar_type} = queryParams; 95 | return { 96 | url:`https://api.mch.weixin.qq.com/v3/bill/tradebill?bill_date=${bill_date}${bill_type?'&bill_type='+bill_type:''}${tar_type?'&tar_type='+tar_type:''}`, 97 | method:`GET`, 98 | pathname:`/v3/bill/tradebill?bill_date=${bill_date}${bill_type?'&bill_type='+bill_type:''}${tar_type?'&tar_type='+tar_type:''}`, 99 | } 100 | }, 101 | fundflowbill:({queryParams}) => { 102 | let {bill_date,account_type,tar_type} = queryParams; 103 | return { 104 | url:`https://api.mch.weixin.qq.com/v3/bill/fundflowbill?bill_date=${bill_date}${account_type?'&account_type='+account_type:''}${tar_type?'&tar_type='+tar_type:''}`, 105 | method:`GET`, 106 | pathname:`/v3/bill/fundflowbill?bill_date=${bill_date}${account_type?'&account_type='+account_type:''}${tar_type?'&tar_type='+tar_type:''}`, 107 | } 108 | }, 109 | downloadbill:({pathParams}) => { 110 | let url = pathParams; 111 | let index = url.indexOf('/v3') 112 | let pathname = url.substr(index) 113 | return { 114 | url, 115 | method:`GET`, 116 | pathname, 117 | } 118 | }, 119 | } 120 | this.decodeCertificates() 121 | } 122 | 123 | async run({pathParams,queryParams,bodyParams,type}){ 124 | assert(type, 'type is required') 125 | let {url,method,pathname} = this.urls[type]({pathParams,queryParams}) 126 | let timestamp = Math.floor(Date.now()/1000) 127 | let onece_str = this.generate(); 128 | let bodyParamsStr = bodyParams&&Object.keys(bodyParams).length?JSON.stringify(bodyParams):'' 129 | let signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`,this.private_key,'SHA256withRSA') 130 | let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"` 131 | let {status, data} = await urllib.request(url, { 132 | method: method, 133 | dataType: 'text', 134 | data: method=='GET'?'':bodyParams, 135 | timeout: [10000, 15000], 136 | headers:{ 137 | 'Content-Type':'application/json', 138 | 'Accept':'application/json', 139 | 'Authorization':Authorization 140 | }, 141 | }) 142 | return {status, data} 143 | } 144 | 145 | //jsapi统一下单 146 | async jsapi(params){ 147 | let bodyParams = { 148 | ...params, 149 | appid:this.appid, 150 | mchid:this.mchid, 151 | notify_url:params.notify_url||this.notify_url, 152 | } 153 | return await this.run({bodyParams,type:'jsapi'}) 154 | } 155 | 156 | //app统一下单 157 | async app(params){ 158 | let bodyParams = { 159 | ...params, 160 | appid:this.appid, 161 | mchid:this.mchid, 162 | notify_url:params.notify_url||this.notify_url, 163 | } 164 | return await this.run({bodyParams,type:'app'}) 165 | } 166 | 167 | //h5统一下单 168 | async h5(params){ 169 | let bodyParams = { 170 | ...params, 171 | appid:this.appid, 172 | mchid:this.mchid, 173 | notify_url:params.notify_url||this.notify_url, 174 | } 175 | return await this.run({bodyParams,type:'h5'}) 176 | } 177 | 178 | //native统一下单 179 | async native(params){ 180 | let bodyParams = { 181 | ...params, 182 | appid:this.appid, 183 | mchid:this.mchid, 184 | notify_url:params.notify_url||this.notify_url, 185 | } 186 | return await this.run({bodyParams,type:'native'}) 187 | } 188 | 189 | //通过transaction_id查询订单 190 | async getTransactionsById(params){ 191 | return await this.run({pathParams:params,type:'getTransactionsById'}) 192 | } 193 | 194 | //通过out_trade_no查询订单 195 | async getTransactionsByOutTradeNo(params){ 196 | return await this.run({pathParams:params,type:'getTransactionsByOutTradeNo'}) 197 | } 198 | 199 | //关闭订单 200 | async close(params){ 201 | return await this.run({pathParams:{ 202 | out_trade_no:params.out_trade_no 203 | },bodyParams:{ 204 | mchid:this.mchid 205 | },type:'close'}) 206 | } 207 | 208 | //退款 209 | async refund(params){ 210 | let bodyParams = { 211 | ...params, 212 | notify_url:params.notify_url||this.notify_url, 213 | } 214 | return await this.run({bodyParams,type:'refund'}) 215 | } 216 | 217 | //查询单笔退款订单 218 | async getRefund(params){ 219 | return await this.run({pathParams:params,type:'getRefund'}) 220 | } 221 | 222 | //获取平台证书列表 223 | async getCertificates(){ 224 | return await this.run({type:'getCertificates'}) 225 | } 226 | 227 | //解密证书列表 解出CERTIFICATE以及public key 228 | async decodeCertificates(){ 229 | let result = await this.getCertificates(); 230 | if(result.status!=200){ 231 | throw new Error('获取证书列表失败') 232 | } 233 | let certificates = typeof result.data == 'string'?JSON.parse(result.data).data:result.data.data 234 | for(let cert of certificates){ 235 | let output = this.decode(cert.encrypt_certificate) 236 | cert.decrypt_certificate = output.toString() 237 | let beginIndex = cert.decrypt_certificate.indexOf('-\n') 238 | let endIndex = cert.decrypt_certificate.indexOf('\n-') 239 | let str = cert.decrypt_certificate.substring(beginIndex+2,endIndex) 240 | let x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64')); 241 | let public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64') 242 | cert.public_key = `-----BEGIN PUBLIC KEY-----\n` + public_key + `\n-----END PUBLIC KEY-----` 243 | } 244 | return this.certificates = certificates 245 | } 246 | 247 | //验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数 248 | async verifySign({timestamp,nonce,serial,body,signature},repeatVerify = true) { 249 | let data = `${timestamp}\n${nonce}\n${typeof body == 'string'?body:JSON.stringify(body)}\n`; 250 | let verify = crypto.createVerify('RSA-SHA256'); 251 | verify.update(Buffer.from(data)); 252 | let verifySerialNoPass = false; 253 | for(let cert of this.certificates){ 254 | if(cert.serial_no == serial){ 255 | verifySerialNoPass = true; 256 | return verify.verify(cert.public_key, signature, 'base64'); 257 | } 258 | } 259 | if(!verifySerialNoPass&&repeatVerify){ 260 | await this.decodeCertificates(); 261 | return await this.verifySign({timestamp,nonce,serial,body,signature},false) 262 | }else{ 263 | throw new Error('平台证书序列号不相符') 264 | } 265 | } 266 | 267 | 268 | //申请交易账单 269 | async tradebill(params){ 270 | return await this.run({queryParams:params,type:'tradebill'}) 271 | } 272 | 273 | //申请资金账单 274 | async fundflowbill(params){ 275 | return await this.run({queryParams:params,type:'fundflowbill'}) 276 | } 277 | 278 | //下载账单 279 | async downloadbill(download_url){ 280 | return await this.run({pathParams:download_url,type:'downloadbill'}) 281 | } 282 | 283 | 284 | //解密支付退款通知资源数据 285 | decodeResource(resource){ 286 | let plaintext = this.decode(resource) 287 | return JSON.parse(plaintext.toString()); 288 | } 289 | 290 | //解密(由于有些开发者反馈window环境安装node-aes-gcm一直不成功,所以该解密方法已弃用,现换用下面解密函数) 291 | // decode(params){ 292 | // let AUTH_KEY_LENGTH = 16; 293 | // let { ciphertext, associated_data , nonce } = params; 294 | // let key_bytes = Buffer.from(this.apiv3_private_key, 'utf8'); 295 | // let nonce_bytes = Buffer.from(nonce, 'utf8'); 296 | // let associated_data_bytes = Buffer.from(associated_data, 'utf8'); 297 | // let ciphertext_bytes = Buffer.from(ciphertext, 'base64'); 298 | // let cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH; 299 | // let cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length); 300 | // let auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length); 301 | // return nodeAesGcm.decrypt(key_bytes, nonce_bytes, cipherdata_bytes, associated_data_bytes, auth_tag_bytes); 302 | // } 303 | 304 | //解密 305 | decode(params) { 306 | const AUTH_KEY_LENGTH = 16; 307 | // ciphertext = 密文,associated_data = 填充内容, nonce = 位移 308 | const { ciphertext, associated_data, nonce } = params; 309 | // 密钥 310 | const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8'); 311 | // 位移 312 | const nonce_bytes = Buffer.from(nonce, 'utf8'); 313 | // 填充内容 314 | const associated_data_bytes = Buffer.from(associated_data, 'utf8'); 315 | // 密文Buffer 316 | const ciphertext_bytes = Buffer.from(ciphertext, 'base64'); 317 | // 计算减去16位长度 318 | const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH; 319 | // upodata 320 | const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length); 321 | // tag 322 | const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length); 323 | const decipher = crypto.createDecipheriv( 324 | 'aes-256-gcm', key_bytes, nonce_bytes 325 | ); 326 | decipher.setAuthTag(auth_tag_bytes); 327 | decipher.setAAD(Buffer.from(associated_data_bytes)); 328 | 329 | const output = Buffer.concat([ 330 | decipher.update(cipherdata_bytes), 331 | decipher.final(), 332 | ]); 333 | return output; 334 | } 335 | 336 | //生成随机字符串 337 | generate(length = 32){ 338 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 339 | let noceStr = '', maxPos = chars.length; 340 | while (length--) noceStr += chars[Math.random() * maxPos | 0]; 341 | return noceStr; 342 | } 343 | 344 | 345 | /** 346 | * rsa签名 347 | * @param content 签名内容 348 | * @param privateKey 私钥,PKCS#1 349 | * @param hash hash算法,SHA256withRSA,SHA1withRSA 350 | * @returns 返回签名字符串,base64 351 | */ 352 | rsaSign(content, privateKey, hash='SHA256withRSA'){ 353 | // 创建 Signature 对象 354 | const signature = new KJUR.crypto.Signature({ 355 | alg: hash, 356 | //!这里指定 私钥 pem! 357 | prvkeypem: privateKey 358 | }) 359 | signature.updateString(content) 360 | const signData = signature.sign() 361 | // 将内容转成base64 362 | return hextob64(signData) 363 | } 364 | } 365 | 366 | module.exports = Payment; 367 | 368 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs') 3 | const path = require('path') 4 | const Payment = require("./index"); 5 | 6 | //商户API证书apiclient_key.pem中内容 可在微信支付平台获取 7 | const private_key = 8 | `-----BEGIN PRIVATE KEY----- 9 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 10 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 12 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 13 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 14 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 15 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 16 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 17 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 18 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 19 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 20 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 21 | -----END PRIVATE KEY-----`; 22 | 23 | //商户API证书序列号,可在微信支付平台获取 也可以通过此命令获取(*_cert.pem为你的证书文件) openssl x509 -in apiclient_cert.pem -noout -serial 24 | const serial_no = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; 25 | //公众号ID 26 | const appid = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; 27 | //微信商户号 28 | const mchid = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; 29 | //支付退款结果通知的回调地址 30 | const notify_url = 'https://xxxx.xxx.xxx/xxx/xxx/xxx'; 31 | //api v3密钥 32 | const apiv3_private_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 33 | let payment = new Payment({ 34 | appid, 35 | mchid, 36 | private_key:private_key,//或fs.readFileSync(path.join(__dirname,'*_key.pem证书文件路径')).toString(), 37 | serial_no, 38 | notify_url, 39 | apiv3_private_key 40 | }) 41 | 42 | //jsapi统一下单 测试命令 node ./lib/test.js --method=jsapi 43 | this.jsapi = async () => { 44 | let result = await payment.jsapi({ 45 | description:'点存云-测试支付', 46 | out_trade_no:Date.now().toString(), 47 | amount:{ 48 | total:1 49 | }, 50 | payer:{ 51 | openid:'ouEJk65CZr8_7eb95RIPDNWZKrvI' 52 | }, 53 | 54 | }) 55 | console.log(result) 56 | } 57 | 58 | //app统一下单 测试命令 node ./lib/test.js --method=app 59 | this.app = async () => { 60 | let result = await payment.app({ 61 | description:'点存云-测试支付', 62 | out_trade_no:Date.now().toString(), 63 | amount:{ 64 | total:1 65 | } 66 | }) 67 | console.log(result) 68 | } 69 | 70 | //h5统一下单 测试命令 node ./lib/test.js --method=h5 71 | this.h5 = async () => { 72 | let result = await payment.h5({ 73 | description:'点存云-测试支付', 74 | out_trade_no:Date.now().toString(), 75 | amount:{ 76 | total:1 77 | }, 78 | scene_info:{ 79 | payer_client_ip:'203.205.219.187' 80 | } 81 | }) 82 | console.log(result) 83 | } 84 | 85 | //native统一下单 测试命令 node ./lib/test.js --method=native 86 | this.native = async () => { 87 | let result = await payment.native({ 88 | description:'点存云-测试支付', 89 | out_trade_no:Date.now().toString(), 90 | amount:{ 91 | total:1 92 | } 93 | }) 94 | console.log(result) 95 | } 96 | 97 | //通过transaction_id查询订单 测试命令 node ./lib/test.js --method=getTransactionsById 98 | this.getTransactionsById = async () => { 99 | let result = await payment.getTransactionsById({ 100 | transaction_id:'4200000928202103013162567337' 101 | }) 102 | console.log(result) 103 | } 104 | 105 | //通过out_trade_no查询订单 测试命令 node ./lib/test.js --method=getTransactionsByOutTradeNo 106 | this.getTransactionsByOutTradeNo = async () => { 107 | let result = await payment.getTransactionsByOutTradeNo({ 108 | out_trade_no:'1614602083807' 109 | }) 110 | console.log(result) 111 | } 112 | 113 | //关闭订单 测试命令 node ./lib/test.js --method=close 114 | this.close = async () => { 115 | let result = await payment.close({ 116 | out_trade_no:'1614602083807' 117 | }) 118 | console.log(result) 119 | } 120 | 121 | //退款 测试命令 node ./lib/test.js --method=refund 122 | this.refund = async () => { 123 | let result = await payment.refund({ 124 | transaction_id:'4200000902202103026804947229', 125 | //out_trade_no:'1614602083807', 126 | out_refund_no:Date.now().toString(), 127 | amount:{ 128 | refund:1, 129 | total:1, 130 | currency:'CNY' 131 | } 132 | }) 133 | console.log(result) 134 | } 135 | 136 | //查询单笔退款订单 测试命令 node ./lib/test.js --method=getRefund 137 | this.getRefund = async () => { 138 | let result = await payment.getRefund({ 139 | out_refund_no:'1614757507992', 140 | }) 141 | console.log(result) 142 | } 143 | 144 | //获取平台证书列表 测试命令 node ./lib/test.js --method=getCertificates 145 | this.getCertificates = async () => { 146 | let result = await payment.getCertificates() 147 | console.log(result) 148 | } 149 | 150 | /** 151 | * 验证签名 测试命令 node ./lib/test.js --method=verifySign 152 | * 为防止 new payment()时调用的decodeCertificates函数还未执行完,所以延迟2秒执行,项目中使用无需延迟 153 | */ 154 | this.verifySign = async () => { 155 | setTimeout(async ()=>{ 156 | //timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数 157 | let result = await payment.verifySign({ 158 | timestamp:'1614829763', 159 | nonce:'Eeumuhd3zA5TirWeJUCLCpkENYM8PSUA', 160 | serial:'3DEA336346E96C002B7B0D514D424C8DEDBF9145', 161 | signature:'ame3lX1y6FeXrlBN973M1Dhg5n77M1wVsD3VgeyZlb8c3dz/hpQ+9vNOMBBHGdv8kDIfZUxKDdfoeUaVJhfqAEn9ZV4x112ntEzCHpJtIXQ3rr8fScY7cO71EN/QyQHtY1Ovt8U2Yr891iYaLujUrBHtWrhiR6UKecRA+/RgsUBYh4D10rrqW5ywNrLVN+PSuG4QB85bz3jSslMvRrSG7HP/Xwo3e2sWMDuQ2Uadefu+8/FK1P3KDLDO2fq5teSaaqs7oof2WpV6zrVtyQ+P4p5t8NJ0ExlOSAs2xGJ0+xi+U996tq3VYZXf/4nVsfGW9rn0m/mOrYTmiST9PF+q1g==', 162 | body:'{"id":"3b66121d-c9b9-5d61-9d92-eeec248e993d","create_time":"2021-03-04T11:49:23+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"PB305U6jR6TN8mBzbzGts5TaKnDXQt/7C+uJpGnvOT1SyCvI18L4f42eTZtrZv+5XUdOkxwEHGWDVl2MwbvpgLjLdjyisaHc+uRQCDoYlusiaeDJzd515Rl36nqmdPD8xFKZahWZBBkAlCgXLuW3qcdxSISTk/pyqPziwUtFKfMeq3LEEm4z8DfBM9cVXJrN8EiY2WaQsm+lGnZAV4+pxCELj67xmccXs3JgJwHSKE4exqW919atQWTwJHzuP3WNd+Xvp0zwm9RtDPTvZ8egehqqBw+DARC5jg8MmDtlMR2sTgH2xq6b4+QqLXPPIooOyvEZKMOteSI4FmSfPNwDfZ26D4ga9yGRIxSQKkWDq3QRNhOzvmSkCax08t2hdq12NxBSE9y7aZkjKIr4/uMEtKDU/3wcSoVKlawfN1hlCKo2nWbdKH1avRvc6FAFxXHtXRw0Y0MRnSk8gPMF/T+QqEMRJniXbrylt21xR0AEKbIVk0xK9jvhXex0AvST4x3eKM0r4DXkmL/pCjo1XmZLZIMc2uJ1jJEyqWcURXirrxADCATIAEWOu1hNL6PE","associated_data":"transaction","nonce":"KcsMoPx5UW1i"}}' 163 | 164 | }) 165 | console.log(result) 166 | },2000) 167 | } 168 | 169 | 170 | //解密支付退款通知资源数据 测试命令 node ./lib/test.js --method=decodeResource 171 | this.decodeResource = async () => { 172 | let result = await payment.decodeResource({ 173 | "original_type":"refund", 174 | "algorithm":"AEAD_AES_256_GCM", 175 | "ciphertext":"d2Zi2VToOGXqB3K6bgQaFKktgA3AHm+cJg0vGZPcD22OUZ+CBymtrFJsFtaKKEwebSDN8Habic7NJVpKJpAxZd8ejm32v4UePg139/gj+X7vJtqB39ZkjZXLH973LT5R5yZQ351R3onlpx9JILN2+FNEbrUNenjgEufuQn45b9jwGSBX/sU6n/+gsCdt8+sSkbMy37sSX1bjMicHzte27fR0QSuO1TDjZjjDqP2ou0j7Jb+x9RRtWlbZ1hOYe7AhSTFzOXvkdCq0M6P6ja1cc2olV9xG8UzKxZN0JLnoqIGWwPzTVOPqmt/N3/MrzCK3TT1mNagBnhqEvSXhL9KUjpAIY8J6tkjfoG+9QwnJA8kW48C3nGsgePvNYvikJooQii7rx78Y2paR7cS8Pn8+sxKg4q91DiovBSdW2/ePDruI6SH/FWFrPmLQCG11fCjz/C9o6bqjaSsHKMaSVSAW9e/et04MP6GcZIDweG5AN9FgOXMI", 176 | "associated_data":"refund", 177 | "nonce":"AqfRSFm7h9Sa" 178 | }) 179 | console.log(result) 180 | } 181 | 182 | //申请交易账单 测试命令 node ./lib/test.js --method=tradebill 183 | this.tradebill = async () => { 184 | let result = await payment.tradebill({ 185 | bill_date:'2021-03-03' 186 | }) 187 | console.log(result) 188 | } 189 | 190 | //申请资金账单 测试命令 node ./lib/test.js --method=fundflowbill 191 | this.fundflowbill = async () => { 192 | let result = await payment.fundflowbill({ 193 | bill_date:'2021-03-03' 194 | }) 195 | console.log(result) 196 | } 197 | 198 | //下载账单 测试命令 node ./lib/test.js --method=downloadbill 199 | this.downloadbill = async () => { 200 | let result = await payment.downloadbill('https://api.mch.weixin.qq.com/v3/billdownload/file?token=ktWgOuBvGNvmCk0NaOTMF41tG3yWsZrdM4zdgl10r1GRRNo4tG5V9mPi04ku-PY8&tartype=gzip') 201 | console.log(result) 202 | } 203 | 204 | 205 | const args = require('minimist')(process.argv.slice(2)) 206 | this[args['method']]() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wxpay-v3", 3 | "version": "3.0.2", 4 | "description": "微信支付API v3 for nodejs", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/yangfuhe/node-wxpay.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/yangfuhe/node-wxpay/issues" 20 | }, 21 | "homepage": "https://github.com/yangfuhe/node-wxpay#readme", 22 | "dependencies": { 23 | "@peculiar/x509": "^1.2.1", 24 | "jsrsasign": "^10.1.12", 25 | "urllib": "^2.36.1" 26 | } 27 | } 28 | --------------------------------------------------------------------------------