recharge-client.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. 'use strict';
  2. const { randomBytes } = require('crypto');
  3. const DEFAULT_PARSE_FUNCTIONS_URL = 'https://server.fmode.cn/parse/functions';
  4. const DEFAULT_PAY_COMPANY = '1AiWpTEDH9';
  5. const DEFAULT_NEW_API_CALLBACK = 'WbyPrzMI3A';
  6. function createTradeNo(prefix = 'VOC') {
  7. const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '');
  8. return `${prefix}${stamp}${randomBytes(4).toString('hex')}`;
  9. }
  10. function buildRechargePaymentRequest({ amount, title = 'VOC 数据调用充值', tradeNo, sessionToken } = {}) {
  11. const numericAmount = Number(amount);
  12. if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
  13. throw new Error('充值金额必须大于 0');
  14. }
  15. const outTradeNo = tradeNo || createTradeNo();
  16. const parseFunctionsUrl = String(process.env.FMODE_PARSE_FUNCTIONS_URL || DEFAULT_PARSE_FUNCTIONS_URL).replace(/\/$/, '');
  17. const body = {
  18. company: process.env.FMODE_PAY_COMPANY || DEFAULT_PAY_COMPANY,
  19. out_trade_no: outTradeNo,
  20. total_fee: Number(numericAmount.toFixed(2)),
  21. body: String(title).slice(0, 80),
  22. fun_id: process.env.FMODE_NEWAPI_RECHARGE_CALLBACK_FUN_ID || DEFAULT_NEW_API_CALLBACK
  23. };
  24. return {
  25. endpoint: `${parseFunctionsUrl}/pay_code2`,
  26. method: 'POST',
  27. headers: sessionToken ? { 'X-Parse-Session-Token': sessionToken } : {},
  28. body,
  29. tradeNo: outTradeNo,
  30. amountCny: body.total_fee,
  31. polling: {
  32. endpoint: `${parseFunctionsUrl}/order_status2`,
  33. method: 'POST',
  34. body: {
  35. out_trade_no: outTradeNo,
  36. company: body.company
  37. }
  38. }
  39. };
  40. }
  41. async function requestPaymentQr({ amount, title, sessionToken, tradeNo, fetchImpl = fetch } = {}) {
  42. const request = buildRechargePaymentRequest({ amount, title, sessionToken, tradeNo });
  43. const response = await fetchImpl(request.endpoint, {
  44. method: request.method,
  45. headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...request.headers },
  46. body: JSON.stringify(request.body)
  47. });
  48. const payload = await response.json().catch(() => ({}));
  49. const codeUrl = Array.isArray(payload?.code_url) ? payload.code_url[0] : payload?.code_url;
  50. if (!response.ok || !codeUrl) {
  51. const error = new Error(payload?.error || payload?.message || `pay_code2 HTTP ${response.status}`);
  52. error.httpStatus = response.status;
  53. throw error;
  54. }
  55. const nonceStr = payload?.nonce_str || '';
  56. return {
  57. ...request,
  58. paymentMode: 'qr',
  59. qrCodeUrl: String(codeUrl),
  60. nonceStr,
  61. polling: {
  62. ...request.polling,
  63. body: {
  64. ...request.polling.body,
  65. ...(nonceStr ? { nonce_str: nonceStr } : {})
  66. }
  67. }
  68. };
  69. }
  70. module.exports = {
  71. DEFAULT_PARSE_FUNCTIONS_URL,
  72. createTradeNo,
  73. buildRechargePaymentRequest,
  74. requestPaymentQr
  75. };