wxpay.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /* eslint-disable strict */
  2. const urllib = require('urllib');
  3. const { KJUR, hextob64 } = require('jsrsasign');
  4. const assert = require('assert');
  5. // const nodeAesGcm = require('node-aes-gcm');
  6. const crypto = require('crypto');
  7. const x509 = require('@peculiar/x509');
  8. class Payment {
  9. constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
  10. assert(appid, 'appid is required');
  11. assert(mchid, 'mchid is required');
  12. assert(private_key, 'private_key is required');
  13. assert(serial_no, 'serial_no is required');
  14. assert(apiv3_private_key, 'apiv3_private_key is required');
  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. this.urls = {
  22. jsapi: () => {
  23. return {
  24. url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
  25. method: 'POST',
  26. pathname: '/v3/pay/transactions/jsapi',
  27. };
  28. },
  29. app: () => {
  30. return {
  31. url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/app',
  32. method: 'POST',
  33. pathname: '/v3/pay/transactions/app',
  34. };
  35. },
  36. h5: () => {
  37. return {
  38. url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5',
  39. method: 'POST',
  40. pathname: '/v3/pay/transactions/h5',
  41. };
  42. },
  43. native: () => {
  44. return {
  45. url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
  46. method: 'POST',
  47. pathname: '/v3/pay/transactions/native',
  48. };
  49. },
  50. getTransactionsById: ({ pathParams }) => {
  51. return {
  52. url: `https://api.mch.weixin.qq.com/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
  53. method: 'GET',
  54. pathname: `/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
  55. };
  56. },
  57. getTransactionsByOutTradeNo: ({ pathParams }) => {
  58. return {
  59. url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
  60. method: 'GET',
  61. pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
  62. };
  63. },
  64. close: ({ pathParams }) => {
  65. return {
  66. url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
  67. method: 'POST',
  68. pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
  69. };
  70. },
  71. refund: () => {
  72. return {
  73. url: 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
  74. method: 'POST',
  75. pathname: '/v3/refund/domestic/refunds',
  76. };
  77. },
  78. getRefund: ({ pathParams }) => {
  79. return {
  80. url: `https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
  81. method: 'GET',
  82. pathname: `/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
  83. };
  84. },
  85. getCertificates: () => {
  86. return {
  87. url: 'https://api.mch.weixin.qq.com/v3/certificates',
  88. method: 'GET',
  89. pathname: '/v3/certificates',
  90. };
  91. },
  92. tradebill: ({ queryParams }) => {
  93. const { bill_date, bill_type, tar_type } = queryParams;
  94. return {
  95. 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 : ''}`,
  96. method: 'GET',
  97. pathname: `/v3/bill/tradebill?bill_date=${bill_date}${bill_type ? '&bill_type=' + bill_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
  98. };
  99. },
  100. fundflowbill: ({ queryParams }) => {
  101. const { bill_date, account_type, tar_type } = queryParams;
  102. return {
  103. 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 : ''}`,
  104. method: 'GET',
  105. pathname: `/v3/bill/fundflowbill?bill_date=${bill_date}${account_type ? '&account_type=' + account_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
  106. };
  107. },
  108. downloadbill: ({ pathParams }) => {
  109. const url = pathParams;
  110. const index = url.indexOf('/v3');
  111. const pathname = url.substr(index);
  112. return {
  113. url,
  114. method: 'GET',
  115. pathname,
  116. };
  117. },
  118. };
  119. this.decodeCertificates();
  120. }
  121. // 调用封装 制作签名
  122. async run({ pathParams, queryParams, bodyParams, type }) {
  123. assert(type, 'type is required');
  124. const { url, method, pathname } = this.urls[type]({ pathParams, queryParams });
  125. const timestamp = Math.floor(Date.now() / 1000);
  126. const onece_str = this.generate();
  127. const bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '';
  128. const signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA');
  129. const Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`;
  130. const { status, data } = await urllib.request(url, {
  131. method,
  132. dataType: 'text',
  133. data: method === 'GET' ? '' : bodyParams,
  134. timeout: [ 10000, 15000 ],
  135. headers: {
  136. 'Content-Type': 'application/json',
  137. Accept: 'application/json',
  138. Authorization,
  139. },
  140. });
  141. return { status, data };
  142. }
  143. // jsapi统一下单
  144. async jsapi(params) {
  145. const bodyParams = {
  146. ...params,
  147. appid: this.appid,
  148. mchid: this.mchid,
  149. notify_url: params.notify_url || this.notify_url,
  150. };
  151. return await this.run({ bodyParams, type: 'jsapi' });
  152. }
  153. // app统一下单
  154. async app(params) {
  155. const bodyParams = {
  156. ...params,
  157. appid: this.appid,
  158. mchid: this.mchid,
  159. notify_url: params.notify_url || this.notify_url,
  160. };
  161. return await this.run({ bodyParams, type: 'app' });
  162. }
  163. // h5统一下单
  164. async h5(params) {
  165. const bodyParams = {
  166. ...params,
  167. appid: this.appid,
  168. mchid: this.mchid,
  169. notify_url: params.notify_url || this.notify_url,
  170. };
  171. return await this.run({ bodyParams, type: 'h5' });
  172. }
  173. // native统一下单
  174. async native(params) {
  175. const bodyParams = {
  176. ...params,
  177. appid: this.appid,
  178. mchid: this.mchid,
  179. notify_url: params.notify_url || this.notify_url,
  180. };
  181. return await this.run({ bodyParams, type: 'native' });
  182. }
  183. // 通过transaction_id查询订单
  184. async getTransactionsById(params) {
  185. return await this.run({ pathParams: params, type: 'getTransactionsById' });
  186. }
  187. // 通过out_trade_no查询订单
  188. async getTransactionsByOutTradeNo(params) {
  189. return await this.run({ pathParams: params, type: 'getTransactionsByOutTradeNo' });
  190. }
  191. // 关闭订单
  192. async close(params) {
  193. return await this.run({ pathParams: {
  194. out_trade_no: params.out_trade_no,
  195. }, bodyParams: {
  196. mchid: this.mchid,
  197. }, type: 'close' });
  198. }
  199. // 退款
  200. async refund(params) {
  201. const bodyParams = {
  202. ...params,
  203. notify_url: params.notify_url || this.notify_url,
  204. };
  205. return await this.run({ bodyParams, type: 'refund' });
  206. }
  207. // 查询单笔退款订单
  208. async getRefund(params) {
  209. return await this.run({ pathParams: params, type: 'getRefund' });
  210. }
  211. // 获取平台证书列表
  212. async getCertificates() {
  213. return await this.run({ type: 'getCertificates' });
  214. }
  215. // 解密证书列表 解出CERTIFICATE以及public key
  216. async decodeCertificates() {
  217. const result = await this.getCertificates();
  218. if (result.status !== 200) {
  219. throw new Error('获取证书列表失败');
  220. }
  221. const certificates = typeof result.data === 'string' ? JSON.parse(result.data).data : result.data.data;
  222. for (const cert of certificates) {
  223. const plaintext = this.decode(cert.encrypt_certificate);
  224. cert.decrypt_certificate = plaintext.toString();
  225. const beginIndex = cert.decrypt_certificate.indexOf('-\n');
  226. const endIndex = cert.decrypt_certificate.indexOf('\n-');
  227. const str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex);
  228. const x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
  229. const public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64');
  230. cert.public_key = '-----BEGIN PUBLIC KEY-----\n' + public_key + '\n-----END PUBLIC KEY-----';
  231. }
  232. // eslint-disable-next-line no-return-assign
  233. return this.certificates = certificates;
  234. }
  235. // 验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
  236. async verifySign({ timestamp, nonce, serial, body, signature }, repeatVerify = true) {
  237. const data = `${timestamp}\n${nonce}\n${typeof body === 'string' ? body : JSON.stringify(body)}\n`;
  238. const verify = crypto.createVerify('RSA-SHA256');
  239. verify.update(Buffer.from(data));
  240. let verifySerialNoPass = false;
  241. for (const cert of this.certificates) {
  242. if (cert.serial_no === serial) {
  243. verifySerialNoPass = true;
  244. return verify.verify(cert.public_key, signature, 'base64');
  245. }
  246. }
  247. if (!verifySerialNoPass && repeatVerify) {
  248. await this.decodeCertificates();
  249. return await this.verifySign({ timestamp, nonce, serial, body, signature }, false);
  250. }
  251. throw new Error('平台证书序列号不相符');
  252. }
  253. // 申请交易账单
  254. async tradebill(params) {
  255. return await this.run({ queryParams: params, type: 'tradebill' });
  256. }
  257. // 申请资金账单
  258. async fundflowbill(params) {
  259. return await this.run({ queryParams: params, type: 'fundflowbill' });
  260. }
  261. // 下载账单
  262. async downloadbill(download_url) {
  263. return await this.run({ pathParams: download_url, type: 'downloadbill' });
  264. }
  265. // 解密支付退款通知资源数据
  266. decodeResource(resource) {
  267. const { plaintext } = this.decode(resource);
  268. return JSON.parse(plaintext.toString());
  269. }
  270. // 解密
  271. decode(params) {
  272. const AUTH_KEY_LENGTH = 16;
  273. // ciphertext = 密文,associated_data = 填充内容, nonce = 位移
  274. const { ciphertext, associated_data, nonce } = params;
  275. // 密钥
  276. const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
  277. // 位移
  278. const nonce_bytes = Buffer.from(nonce, 'utf8');
  279. // 填充内容
  280. const associated_data_bytes = Buffer.from(associated_data, 'utf8');
  281. // 密文Buffer
  282. const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
  283. // 计算减去16位长度
  284. const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
  285. // upodata
  286. const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
  287. // tag
  288. const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
  289. const decipher = crypto.createDecipheriv(
  290. 'aes-256-gcm', key_bytes, nonce_bytes
  291. );
  292. decipher.setAuthTag(auth_tag_bytes);
  293. decipher.setAAD(Buffer.from(associated_data_bytes));
  294. const output = Buffer.concat([
  295. decipher.update(cipherdata_bytes),
  296. decipher.final(),
  297. ]);
  298. // output = Buffer对象
  299. return output;
  300. // return nodeAesGcm.decrypt(key_bytes, nonce_bytes, cipherdata_bytes, associated_data_bytes, auth_tag_bytes);
  301. }
  302. // 生成随机字符串
  303. generate(length = 32) {
  304. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  305. let noceStr = '',
  306. // eslint-disable-next-line prefer-const
  307. maxPos = chars.length;
  308. // eslint-disable-next-line no-bitwise
  309. while (length--) noceStr += chars[Math.random() * maxPos | 0];
  310. return noceStr;
  311. }
  312. /*
  313. * rsa签名
  314. * 签名内容
  315. * 私钥,PKCS#1
  316. * hash算法,SHA256withRSA,SHA1withRSA
  317. * 返回签名字符串,base64
  318. */
  319. rsaSign(content, privateKey, hash = 'SHA256withRSA') {
  320. // 创建 Signature 对象
  321. const signature = new KJUR.crypto.Signature({
  322. alg: hash,
  323. // !这里指定 私钥 pem!
  324. prvkeypem: privateKey,
  325. });
  326. signature.updateString(content);
  327. const signData = signature.sign();
  328. // 将内容转成base64
  329. return hextob64(signData);
  330. }
  331. }
  332. module.exports = Payment;