Browse Source

fix: 易支付轮询直连并规范化请求

禁用系统代理访问支付平台,修复 ERR_FR_TOO_MANY_REDIRECTS 重定向循环。

Co-authored-by: Cursor <cursoragent@cursor.com>
Pchen. 5 days ago
parent
commit
82fd924d35
2 changed files with 109 additions and 8 deletions
  1. 7 8
      apis/Order/CreateOrder.js
  2. 102 0
      lib/PaymentClient.js

+ 7 - 8
apis/Order/CreateOrder.js

@@ -4,12 +4,12 @@ const Redis = require('../../plugin/DataBase/Redis')
 const { BaseStdResponse } = require("../../BaseStdResponse.js")
 const AccessControl = require("../../lib/AccessControl.js")
 const crypto = require('crypto')
-const axios = require('axios')
 const config = require('../../config.json')
 const mq = require('../../plugin/mq')
 const { mq: mqName } = require('../../plugin/mq/mqPrefix')
 const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
 const { validateCoupon, recordUsage, releaseUsageForOrder, roundMoney } = require('../../lib/CouponService')
+const { normalizePayBaseUrl, queryPaymentOrder } = require('../../lib/PaymentClient')
 
 const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
 let orderPaymentWorkerStarted = false
@@ -92,8 +92,6 @@ async function pollOrderPaymentStatus(orderId, logger) {
     const MAX_RETRIES = 120 // 5分钟 / 5秒
     const DELAY = 2500 // 5秒
 
-    const queryUrl = `${paymentConfig.url}/api.php?act=order&pid=${paymentConfig.pid}&key=${paymentConfig.key}&out_trade_no=${orderId}`
-
     const pollOrderStatus = async (retry = 0) => {
         if (retry >= MAX_RETRIES) {
             const closeRes = await db.query(
@@ -108,9 +106,8 @@ async function pollOrderPaymentStatus(orderId, logger) {
         }
 
         try {
-            const queryRes = await axios.get(queryUrl)
-            const queryData = queryRes.data
-            console.log(`轮询订单支付状态,订单号:${orderId},尝试次数:${retry + 1},查询结果:${JSON.stringify(queryData)}`)
+            const queryData = await queryPaymentOrder(orderId, logger)
+            logger.info(`轮询订单支付状态,订单号:${orderId},尝试次数:${retry + 1},查询结果:${JSON.stringify(queryData)}`)
 
             if (queryData.code == 1 && queryData.status == 1) {
                 const { trade_no, out_trade_no, type } = queryData
@@ -165,7 +162,7 @@ async function pollOrderPaymentStatus(orderId, logger) {
             // 未支付,继续轮询
             setTimeout(() => pollOrderStatus(retry + 1), DELAY)
         } catch (error) {
-            logger.warn(`轮询支付状态失败:${error.stack || error}`)
+            logger.warn(`轮询支付状态失败,订单号:${orderId},原因:${error.message || error}`)
             setTimeout(() => pollOrderStatus(retry + 1), DELAY)
         }
     }
@@ -337,6 +334,8 @@ class CreateOrder extends API {
                     })
                 }
 
+                const payBaseUrl = normalizePayBaseUrl(paymentConfig.url)
+
                 const deviceType = req.headers['device-type'] ?? '浏览器'
                 let return_url
                 if(deviceType === 'RunForge Uniapp Client') 
@@ -383,7 +382,7 @@ class CreateOrder extends API {
                     ...BaseStdResponse.OK,
                     id: orderId,
                     pay: {
-                        payUrl: `${paymentConfig.url}/submit.php`,
+                        payUrl: `${payBaseUrl}/submit.php`,
                         payData: payParams
                     }
                 })

+ 102 - 0
lib/PaymentClient.js

@@ -0,0 +1,102 @@
+const axios = require('axios')
+const config = require('../config.json')
+
+function getPaymentConfig() {
+    return config.pay || {}
+}
+
+function normalizePayBaseUrl(url) {
+    return String(url || '').trim().replace(/\/+$/, '')
+}
+
+function buildPaymentApiUrl(act, params = {}) {
+    const paymentConfig = getPaymentConfig()
+    const baseUrl = normalizePayBaseUrl(paymentConfig.url)
+    if (!baseUrl) {
+        throw new Error('支付配置错误')
+    }
+
+    const url = new URL(`${baseUrl}/api.php`)
+    url.searchParams.set('act', act)
+    for (const [key, value] of Object.entries(params)) {
+        if (value === undefined || value === null || value === '') continue
+        url.searchParams.set(key, String(value))
+    }
+    return url.toString()
+}
+
+function parsePaymentResponseBody(data) {
+    if (data && typeof data === 'object') return data
+    if (typeof data !== 'string') return null
+    const text = data.trim()
+    if (!text) return null
+    try {
+        return JSON.parse(text)
+    } catch (_) {
+        return null
+    }
+}
+
+function getAxiosPaymentOptions(extra = {}) {
+    return {
+        timeout: 15000,
+        maxRedirects: 5,
+        proxy: false,
+        validateStatus: (status) => status >= 200 && status < 500,
+        ...extra
+    }
+}
+
+function formatPaymentHttpError(error, fallbackMessage) {
+    if (error?.code === 'ECONNABORTED') {
+        return '支付平台响应超时,请稍后重试'
+    }
+    if (error?.code === 'ERR_FR_TOO_MANY_REDIRECTS') {
+        return '支付平台重定向异常,请检查 pay.url 配置或关闭系统代理后重试'
+    }
+    const status = Number(error?.response?.status || 0)
+    const parsed = parsePaymentResponseBody(error?.response?.data)
+    if (parsed?.msg) return String(parsed.msg)
+    if (status === 503) return '支付平台暂时不可用,请稍后重试'
+    if (status >= 500) return `支付平台异常(${status}),请稍后重试`
+    return fallbackMessage || error?.message || '支付平台请求失败'
+}
+
+async function queryPaymentOrder(orderId, logger) {
+    const paymentConfig = getPaymentConfig()
+    if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key) {
+        throw new Error('支付配置错误')
+    }
+
+    const queryUrl = buildPaymentApiUrl('order', {
+        pid: paymentConfig.pid,
+        key: paymentConfig.key,
+        out_trade_no: orderId
+    })
+
+    let response
+    try {
+        response = await axios.get(queryUrl, getAxiosPaymentOptions())
+    } catch (error) {
+        const message = formatPaymentHttpError(error, '查询支付状态失败')
+        logger?.warn?.(`查询支付状态失败,订单号:${orderId},URL:${queryUrl},原因:${message}`)
+        throw new Error(message)
+    }
+
+    const result = parsePaymentResponseBody(response.data)
+    if (!result) {
+        logger?.warn?.(`支付平台返回非 JSON,订单号:${orderId},HTTP ${response.status}`)
+        throw new Error('支付平台响应异常')
+    }
+
+    return result
+}
+
+module.exports = {
+    normalizePayBaseUrl,
+    buildPaymentApiUrl,
+    getAxiosPaymentOptions,
+    formatPaymentHttpError,
+    parsePaymentResponseBody,
+    queryPaymentOrder
+}