├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
74 |
75 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | true
89 | DEFINITION_ORDER
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
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 |
202 |
203 |
204 |
205 |
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 |
238 |
239 |
240 |
241 |
242 | 1615426221422
243 |
244 |
245 | 1615426221422
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
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 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
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 |
--------------------------------------------------------------------------------