Browse Source

feat: improve order payment and runtime config

Pchen. 2 weeks ago
parent
commit
3d7410189a

+ 35 - 105
apis/Order/CallBack.js

@@ -1,39 +1,21 @@
-const API = require("../../lib/API.js")
-const db = require("../../plugin/DataBase/db.js")
-const { BaseStdResponse } = require("../../BaseStdResponse.js")
-const config = require('../../config.json')
-const crypto = require("crypto")
-const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
-const { releaseUsageForOrder } = require('../../lib/CouponService')
-
-const PAYMENT_KEY = config.pay.key
-
-async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
-    const delta = Number(addCount || 0)
-    if (!orderId || !userUuid || delta === 0) return
-    try {
-        const userRows = await db.query('SELECT lepao_count FROM users WHERE uuid = ?', [userUuid])
-        if (!userRows || userRows.length !== 1) return
-        const afterCount = Number(userRows[0].lepao_count || 0)
-        const beforeCount = afterCount - delta
-        await insertLedgerRecord({
-            userUuid,
-            delta,
-            balanceBefore: beforeCount,
-            balanceAfter: afterCount,
-            bizType: 'purchase',
-            bizId: orderId,
-            remark: `订单号:${orderId}`
-        })
-    } catch (error) {
-        logger?.error?.(`写入购买里程流水失败 ${orderId}: ${error.stack || error}`)
-    }
+const API = require('../../lib/API')
+const { BaseStdResponse } = require('../../BaseStdResponse')
+const crypto = require('crypto')
+const { resolveBusinessOrderId } = require('../../lib/OrderPaymentAttempt')
+const { getPaymentConfig } = require('../../lib/PaymentClient')
+const { completePaidOrder } = require('../../lib/OrderSettlement')
+
+function buildSign(params, key) {
+    const raw = Object.keys(params)
+        .sort()
+        .map(name => `${name}=${params[name]}`)
+        .join('&') + key
+    return crypto.createHash('md5').update(raw, 'utf8').digest('hex')
 }
 
 class CallBack extends API {
     constructor() {
         super()
-
         this.noEncrypt()
         this.setPath('/Order/CallBack')
         this.setMethod('GET')
@@ -45,92 +27,40 @@ class CallBack extends API {
             trade_status, sign, sign_type
         } = req.query
 
-        // 参数校验
         if ([pid, trade_no, out_trade_no, type, name, money, trade_status, sign, sign_type].some(v => v === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
         }
 
-        this.logger.info(`收到支付回调订单号:${out_trade_no}`)
+        this.logger.info(`收到支付回调,网关订单号:${out_trade_no}`)
 
-        // 签名校验
-        const rawParams = {
-            money, name, out_trade_no, pid, trade_no, trade_status, type
+        const paymentConfig = await getPaymentConfig()
+        const rawParams = { money, name, out_trade_no, pid, trade_no, trade_status, type }
+        const localSign = buildSign(rawParams, paymentConfig.key)
+        if (localSign.toLowerCase() !== String(sign).toLowerCase()) {
+            this.logger.error(`支付回调签名校验失败,网关订单号:${out_trade_no}`)
+            return res.send('sign fail')
         }
-        const sortedKeys = Object.keys(rawParams).sort()
-        const preSignStr = sortedKeys.map(key => `${key}=${rawParams[key]}`).join('&') + PAYMENT_KEY
-        const localSign = crypto.createHash('md5').update(preSignStr, 'utf8').digest('hex')
 
-        if (localSign.toLowerCase() !== sign.toLowerCase()) {
-            this.logger.error(`签名校验失败,订单号:${out_trade_no}`)
-            return res.send('签名验证失败')
-        }
+        const orderId = await resolveBusinessOrderId(out_trade_no)
 
-        // 支付未成功,标记为失败
         if (trade_status !== 'TRADE_SUCCESS') {
-            const sql = 'UPDATE orders SET state = 3, pay_id = ? WHERE orderId = ? AND state = 0'
-            const updateRes = await db.query(sql, [trade_no, out_trade_no])
-            if (updateRes?.affectedRows > 0) {
-                await releaseUsageForOrder(out_trade_no)
-            }
-            this.logger.info(`支付未成功。订单号:${out_trade_no}`)
+            this.logger.info(`支付回调未成功,忽略并等待订单超时处理。网关订单号:${out_trade_no},业务订单号:${orderId}`)
             return res.send('success')
         }
 
-        try {
-            // 更新订单状态为已支付(state=1)
-            const time = new Date().getTime()
-            let sql = 'UPDATE orders SET state = 1, pay_type = ?, pay_id = ?, pay_time = ? WHERE orderId = ? AND state = 0'
-            const result = await db.query(sql, [type, trade_no, time, out_trade_no])
-
-            if (result.affectedRows > 0) {
-                // 查询订单与商品信息
-                sql = `
-                    SELECT 
-                        g.lepao_count,
-                        a.create_user
-                    FROM 
-                        orders a
-                    LEFT JOIN 
-                        goods g 
-                    ON 
-                        a.goods_id = g.id
-                    WHERE 
-                        a.orderId = ?
-                `
-                const rows = await db.query(sql, [out_trade_no])
-                if (!rows || rows.length !== 1) {
-                    this.logger.error(`订单商品信息异常,订单号:${out_trade_no}`)
-                    await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
-                    return res.send('fail')
-                }
-
-                const { lepao_count, create_user } = rows[0]
-
-                sql = 'UPDATE users SET lepao_count = lepao_count + ? WHERE uuid = ?'
-                const updateUser = await db.query(sql, [lepao_count, create_user])
-                if (!updateUser || updateUser.affectedRows !== 1) {
-                    this.logger.error(`更新用户失败,UUID: ${create_user}`)
-                    await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
-                    return res.send('fail')
-                }
-
-                sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
-                await db.query(sql, [out_trade_no])
-                await writePurchaseLedger(out_trade_no, create_user, lepao_count, this.logger)
-
-                this.logger.info(`支付成功,订单处理完毕。订单号:${out_trade_no}`)
+        const result = await completePaidOrder({
+            orderId,
+            payType: type,
+            payId: trade_no,
+            payTime: Date.now(),
+            paidAmount: money,
+            logger: this.logger
+        })
 
-                return res.send('success')
-            } else {
-                this.logger.warn(`订单不存在或已处理,订单号:${out_trade_no}`)
-                return res.send('订单不存在或已处理')
-            }
-        } catch (err) {
-            this.logger.error(`支付回调处理异常:${err.stack}`)
-            return res.send('服务器错误')
-        }
+        if (result.completed) return res.send('success')
+        if (result.reason === 'state_changed') return res.send('success')
+        this.logger.error(`支付回调处理失败,业务订单号:${orderId},原因:${result.reason}`)
+        return res.send('fail')
     }
 }
 

+ 10 - 40
apis/Order/CancelOrder.js

@@ -1,9 +1,8 @@
 const API = require('../../lib/API')
 const db = require('../../plugin/DataBase/db')
-const Redis = require('../../plugin/DataBase/Redis')
 const { BaseStdResponse } = require('../../BaseStdResponse')
 const AccessControl = require('../../lib/AccessControl')
-const { releaseUsageForOrder } = require('../../lib/CouponService')
+const { closePendingOrder } = require('../../lib/OrderSettlement')
 
 class CancelOrder extends API {
     constructor() {
@@ -16,15 +15,11 @@ class CancelOrder extends API {
         const { uuid, session, orderId } = req.body
 
         if ([uuid, session, orderId].some(v => v === '' || v === null || v === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
         }
 
         if (!await AccessControl.checkSession(uuid, session)) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
         }
 
         try {
@@ -33,48 +28,23 @@ class CancelOrder extends API {
                 [orderId, uuid]
             )
             if (!rows || rows.length !== 1) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '订单不存在'
-                })
+                return res.json({ ...BaseStdResponse.ERR, msg: '订单不存在' })
             }
 
             if (Number(rows[0].state) !== 0) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '仅未支付订单可取消'
-                })
+                return res.json({ ...BaseStdResponse.ERR, msg: '仅未支付订单可取消' })
             }
 
-            const updateRes = await db.query(
-                'UPDATE orders SET state = 3 WHERE orderId = ? AND create_user = ? AND state = 0',
-                [orderId, uuid]
-            )
-            if (!updateRes || updateRes.affectedRows !== 1) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '取消订单失败,订单状态可能已变更'
-                })
-            }
-
-            await releaseUsageForOrder(orderId)
-            try {
-                await Redis.del(`payData:${orderId}`)
-            } catch (redisErr) {
-                this.logger.warn(`清除订单支付缓存失败 ${orderId}: ${redisErr.message || redisErr}`)
+            const closeResult = await closePendingOrder({ orderId, operatorUuid: uuid, logger: this.logger })
+            if (!closeResult.closed) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '取消订单失败,订单状态可能已变更' })
             }
 
             this.logger.info(`用户取消订单成功,订单号:${orderId}`)
-            return res.json({
-                ...BaseStdResponse.OK,
-                msg: '订单已取消'
-            })
+            return res.json({ ...BaseStdResponse.OK, msg: '订单已取消' })
         } catch (error) {
             this.logger.error(`取消订单失败 ${orderId}: ${error.stack || error}`)
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '取消订单失败,请稍后再试'
-            })
+            return res.json({ ...BaseStdResponse.ERR, msg: '取消订单失败,请稍后再试' })
         }
     }
 }

+ 89 - 132
apis/Order/CreateOrder.js

@@ -1,12 +1,11 @@
-const API = require("../../lib/API.js")
-const db = require("../../plugin/DataBase/db.js")
+const API = require('../../lib/API')
+const db = require('../../plugin/DataBase/db')
 const Redis = require('../../plugin/DataBase/Redis')
-const { BaseStdResponse } = require("../../BaseStdResponse.js")
-const AccessControl = require("../../lib/AccessControl.js")
-const crypto = require('crypto')
-const config = require('../../config.json')
+const { BaseStdResponse } = require('../../BaseStdResponse')
+const AccessControl = require('../../lib/AccessControl')
 const { validateCoupon, recordUsage, roundMoney } = require('../../lib/CouponService')
-const { normalizePayBaseUrl } = require('../../lib/PaymentClient')
+const { buildPaymentSession } = require('../../lib/OrderPayment')
+const { createPaymentAttempt, generateGatewayOrderNo } = require('../../lib/OrderPaymentAttempt')
 const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker')
 
 function generateOrderId() {
@@ -17,13 +16,6 @@ function generateOrderId() {
         `${pad(now.getMilliseconds(), 3)}`
 }
 
-function generatePaymentSign(params, key) {
-    const sorted = Object.keys(params).sort()
-    const query = sorted.map(k => `${k}=${params[k]}`).join('&') + key
-
-    return crypto.createHash('md5').update(query, 'utf8').digest('hex')
-}
-
 async function acquireCouponUsageLock(couponId) {
     const lockKey = `coupon:usage:${couponId}`
     const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey])
@@ -32,11 +24,7 @@ async function acquireCouponUsageLock(couponId) {
 
 async function releaseCouponUsageLock(lockKey) {
     if (!lockKey) return
-    try {
-        await db.query('SELECT RELEASE_LOCK(?)', [lockKey])
-    } catch (e) {
-        // 释放失败仅记录,不影响主流程
-    }
+    try { await db.query('SELECT RELEASE_LOCK(?)', [lockKey]) } catch (_) { }
 }
 
 class CreateOrder extends API {
@@ -50,41 +38,31 @@ class CreateOrder extends API {
         const { uuid, session, goods_id, pay_type, coupon_code } = req.body
 
         if ([uuid, session, goods_id, pay_type].some(v => v === '' || v === null || v === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
         }
 
-        const sessionValid = await AccessControl.checkSession(uuid, session)
-        if (!sessionValid) {
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
         }
 
         let couponLockKey = null
         try {
-            const goodsSql = 'SELECT name, price, num, state FROM goods WHERE id = ?'
-            const goodsRows = await db.query(goodsSql, [goods_id])
-
+            const goodsRows = await db.query(
+                'SELECT name, price, num, state FROM goods WHERE id = ? LIMIT 1',
+                [goods_id]
+            )
             if (!goodsRows || goodsRows.length !== 1) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '商品不存在'
-                })
+                return res.json({ ...BaseStdResponse.ERR, msg: '商品不存在' })
             }
 
             const goods = goodsRows[0]
-            if (goods.num < 1 || goods.state !== 1) {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '商品已下架或库存不足'
-                })
+            if (Number(goods.num) < 1 || Number(goods.state) !== 1) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '商品已下架或库存不足' })
             }
 
             const createTime = Date.now()
             const orderId = generateOrderId()
-
+            const normalizedPayType = String(pay_type).trim()
             const originalPrice = roundMoney(goods.price)
             let finalPrice = originalPrice
             let discountAmount = 0
@@ -98,20 +76,11 @@ class CreateOrder extends API {
                     goodsId: goods_id,
                     goodsPrice: goods.price
                 })
-                if (!couponResult.ok) {
-                    return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
-                }
-                finalPrice = couponResult.finalPrice
-                discountAmount = couponResult.discountAmount
-                couponId = couponResult.couponId
-                appliedCouponCode = couponResult.code
+                if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
 
-                const lockRet = await acquireCouponUsageLock(couponId)
+                const lockRet = await acquireCouponUsageLock(couponResult.couponId)
                 if (!lockRet.ok) {
-                    return res.json({
-                        ...BaseStdResponse.ERR,
-                        msg: '优惠码校验繁忙,请稍后重试'
-                    })
+                    return res.json({ ...BaseStdResponse.ERR, msg: '优惠码校验繁忙,请稍后重试' })
                 }
                 couponLockKey = lockRet.lockKey
 
@@ -121,9 +90,7 @@ class CreateOrder extends API {
                     goodsId: goods_id,
                     goodsPrice: goods.price
                 })
-                if (!couponResult.ok) {
-                    return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
-                }
+                if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
 
                 finalPrice = couponResult.finalPrice
                 discountAmount = couponResult.discountAmount
@@ -131,97 +98,87 @@ class CreateOrder extends API {
                 appliedCouponCode = couponResult.code
             }
 
-            const insertSql = `
-                INSERT INTO orders (
-                    orderId, create_user, create_time, goods_id, price, pay_type,
-                    original_price, discount_amount, coupon_id, coupon_code
-                )
-                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-            `
-            const result = await db.query(insertSql, [
+            const gatewayOrderNo = generateGatewayOrderNo(orderId)
+            const payment = await buildPaymentSession({
                 orderId,
-                uuid,
-                createTime,
-                goods_id,
-                finalPrice,
-                pay_type,
-                originalPrice,
-                discountAmount,
-                couponId,
-                appliedCouponCode
-            ])
-
-            const updateSql = 'UPDATE goods SET num = num - 1 WHERE id = ?'
-            await db.query(updateSql, [goods_id])
-
-            if (result && result.affectedRows > 0) {
-                if (couponId) {
-                    await recordUsage(couponId, orderId, uuid, discountAmount)
-                }
-
-                const paymentConfig = config.pay || {}
+                gatewayOrderNo,
+                payType: normalizedPayType,
+                goodsName: goods.name,
+                price: finalPrice,
+                deviceType: req.headers['device-type']
+            })
 
-                if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key || !paymentConfig.return_url) {
-                    return res.json({
-                        ...BaseStdResponse.ERR,
-                        msg: '支付配置错误,请联系管理员'
-                    })
-                }
+            const conn = await db.connect()
+            let insertResult
+            try {
+                await conn.beginTransaction()
 
-                const payBaseUrl = normalizePayBaseUrl(paymentConfig.url)
-
-                const deviceType = req.headers['device-type'] ?? '浏览器'
-                let return_url
-                if (deviceType === 'RunForge Uniapp Client' || deviceType === '沐晨乐跑 Uniapp Client')
-                    return_url = paymentConfig.uni_return_url + orderId
-                else
-                    return_url = paymentConfig.return_url + orderId
-
-                const payParams = {
-                    pid: paymentConfig.pid,
-                    type: pay_type,
-                    out_trade_no: orderId,
-                    notify_url: `${config.url}/Order/CallBack`,
-                    return_url,
-                    name: goods.name,
-                    money: String(finalPrice)
+                const [stockRes] = await conn.execute(
+                    'UPDATE goods SET num = num - 1 WHERE id = ? AND num > 0 AND state = 1',
+                    [goods_id]
+                )
+                if (!stockRes || stockRes.affectedRows !== 1) {
+                    await conn.rollback()
+                    return res.json({ ...BaseStdResponse.ERR, msg: '商品库存不足或已下架' })
                 }
 
-                const sign = generatePaymentSign(payParams, paymentConfig.key)
-                payParams.sign = sign
-                payParams.sign_type = 'MD5'
+                const [orderRes] = await conn.execute(
+                    `INSERT INTO orders (
+                        orderId, create_user, create_time, goods_id, price, pay_type,
+                        original_price, discount_amount, coupon_id, coupon_code
+                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+                    [
+                        orderId,
+                        uuid,
+                        createTime,
+                        goods_id,
+                        finalPrice,
+                        normalizedPayType,
+                        originalPrice,
+                        discountAmount,
+                        couponId,
+                        appliedCouponCode
+                    ]
+                )
+                insertResult = orderRes
 
-                await Redis.set(`payData:${orderId}`, JSON.stringify(payParams), {
-                    EX: 300
+                await createPaymentAttempt({
+                    orderId,
+                    payType: normalizedPayType,
+                    gatewayOrderNo,
+                    executor: conn
                 })
 
-                try {
-                    await enqueueOrderPaymentCheck(orderId)
-                } catch (error) {
-                    this.logger.error(`推送订单支付检查消息到 MQ 失败,订单号:${orderId},错误:${error.stack || error}`)
+                if (couponId) {
+                    await recordUsage(couponId, orderId, uuid, discountAmount, conn)
                 }
 
-                res.json({
-                    ...BaseStdResponse.OK,
-                    id: orderId,
-                    pay: {
-                        payUrl: `${payBaseUrl}/submit.php`,
-                        payData: payParams
-                    }
-                })
+                await conn.commit()
+            } catch (writeError) {
+                try { await conn.rollback() } catch (_) { }
+                throw writeError
+            }
 
-            } else {
-                return res.json({
-                    ...BaseStdResponse.ERR,
-                    msg: '创建订单失败'
-                })
+            if (!insertResult || insertResult.affectedRows < 1) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '创建订单失败' })
             }
-        } catch (err) {
-            this.logger.error(`创建订单失败!${err.stack}`)
+
+            await Redis.set(`payData:${orderId}`, JSON.stringify(payment.payData), { EX: 300 })
+
+            try {
+                await enqueueOrderPaymentCheck(orderId)
+            } catch (error) {
+                this.logger.error(`推送订单支付检查消息失败,订单号:${orderId},错误:${error.stack || error}`)
+            }
+
             return res.json({
-                ...BaseStdResponse.ERR,
-                msg: "创建订单异常,请联系管理员"
+                ...BaseStdResponse.OK,
+                id: orderId,
+                pay: payment
             })
+        } catch (err) {
+            this.logger.error(`创建订单失败:${err.stack || err}`)
+            return res.json({ ...BaseStdResponse.ERR, msg: '创建订单异常,请联系管理员' })
         } finally {
             await releaseCouponUsageLock(couponLockKey)
         }

+ 21 - 3
apis/Order/GetOrderDetail.js

@@ -1,10 +1,12 @@
 const API = require("../../lib/API.js");
 const db = require("../../plugin/DataBase/db.js");
 const Redis = require('../../plugin/DataBase/Redis');
-const config = require('../../config.json');
 const { BaseStdResponse } = require("../../BaseStdResponse.js");
 const AccessControl = require("../../lib/AccessControl.js");
 const { evaluateRefundEligibility } = require('../../lib/OrderRefundService');
+const { getPaymentMethods } = require('../../lib/OrderPayment');
+const { normalizePayBaseUrl } = require('../../lib/PaymentClient');
+const { getPaymentConfig } = require('../../lib/PaymentClient');
 
 class GetAccount extends API {
     constructor() {
@@ -91,6 +93,8 @@ class GetAccount extends API {
         order.canRefund = refundEligibility.canRefund;
         order.refundDisabledReason = refundEligibility.reason;
         order.user_lepao_count = Number(userLepaoCount || 0);
+        order.canChangePayType = Number(order.state) === 0;
+        order.paymentMethods = await getPaymentMethods();
 
         // 订单未支付
         if (order.state === 0) {
@@ -108,18 +112,32 @@ class GetAccount extends API {
                     });
                 }
 
+                order.canChangePayType = Number(order.state) === 0;
+                order.paymentMethods = await getPaymentMethods();
+
                 return res.json({
                     ...BaseStdResponse.OK,
                     data: order
                 });
             }
 
+            let paymentConfig
+            try {
+                paymentConfig = await getPaymentConfig();
+            } catch (error) {
+                this.logger.warn(`读取支付配置失败,订单详情不返回支付表单 ${order.orderId}: ${error.message || error}`)
+                return res.json({
+                    ...BaseStdResponse.OK,
+                    data: order
+                });
+            }
             return res.json({
                 ...BaseStdResponse.OK,
                 data: order,
                 payData: {
-                    payUrl: `${config.pay.url}/submit.php`,
-                    payData: JSON.parse(payData)
+                    payUrl: `${normalizePayBaseUrl(paymentConfig?.url)}/submit.php`,
+                    payData: JSON.parse(payData),
+                    autoSubmit: true
                 }
             });
         }

+ 82 - 0
apis/Order/PayOrder.js

@@ -0,0 +1,82 @@
+const API = require('../../lib/API')
+const db = require('../../plugin/DataBase/db')
+const Redis = require('../../plugin/DataBase/Redis')
+const { BaseStdResponse } = require('../../BaseStdResponse')
+const AccessControl = require('../../lib/AccessControl')
+const { buildPaymentSession } = require('../../lib/OrderPayment')
+const { createPaymentAttempt, generateGatewayOrderNo } = require('../../lib/OrderPaymentAttempt')
+const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker')
+
+class PayOrder extends API {
+    constructor() {
+        super()
+        this.setPath('/Order/Pay')
+        this.setMethod('POST')
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, orderId, pay_type } = req.body
+        if ([uuid, session, orderId, pay_type].some(v => v === '' || v === null || v === undefined)) {
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        }
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
+        }
+
+        try {
+            const normalizedPayType = String(pay_type).trim()
+            const rows = await db.query(`
+                SELECT o.orderId, o.price, o.state, g.name
+                FROM orders o
+                LEFT JOIN goods g ON o.goods_id = g.id
+                WHERE o.orderId = ? AND o.create_user = ?
+                LIMIT 1
+            `, [orderId, uuid])
+            const order = rows?.[0]
+            if (!order) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '订单不存在' })
+            }
+            if (Number(order.state) !== 0) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '仅待付款订单可更换支付方式' })
+            }
+
+            const gatewayOrderNo = generateGatewayOrderNo(order.orderId)
+            const payment = await buildPaymentSession({
+                orderId: order.orderId,
+                gatewayOrderNo,
+                payType: normalizedPayType,
+                goodsName: order.name,
+                price: order.price,
+                deviceType: req.headers['device-type']
+            })
+
+            const updateRes = await db.query(
+                'UPDATE orders SET pay_type = ? WHERE orderId = ? AND create_user = ? AND state = 0',
+                [normalizedPayType, orderId, uuid]
+            )
+            if (!updateRes?.affectedRows) {
+                return res.json({ ...BaseStdResponse.ERR, msg: '订单状态已变更,请刷新后重试' })
+            }
+
+            await createPaymentAttempt({
+                orderId: order.orderId,
+                payType: normalizedPayType,
+                gatewayOrderNo
+            })
+
+            await Redis.set(`payData:${orderId}`, JSON.stringify(payment.payData), { EX: 300 })
+            try {
+                await enqueueOrderPaymentCheck(orderId)
+            } catch (error) {
+                this.logger.error(`投递订单支付检查消息失败,订单号:${orderId},错误:${error.stack || error}`)
+            }
+
+            return res.json({ ...BaseStdResponse.OK, id: orderId, pay: payment })
+        } catch (error) {
+            this.logger.error(`发起订单支付失败,订单号:${orderId},错误:${error.stack || error}`)
+            return res.json({ ...BaseStdResponse.ERR, msg: error.message || '发起支付失败,请稍后重试' })
+        }
+    }
+}
+
+module.exports.PayOrder = PayOrder

+ 49 - 0
apis/System/Admin/RuntimeConfig.js

@@ -0,0 +1,49 @@
+const API = require('../../../lib/API')
+const AccessControl = require('../../../lib/AccessControl')
+const { BaseStdResponse } = require('../../../BaseStdResponse')
+const { CONFIG_KEYS, getAllRuntimeConfigs, saveRuntimeConfig, selectRuntimeConfig } = require('../../../lib/RuntimeConfig')
+
+async function checkAdmin(uuid, session) {
+    if (!await AccessControl.checkSession(uuid, session)) return false
+    const permissions = await AccessControl.getPermission(uuid)
+    return permissions.includes('admin') || permissions.includes('server')
+}
+
+class GetRuntimeConfig extends API {
+    constructor() { super(); this.setPath('/Admin/System/RuntimeConfig'); this.setMethod('GET') }
+    async onRequest(req, res) {
+        const { uuid, session } = req.query
+        if (!uuid || !session) return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        if (!await checkAdmin(uuid, session)) return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
+        return res.json({ ...BaseStdResponse.OK, data: await getAllRuntimeConfigs() })
+    }
+}
+
+class SaveRuntimeConfig extends API {
+    constructor() { super(); this.setPath('/Admin/System/RuntimeConfig'); this.setMethod('PUT') }
+    async onRequest(req, res) {
+        const { uuid, session, config } = req.body
+        if (!uuid || !session || !config || typeof config !== 'object') return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        if (!await checkAdmin(uuid, session)) return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
+        try {
+            if (!CONFIG_KEYS.includes(config.key)) return res.json({ ...BaseStdResponse.ERR, msg: `不支持的配置项:${config.key}` })
+            await saveRuntimeConfig({ ...config, updatedBy: uuid })
+            return res.json({ ...BaseStdResponse.OK })
+        } catch (error) {
+            return res.json({ ...BaseStdResponse.ERR, msg: error.message || '保存配置失败' })
+        }
+    }
+}
+
+class SelectRuntimeConfig extends API {
+    constructor() { super(); this.setPath('/Admin/System/RuntimeConfig/Select'); this.setMethod('PUT') }
+    async onRequest(req, res) {
+        const { uuid, session, key, name } = req.body
+        if (!uuid || !session || !key || !name) return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
+        if (!await checkAdmin(uuid, session)) return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
+        try { await selectRuntimeConfig({ key, name, updatedBy: uuid }); return res.json({ ...BaseStdResponse.OK }) }
+        catch (error) { return res.json({ ...BaseStdResponse.ERR, msg: error.message || '切换配置失败' }) }
+    }
+}
+
+module.exports = { GetRuntimeConfig, SaveRuntimeConfig, SelectRuntimeConfig }

+ 51 - 6
apis/User/uniLogin/GetLoginUrl.js

@@ -1,8 +1,9 @@
 const API = require("../../../lib/API")
 const axios = require('axios')
-const config = require('../../../config.json')
 const { BaseStdResponse } = require("../../../BaseStdResponse");
 const https = require("https")
+const { normalizeSocialType } = require('../../../lib/UniLoginClient')
+const { getRuntimeConfig } = require('../../../lib/RuntimeConfig')
 
 class GetLoginUrl extends API {
     constructor() {
@@ -13,13 +14,57 @@ class GetLoginUrl extends API {
     }
 
     async onRequest(req, res) {
-        let { type, device } = req.query
+        let { type, device, action, from, mode } = req.query
+        type = normalizeSocialType(type)
 
-        const uniConfig = config.unilogin
+        if (!type)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '不支持的第三方登录类型'
+            })
+
+        const uniConfig = await getRuntimeConfig('unilogin', { required: false, defaultValue: null })
+        if (!uniConfig || !uniConfig.url || !uniConfig.appid || !uniConfig.appkey) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '聚合登录暂未配置,请使用账号密码登录'
+            })
+        }
+
+        /** 解析 URL 与 hash 内查询串,判断 query 是否已包含某键(避免对整段 URL 做子串误判) */
+        const redirectUrlHasQueryKey = (redirectUrl, key) => {
+            if (!redirectUrl || !key) return false
+            try {
+                const u = new URL(redirectUrl)
+                if (u.searchParams.has(key)) return true
+                const h = u.hash || ''
+                const qPos = h.indexOf('?')
+                if (qPos === -1) return false
+                return new URLSearchParams(h.slice(qPos + 1)).has(key)
+            } catch {
+                return false
+            }
+        }
+
+        const appendQuery = (redirectUrl) => {
+            const params = []
+            if (mode && !redirectUrlHasQueryKey(redirectUrl, 'mode'))
+                params.push(`mode=${encodeURIComponent(mode)}`)
+            if (action && !redirectUrlHasQueryKey(redirectUrl, 'action'))
+                params.push(`action=${encodeURIComponent(action)}`)
+            if (from && !redirectUrlHasQueryKey(redirectUrl, 'from'))
+                params.push(`from=${encodeURIComponent(from)}`)
+
+            if (params.length === 0)
+                return redirectUrl
+
+            return `${redirectUrl}${redirectUrl.includes('?') ? '&' : '?'}${params.join('&')}`
+        }
 
-        let url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type || 'qq'}&redirect_uri=${encodeURIComponent(uniConfig.return_url)}`
+        let redirectUri = appendQuery(uniConfig.return_url)
+        let url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type}&redirect_uri=${encodeURIComponent(redirectUri)}`
         if (device && device === 'uniapp')
-            url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type || 'qq'}&redirect_uri=${encodeURIComponent(uniConfig.uni_return_url)}`
+            url = `${uniConfig.url}/connect.php?act=login&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${type}&redirect_uri=${encodeURIComponent(appendQuery(uniConfig.uni_return_url))}`
 
         try {
             const r = await axios.get(url, {
@@ -51,4 +96,4 @@ class GetLoginUrl extends API {
     }
 }
 
-module.exports.GetLoginUrl = GetLoginUrl;
+module.exports.GetLoginUrl = GetLoginUrl;

+ 30 - 43
config.example.json

@@ -1,11 +1,6 @@
 {
     "port": 30003,
-    "proxyForwardServer": {
-        "url": "http://127.0.0.1:30010",
-        "enabled": true,
-        "timeout": 120000
-    },
-    "server": "YOUR_SERVER_LABEL",
+    "serverRole": "all",
     "database": {
         "host": "YOUR_MYSQL_HOST",
         "database": "YOUR_DB_NAME",
@@ -15,54 +10,46 @@
         "connectTimeout": 30000
     },
     "redis": {
-        "host": "localhost",
+        "host": "127.0.0.1",
         "port": 6379,
         "password": ""
     },
     "rabbitmq": {
         "url": "amqp://USER:PASSWORD@HOST:5672",
+        "vhost": "/",
         "managementBaseUrl": "http://HOST:15672",
-        "mqPrefix": "",
+        "managementUser": "",
+        "managementPassword": "",
         "reconnectInterval": 5000,
         "lepaoScheduleTickMs": 2000,
-        "lepaoScheduleBatch": 100
+        "lepaoScheduleBatch": 100,
+        "mqPrefix": ""
     },
-    "jkes": {
-        "apiBase": "https://jkes.smart-campus.com.cn:50077/api",
-        "requestTimeoutMs": 30000,
-        "tlsRejectUnauthorized": true,
-        "gpsAltitude": 269.75,
-        "gpsDefaultAccuracy": 6,
-        "paceRandomMinSecPerKm": 180,
-        "paceRandomMaxSecPerKm": 600,
-        "autoSingleRunMaxKm": 10,
-        "autoDistanceStepKm": 0.1
+    "qk": {
+        "passwordAesKey": "CHANGE_ME_TO_A_RANDOM_SECRET",
+        "leaseMs": 90000,
+        "heartbeatTtlSeconds": 45,
+        "leaseWatcherIntervalMs": 30000,
+        "staleTaskMs": 3600000,
+        "memPerSlotMb": 3072,
+        "memReserveMb": 1024,
+        "maxSlotsCap": 10,
+        "slotCapacityFactor": 1.5
     },
-    "email": [
-        {
-            "host": "smtp.example.com",
-            "port": 587,
-            "secure": false,
-            "user": "noreply@example.com",
-            "password": "YOUR_SMTP_PASSWORD",
-            "from": "Display Name <noreply@example.com>"
-        }
-    ],
     "url": "https://your-lepao-api.example.com",
     "url2": "http://127.0.0.1:30004",
-    "url3": "http://127.0.0.1:30001",
-    "pay": {
-        "url": "https://pay.example.com",
-        "pid": 0,
-        "key": "YOUR_PAY_KEY",
-        "return_url": "https://your-site.example/#/store/orderDetail/",
-        "uni_return_url": "https://m.your-site.example/#/pages/order/order-detail?id="
-    },
-    "unilogin": {
-        "url": "https://open.example.com",
-        "appid": "YOUR_APPID",
-        "appkey": "YOUR_APPKEY",
-        "return_url": "https://your-site.example/uniLogin/loginSuccess",
-        "uni_return_url": "https://m.your-site.example/#/pages/login/login"
+    "onebotv11": {
+        "enabled": false,
+        "transport": "reverse_ws",
+        "reverseWsUrl": "ws://127.0.0.1:15700",
+        "reverseWsPath": "/ws",
+        "reverseWsHost": "0.0.0.0",
+        "reverseWsPort": 15700,
+        "reverseWsToken": "YOUR_WS_TOKEN",
+        "callbackToken": "YOUR_CALLBACK_TOKEN",
+        "ticketSenderNickname": "Ticket Bot",
+        "selfId": "your-bot-id",
+        "botName": "BotName",
+        "botUuid": "your-bot-uuid"
     }
 }

+ 37 - 0
docs/order-payment-flow.md

@@ -0,0 +1,37 @@
+# 订单支付流程
+
+创建订单成功后,`POST /Order/CreateOrder` 会返回 `pay.autoSubmit = true`。前端应使用 `pay.payUrl` 与 `pay.payData` 自动提交 POST 表单到支付网关,当前页面保留在订单详情页。
+
+订单详情 `GET /Order/Detail` 在待支付状态下返回:
+
+- `data.canChangePayType`: 是否允许切换付款方式。
+- `data.paymentMethods`: 后台支付配置中启用的支付方式。
+
+用户在订单详情页切换付款方式时调用:
+
+```json
+{ "uuid": "...", "session": "...", "orderId": "...", "pay_type": "wxpay" }
+```
+
+每次支付都会生成独立的易支付 `out_trade_no`,并写入 `order_payment_attempts`。切换支付方式不会尝试修改易支付平台已经创建的旧订单;回调和主动轮询会检查该业务订单下的所有支付尝试,任意一个支付成功都会完成原业务订单。
+
+需要手动执行的表结构:
+
+```sql
+CREATE TABLE order_payment_attempts (
+  gateway_order_no VARCHAR(64) NOT NULL,
+  order_id VARCHAR(64) NOT NULL,
+  pay_type VARCHAR(32) NOT NULL,
+  create_time BIGINT NOT NULL,
+  PRIMARY KEY (gateway_order_no),
+  KEY idx_order_payment_attempt_order_id (order_id),
+  KEY idx_order_payment_attempt_order_time (order_id, create_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+订单关闭规则:
+
+- 用户主动取消待支付订单时,订单状态改为取消,并返还商品库存、释放优惠券占用、删除支付缓存。
+- 轮询超时仍未支付时执行同样的关闭逻辑。
+- 支付平台非成功回调不会立即关闭业务订单,避免旧支付尝试失败影响用户切换后的新支付。
+- 支付成功会校验实付金额与订单金额,金额不一致不会发货。

+ 63 - 0
docs/runtime-config.md

@@ -0,0 +1,63 @@
+# 运行时配置
+
+支付、邮件、聚合登录和代理转发配置存放在数据库 `runtime_configs` 表中。服务不会自动建表,也不会从 `config.json` 导入;请先手动执行 SQL。运行时读取 Redis 缓存,缓存未命中才查 MySQL;后台保存或切换方案会主动删除缓存,因此修改可即时生效。
+
+```sql
+CREATE TABLE runtime_configs (
+  config_key VARCHAR(64) NOT NULL,
+  config_name VARCHAR(64) NOT NULL,
+  config_value JSON NOT NULL,
+  enabled TINYINT(1) NOT NULL DEFAULT 1,
+  is_default TINYINT(1) NOT NULL DEFAULT 0,
+  updated_at BIGINT NOT NULL,
+  updated_by VARCHAR(64) NULL,
+  PRIMARY KEY (config_key, config_name),
+  KEY idx_runtime_configs_active (config_key, enabled, is_default, updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+管理接口:
+
+- `GET /Admin/System/RuntimeConfig`: 获取全部配置方案。
+- `PUT /Admin/System/RuntimeConfig`: 新增或更新一个方案。
+- `PUT /Admin/System/RuntimeConfig/Select`: 切换当前方案。
+
+配置类型:
+
+- `pay`: 易支付配置,包含 `url`、`pid`、`key`、`return_url`、`uni_return_url`、`methods`。
+- `email`: SMTP 账号数组,按 `priority` 从小到大尝试发送。
+- `unilogin`: 聚合登录配置,包含 `url`、`appid`、`appkey` 等。
+- `proxyForwardServer`: 代理转发配置,包含 `enabled`、`url`、`timeout`。
+
+示例初始化语句:
+
+```sql
+INSERT INTO runtime_configs
+(config_key, config_name, config_value, enabled, is_default, updated_at, updated_by)
+VALUES
+('pay', 'default', JSON_OBJECT(
+  'url', 'https://pay.example.com',
+  'pid', '1000',
+  'key', 'CHANGE_ME',
+  'return_url', 'https://your-site.example/#/store/orderDetail/',
+  'uni_return_url', 'https://m.your-site.example/#/pages/order/order-detail?id=',
+  'methods', JSON_ARRAY(
+    JSON_OBJECT('type', 'alipay', 'name', '支付宝'),
+    JSON_OBJECT('type', 'wxpay', 'name', '微信支付')
+  )
+), 1, 1, UNIX_TIMESTAMP(NOW(3)) * 1000, 'init');
+
+INSERT INTO runtime_configs
+(config_key, config_name, config_value, enabled, is_default, updated_at, updated_by)
+VALUES
+('email', 'default', JSON_ARRAY(
+  JSON_OBJECT(
+    'host', 'smtp.example.com',
+    'port', 465,
+    'user', 'service@example.com',
+    'password', 'CHANGE_ME',
+    'from', 'RunForge <service@example.com>',
+    'priority', 0
+  )
+), 1, 1, UNIX_TIMESTAMP(NOW(3)) * 1000, 'init');
+```

+ 13 - 1
lib/API.js

@@ -130,7 +130,19 @@ class API {
                 return originalJson(data)
             }
 
-            await this.onRequest(req, res)
+            try {
+                await this.onRequest(req, res)
+            } catch (err) {
+                this.logger.error(`接口处理异常: ${err.stack || err}`)
+                if (!res.headersSent) {
+                    return res.json({
+                        ...BaseStdResponse.ERR,
+                        msg: err?.code === 'RUNTIME_CONFIG_NOT_FOUND'
+                            ? `${err.key || '运行时配置'} 暂未配置或未启用`
+                            : '服务处理失败,请稍后再试'
+                    })
+                }
+            }
         })
     }
 }

+ 9 - 3
lib/CouponService.js

@@ -128,14 +128,20 @@ async function validateCoupon({ code, userUuid, goodsId, goodsPrice }) {
     }
 }
 
-async function recordUsage(couponId, orderId, userUuid, discountAmount) {
+async function executeSql(executor, sql, params) {
+    if (executor && typeof executor.execute === 'function') return executor.execute(sql, params)
+    return db.query(sql, params)
+}
+
+async function recordUsage(couponId, orderId, userUuid, discountAmount, executor = null) {
     const time = Date.now()
-    await db.query(
+    await executeSql(
+        executor,
         `INSERT INTO coupon_usage (coupon_id, order_id, user_uuid, discount_amount, create_time)
          VALUES (?, ?, ?, ?, ?)`,
         [couponId, orderId, userUuid, discountAmount, time]
     )
-    await db.query('UPDATE coupons SET used_count = used_count + 1 WHERE id = ?', [couponId])
+    await executeSql(executor, 'UPDATE coupons SET used_count = used_count + 1 WHERE id = ?', [couponId])
 }
 
 async function releaseUsageForOrder(orderId) {

+ 1 - 1
lib/Lepao/Worker.js

@@ -1080,7 +1080,7 @@ class Worker {
                     if (needEarlyAck) {
                         safeAck()
                     }
-                    const proxyEnabled = isProxyForwardEnabled()
+                    const proxyEnabled = await isProxyForwardEnabled()
                     const outboundMode = proxyEnabled ? 'proxy' : 'direct'
                     const result = await this.withTimeout(
                         handler(data, { traceId, channel, taskId: id, outboundMode }),

+ 80 - 0
lib/OrderPayment.js

@@ -0,0 +1,80 @@
+const crypto = require('crypto')
+const config = require('../config.json')
+const { normalizePayBaseUrl } = require('./PaymentClient')
+const { getRuntimeConfig } = require('./RuntimeConfig')
+
+const DEFAULT_PAYMENT_METHODS = [
+    { type: 'alipay', name: '支付宝' },
+    { type: 'wxpay', name: '微信支付' }
+]
+
+async function getPaymentMethods() {
+    const paymentConfig = await getRuntimeConfig('pay', {
+        required: false,
+        defaultValue: { methods: DEFAULT_PAYMENT_METHODS }
+    })
+    const methods = paymentConfig?.methods
+    if (!Array.isArray(methods) || methods.length === 0) {
+        return DEFAULT_PAYMENT_METHODS
+    }
+
+    return methods
+        .filter(method => method && typeof method.type === 'string' && method.type.trim())
+        .map(method => ({
+            type: method.type.trim(),
+            name: String(method.name || method.type).trim()
+        }))
+}
+
+async function isSupportedPaymentMethod(payType) {
+    return (await getPaymentMethods()).some(method => method.type === payType)
+}
+
+function generatePaymentSign(params, key) {
+    const query = Object.keys(params)
+        .sort()
+        .map(keyName => `${keyName}=${params[keyName]}`)
+        .join('&') + key
+    return crypto.createHash('md5').update(query, 'utf8').digest('hex')
+}
+
+async function buildPaymentSession({ orderId, gatewayOrderNo = orderId, payType, goodsName, price, deviceType }) {
+    const paymentConfig = await getRuntimeConfig('pay')
+    if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key || !paymentConfig.return_url) {
+        throw new Error('支付配置错误,请联系管理员')
+    }
+    if (!await isSupportedPaymentMethod(payType)) {
+        throw new Error('不支持的支付方式')
+    }
+
+    const isUniapp = deviceType === 'RunForge Uniapp Client'
+    const returnBaseUrl = isUniapp ? paymentConfig.uni_return_url : paymentConfig.return_url
+    if (!returnBaseUrl) {
+        throw new Error('支付返回地址配置错误,请联系管理员')
+    }
+
+    const payData = {
+        pid: paymentConfig.pid,
+        type: payType,
+        out_trade_no: gatewayOrderNo,
+        notify_url: `${config.url}/Order/CallBack`,
+        return_url: `${returnBaseUrl}${orderId}`,
+        name: goodsName,
+        money: String(price)
+    }
+    payData.sign = generatePaymentSign(payData, paymentConfig.key)
+    payData.sign_type = 'MD5'
+
+    return {
+        payUrl: `${normalizePayBaseUrl(paymentConfig.url)}/submit.php`,
+        payData,
+        // 客户端收到后应以 payData 创建并自动提交一个 POST 表单,进入支付网关。
+        autoSubmit: true
+    }
+}
+
+module.exports = {
+    buildPaymentSession,
+    getPaymentMethods,
+    isSupportedPaymentMethod
+}

+ 62 - 0
lib/OrderPaymentAttempt.js

@@ -0,0 +1,62 @@
+const crypto = require('crypto')
+const db = require('../plugin/DataBase/db')
+
+function generateGatewayOrderNo(orderId) {
+    const suffix = `${Date.now().toString(36)}${crypto.randomBytes(3).toString('hex')}`
+    return `${String(orderId).slice(0, 40)}P${suffix}`
+}
+
+function normalizeResult(result) {
+    return Array.isArray(result) ? result[0] : result
+}
+
+async function executeSql(executor, sql, params) {
+    if (executor && typeof executor.execute === 'function') return executor.execute(sql, params)
+    return db.query(sql, params)
+}
+
+async function createPaymentAttempt({ orderId, payType, gatewayOrderNo = generateGatewayOrderNo(orderId), executor = null }) {
+    const result = await executeSql(
+        executor,
+        'INSERT INTO order_payment_attempts (gateway_order_no, order_id, pay_type, create_time) VALUES (?, ?, ?, ?)',
+        [gatewayOrderNo, orderId, payType, Date.now()]
+    )
+    if (!normalizeResult(result)?.affectedRows) throw new Error('保存支付尝试失败')
+    return gatewayOrderNo
+}
+
+async function resolveBusinessOrderId(gatewayOrderNo) {
+    const rows = await db.query(
+        'SELECT order_id FROM order_payment_attempts WHERE gateway_order_no = ? LIMIT 1',
+        [gatewayOrderNo]
+    )
+    return rows?.[0]?.order_id || gatewayOrderNo
+}
+
+async function getLatestGatewayOrderNo(orderId) {
+    const rows = await db.query(
+        'SELECT gateway_order_no FROM order_payment_attempts WHERE order_id = ? ORDER BY create_time DESC LIMIT 1',
+        [orderId]
+    )
+    return rows?.[0]?.gateway_order_no || orderId
+}
+
+async function getGatewayOrderNos(orderId) {
+    const rows = await db.query(
+        'SELECT gateway_order_no FROM order_payment_attempts WHERE order_id = ? ORDER BY create_time DESC',
+        [orderId]
+    )
+    const values = (rows || [])
+        .map(row => row.gateway_order_no)
+        .filter(Boolean)
+    if (!values.includes(orderId)) values.push(orderId)
+    return values
+}
+
+module.exports = {
+    generateGatewayOrderNo,
+    createPaymentAttempt,
+    resolveBusinessOrderId,
+    getLatestGatewayOrderNo,
+    getGatewayOrderNos
+}

+ 193 - 0
lib/OrderSettlement.js

@@ -0,0 +1,193 @@
+const db = require('../plugin/DataBase/db')
+const Redis = require('../plugin/DataBase/Redis')
+const { releaseUsageForOrder } = require('./CouponService')
+const { insertLedgerRecord } = require('./Lepao/CountLedger')
+
+const ORDER_STATE_PENDING = 0
+const ORDER_STATE_PROCESSING = 1
+const ORDER_STATE_COMPLETED = 2
+const ORDER_STATE_CLOSED = 3
+const ORDER_STATE_ERROR = 4
+
+function normalizeResult(result) {
+    return Array.isArray(result) ? result[0] : result
+}
+
+async function closePendingOrder({ orderId, operatorUuid = null, payId = null, logger } = {}) {
+    if (!orderId) return { closed: false, reason: 'missing_order_id' }
+
+    const conn = await db.connect()
+    try {
+        await conn.beginTransaction()
+
+        const [orderRows] = await conn.execute(
+            'SELECT orderId, state, goods_id, create_user FROM orders WHERE orderId = ? FOR UPDATE',
+            [orderId]
+        )
+        const order = orderRows?.[0]
+        if (!order) {
+            await conn.rollback()
+            return { closed: false, reason: 'not_found' }
+        }
+        if (Number(order.state) !== ORDER_STATE_PENDING) {
+            await conn.rollback()
+            return { closed: false, reason: 'state_changed', state: Number(order.state) }
+        }
+
+        const params = payId
+            ? [ORDER_STATE_CLOSED, payId, orderId, ORDER_STATE_PENDING]
+            : [ORDER_STATE_CLOSED, orderId, ORDER_STATE_PENDING]
+        const sql = payId
+            ? 'UPDATE orders SET state = ?, pay_id = ? WHERE orderId = ? AND state = ?'
+            : 'UPDATE orders SET state = ? WHERE orderId = ? AND state = ?'
+        const [closeRes] = await conn.execute(sql, params)
+        if (!closeRes || closeRes.affectedRows !== 1) {
+            await conn.rollback()
+            return { closed: false, reason: 'update_failed' }
+        }
+
+        if (order.goods_id != null) {
+            await conn.execute('UPDATE goods SET num = num + 1 WHERE id = ?', [order.goods_id])
+        }
+
+        await conn.commit()
+    } catch (error) {
+        try { await conn.rollback() } catch (_) { }
+        logger?.error?.(`关闭订单失败 ${orderId}: ${error.stack || error}`)
+        throw error
+    }
+
+    try {
+        await releaseUsageForOrder(orderId)
+        await Redis.del(`payData:${orderId}`)
+    } catch (cleanupError) {
+        logger?.warn?.(`关闭订单后清理资源失败 ${orderId}: ${cleanupError.stack || cleanupError}`)
+    }
+
+    logger?.info?.(`订单已关闭并返还库存,订单号:${orderId}${operatorUuid ? `,操作人:${operatorUuid}` : ''}`)
+    return { closed: true }
+}
+
+async function completePaidOrder({ orderId, payType, payId, payTime = Date.now(), paidAmount = null, logger } = {}) {
+    if (!orderId) return { completed: false, reason: 'missing_order_id' }
+
+    const conn = await db.connect()
+    try {
+        await conn.beginTransaction()
+
+        const [lockRows] = await conn.execute(
+            'SELECT orderId, state, price FROM orders WHERE orderId = ? FOR UPDATE',
+            [orderId]
+        )
+        const locked = lockRows?.[0]
+        if (!locked || Number(locked.state) !== ORDER_STATE_PENDING) {
+            await conn.rollback()
+            return {
+                completed: false,
+                reason: locked ? 'state_changed' : 'not_found',
+                state: locked ? Number(locked.state) : null
+            }
+        }
+
+        if (paidAmount !== null && Math.abs(Number(locked.price) - Number(paidAmount)) > 0.001) {
+            await conn.rollback()
+            logger?.error?.(`订单支付金额不匹配,订单号:${orderId},应付:${locked.price},实付:${paidAmount}`)
+            return { completed: false, reason: 'amount_mismatch' }
+        }
+
+        const [markRows] = await conn.execute(
+            'UPDATE orders SET state = ?, pay_type = ?, pay_id = ?, pay_time = ? WHERE orderId = ? AND state = ?',
+            [ORDER_STATE_PROCESSING, payType, payId, payTime, orderId, ORDER_STATE_PENDING]
+        )
+        if (!markRows || markRows.affectedRows !== 1) {
+            await conn.rollback()
+            return { completed: false, reason: 'state_changed' }
+        }
+
+        const [orderRows] = await conn.execute(
+            `SELECT
+                o.create_user,
+                g.lepao_count,
+                g.ic_count,
+                g.vip
+             FROM orders o
+             LEFT JOIN goods g ON o.goods_id = g.id
+             WHERE o.orderId = ?
+             LIMIT 1`,
+            [orderId]
+        )
+        const order = orderRows?.[0]
+        if (!order || !order.create_user) {
+            await conn.execute('UPDATE orders SET state = ? WHERE orderId = ?', [ORDER_STATE_ERROR, orderId])
+            await conn.commit()
+            return { completed: false, reason: 'bad_order_data' }
+        }
+
+        const addLepao = Number(order.lepao_count || 0)
+        const addIc = Number(order.ic_count || 0)
+        const [userRows] = await conn.execute(
+            'SELECT lepao_count FROM users WHERE uuid = ? FOR UPDATE',
+            [order.create_user]
+        )
+        if (!userRows || userRows.length !== 1) {
+            await conn.execute('UPDATE orders SET state = ? WHERE orderId = ?', [ORDER_STATE_ERROR, orderId])
+            await conn.commit()
+            return { completed: false, reason: 'user_not_found' }
+        }
+
+        const beforeLepao = Number(userRows[0].lepao_count || 0)
+        const afterLepao = beforeLepao + addLepao
+        const [updateUserRes] = await conn.execute(
+            'UPDATE users SET lepao_count = lepao_count + ?, ic_count = ic_count + ?, vip = ? WHERE uuid = ?',
+            [addLepao, addIc, order.vip, order.create_user]
+        )
+        if (!updateUserRes || updateUserRes.affectedRows !== 1) {
+            await conn.execute('UPDATE orders SET state = ? WHERE orderId = ?', [ORDER_STATE_ERROR, orderId])
+            await conn.commit()
+            return { completed: false, reason: 'update_user_failed' }
+        }
+
+        const [finishRes] = await conn.execute(
+            'UPDATE orders SET state = ? WHERE orderId = ? AND state = ?',
+            [ORDER_STATE_COMPLETED, orderId, ORDER_STATE_PROCESSING]
+        )
+        if (!finishRes || finishRes.affectedRows !== 1) {
+            await conn.rollback()
+            return { completed: false, reason: 'finish_failed' }
+        }
+
+        if (addLepao !== 0) {
+            await insertLedgerRecord({
+                executor: conn,
+                userUuid: order.create_user,
+                delta: addLepao,
+                balanceBefore: beforeLepao,
+                balanceAfter: afterLepao,
+                bizType: 'purchase',
+                bizId: orderId,
+                remark: `订单号:${orderId}`
+            })
+        }
+
+        await conn.commit()
+
+        try { await Redis.del(`payData:${orderId}`) } catch (_) { }
+        logger?.info?.(`订单支付处理完成,订单号:${orderId}`)
+        return { completed: true, userUuid: order.create_user, addLepao }
+    } catch (error) {
+        try { await conn.rollback() } catch (_) { }
+        logger?.error?.(`完成订单失败 ${orderId}: ${error.stack || error}`)
+        throw error
+    }
+}
+
+module.exports = {
+    ORDER_STATE_PENDING,
+    ORDER_STATE_PROCESSING,
+    ORDER_STATE_COMPLETED,
+    ORDER_STATE_CLOSED,
+    ORDER_STATE_ERROR,
+    closePendingOrder,
+    completePaidOrder,
+    normalizeResult
+}

+ 10 - 9
lib/PaymentClient.js

@@ -1,16 +1,16 @@
 const axios = require('axios')
-const config = require('../config.json')
+const { getRuntimeConfig } = require('./RuntimeConfig')
 
-function getPaymentConfig() {
-    return config.pay || {}
+async function getPaymentConfig() {
+    return getRuntimeConfig('pay')
 }
 
 function normalizePayBaseUrl(url) {
     return String(url || '').trim().replace(/\/+$/, '')
 }
 
-function buildPaymentApiUrl(act, params = {}) {
-    const paymentConfig = getPaymentConfig()
+async function buildPaymentApiUrl(act, params = {}) {
+    const paymentConfig = await getPaymentConfig()
     const baseUrl = normalizePayBaseUrl(paymentConfig.url)
     if (!baseUrl) {
         throw new Error('支付配置错误')
@@ -63,12 +63,12 @@ function formatPaymentHttpError(error, fallbackMessage) {
 }
 
 async function queryPaymentOrder(orderId, logger) {
-    const paymentConfig = getPaymentConfig()
+    const paymentConfig = await getPaymentConfig()
     if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key) {
         throw new Error('支付配置错误')
     }
 
-    const queryUrl = buildPaymentApiUrl('order', {
+    const queryUrl = await buildPaymentApiUrl('order', {
         pid: paymentConfig.pid,
         key: paymentConfig.key,
         out_trade_no: orderId
@@ -106,12 +106,12 @@ function isRefundResponseSuccess(result) {
 }
 
 async function requestPaymentRefund({ orderId, tradeNo, money, logger }) {
-    const paymentConfig = getPaymentConfig()
+    const paymentConfig = await getPaymentConfig()
     if (!paymentConfig.url || !paymentConfig.pid || !paymentConfig.key) {
         throw new Error('支付配置错误')
     }
 
-    const refundUrl = buildPaymentApiUrl('refund')
+    const refundUrl = await buildPaymentApiUrl('refund')
     const params = new URLSearchParams()
     params.append('pid', String(paymentConfig.pid))
     params.append('key', paymentConfig.key)
@@ -153,6 +153,7 @@ async function requestPaymentRefund({ orderId, tradeNo, money, logger }) {
 }
 
 module.exports = {
+    getPaymentConfig,
     normalizePayBaseUrl,
     buildPaymentApiUrl,
     getAxiosPaymentOptions,

+ 12 - 7
lib/ProxyForwardClient.js

@@ -2,10 +2,15 @@
  * 通过 RunForge-ProxyServer 的 /Proxy/Forward 发起出站 HTTP 请求。
  */
 const axios = require('axios')
-const config = require('../config.json')
+const { getRuntimeConfig } = require('./RuntimeConfig')
 
-function getServerConfig() {
-    const cfg = config.proxyForwardServer
+async function getServerConfig() {
+    let cfg = null
+    try {
+        cfg = await getRuntimeConfig('proxyForwardServer', { required: false, defaultValue: null })
+    } catch (_) {
+        return { url: '', enabled: false }
+    }
     if (!cfg || typeof cfg !== 'object') return { url: '', enabled: false }
     return {
         url: String(cfg.url || '').trim().replace(/\/+$/, ''),
@@ -14,8 +19,8 @@ function getServerConfig() {
     }
 }
 
-function isProxyForwardEnabled() {
-    const { url, enabled } = getServerConfig()
+async function isProxyForwardEnabled() {
+    const { url, enabled } = await getServerConfig()
     return enabled && url.length > 0
 }
 
@@ -184,7 +189,7 @@ async function forwardRequest(opts) {
         return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
     }
 
-    if (!isProxyForwardEnabled()) {
+    if (!await isProxyForwardEnabled()) {
         logger?.info?.(`${logPrefix} (${scene}) 未配置代理服务,直连 ${path}`)
         if (outboundMode === 'proxy') {
             const err = new Error('未配置 proxyForwardServer.url')
@@ -195,7 +200,7 @@ async function forwardRequest(opts) {
         return execDirect({ method: m, url, data, headers, timeout, validateStatus, responseType, transformResponse })
     }
 
-    const { url: serverUrl, defaultTimeout } = getServerConfig()
+    const { url: serverUrl, defaultTimeout } = await getServerConfig()
     logger?.info?.(`${logPrefix} (${scene}) 经代理服务 ${m.toUpperCase()} ${path}`)
 
     const reqHeaders = normalizeForwardHeaders(headers)

+ 102 - 0
lib/RuntimeConfig.js

@@ -0,0 +1,102 @@
+const db = require('../plugin/DataBase/db')
+const Redis = require('../plugin/DataBase/Redis')
+
+const CONFIG_KEYS = ['pay', 'email', 'unilogin', 'proxyForwardServer']
+const CACHE_TTL_SECONDS = 300
+
+class RuntimeConfigNotFoundError extends Error {
+    constructor(key) {
+        super(`未配置或未启用 ${key}`)
+        this.name = 'RuntimeConfigNotFoundError'
+        this.code = 'RUNTIME_CONFIG_NOT_FOUND'
+        this.key = key
+    }
+}
+
+function cacheKey(key) {
+    return `runtimeConfig:${key}`
+}
+
+async function getRuntimeConfig(key, options = {}) {
+    const { required = true, defaultValue = undefined } = options
+    if (!CONFIG_KEYS.includes(key)) throw new Error('不支持的运行时配置')
+    const cached = await Redis.get(cacheKey(key))
+    if (cached) return JSON.parse(cached)
+
+    const rows = await db.query(
+        `SELECT config_value FROM runtime_configs
+         WHERE config_key = ? AND enabled = 1
+        ORDER BY is_default DESC, updated_at DESC LIMIT 1`,
+        [key]
+    )
+    if (!rows?.[0]) {
+        if (!required) return defaultValue
+        throw new RuntimeConfigNotFoundError(key)
+    }
+    const value = typeof rows[0].config_value === 'object'
+        ? rows[0].config_value
+        : JSON.parse(rows[0].config_value)
+    await Redis.set(cacheKey(key), JSON.stringify(value), { EX: CACHE_TTL_SECONDS })
+    return value
+}
+
+async function getAllRuntimeConfigs() {
+    const rows = await db.query(
+        `SELECT config_key, config_name, config_value, enabled, is_default, updated_at, updated_by
+         FROM runtime_configs ORDER BY config_key, is_default DESC, updated_at DESC`
+    ) || []
+    return rows.map(row => ({
+        ...row,
+        config_value: typeof row.config_value === 'object' ? row.config_value : JSON.parse(row.config_value)
+    }))
+}
+
+function validateRuntimeConfig(key, value) {
+    if (key === 'email') {
+        if (!Array.isArray(value)) throw new Error('邮件配置必须为数组')
+        return value.map((item, index) => ({ ...item, priority: Number(item.priority ?? index) }))
+            .sort((a, b) => a.priority - b.priority)
+    }
+    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${key} 配置必须为对象`)
+    if (key === 'pay' && (!Array.isArray(value.methods) || value.methods.some(item => !item?.type))) {
+        throw new Error('付款方式配置无效')
+    }
+    return value
+}
+
+async function saveRuntimeConfig({ key, name = 'default', value, enabled = true, isDefault = false, updatedBy }) {
+    if (!CONFIG_KEYS.includes(key)) throw new Error('不支持的运行时配置')
+    const normalized = validateRuntimeConfig(key, value)
+    const result = await db.query(
+        `INSERT INTO runtime_configs (config_key, config_name, config_value, enabled, is_default, updated_at, updated_by)
+         VALUES (?, ?, ?, ?, ?, ?, ?)
+         ON DUPLICATE KEY UPDATE config_value = VALUES(config_value), enabled = VALUES(enabled),
+         is_default = VALUES(is_default), updated_at = VALUES(updated_at), updated_by = VALUES(updated_by)`,
+        [key, String(name).trim(), JSON.stringify(normalized), enabled ? 1 : 0, isDefault ? 1 : 0, Date.now(), updatedBy || null]
+    )
+    if (result === undefined) throw new Error('保存运行时配置失败')
+    if (isDefault) {
+        await db.query('UPDATE runtime_configs SET is_default = IF(config_name = ?, 1, 0) WHERE config_key = ?', [String(name).trim(), key])
+    }
+    await Redis.del(cacheKey(key))
+    return normalized
+}
+
+async function selectRuntimeConfig({ key, name, updatedBy }) {
+    if (!CONFIG_KEYS.includes(key) || !name) throw new Error('配置类型或名称无效')
+    const result = await db.query(
+        'UPDATE runtime_configs SET is_default = IF(config_name = ?, 1, 0), updated_at = ?, updated_by = ? WHERE config_key = ?',
+        [name, Date.now(), updatedBy || null, key]
+    )
+    if (result === undefined) throw new Error('切换运行时配置失败')
+    await Redis.del(cacheKey(key))
+}
+
+module.exports = {
+    CONFIG_KEYS,
+    RuntimeConfigNotFoundError,
+    getRuntimeConfig,
+    getAllRuntimeConfigs,
+    saveRuntimeConfig,
+    selectRuntimeConfig
+}

+ 43 - 0
lib/UniLoginClient.js

@@ -0,0 +1,43 @@
+const axios = require('axios')
+const https = require('https')
+const { getRuntimeConfig } = require('./RuntimeConfig')
+
+const VALID_SOCIAL_TYPES = ['qq', 'wx']
+
+function normalizeSocialType(type) {
+    const socialType = type || 'qq'
+    return VALID_SOCIAL_TYPES.includes(socialType) ? socialType : null
+}
+
+async function fetchUniLoginProfile(type, code) {
+    const socialType = normalizeSocialType(type)
+    if (!socialType)
+        throw new Error('不支持的第三方登录类型')
+
+    const uniConfig = await getRuntimeConfig('unilogin', { required: false, defaultValue: null })
+    if (!uniConfig || !uniConfig.url || !uniConfig.appid || !uniConfig.appkey) {
+        throw new Error('聚合登录暂未配置')
+    }
+    const url = `${uniConfig.url}/connect.php?act=callback&appid=${uniConfig.appid}&appkey=${uniConfig.appkey}&type=${socialType}&code=${code}`
+
+    const r = await axios.get(url, {
+        httpsAgent: new https.Agent({
+            rejectUnauthorized: false
+        }),
+        proxy: false
+    })
+
+    if (!r || r.data?.code !== 0)
+        throw new Error(r.data?.msg || 'api接口错误')
+
+    return {
+        ...r.data,
+        social_type: socialType
+    }
+}
+
+module.exports = {
+    VALID_SOCIAL_TYPES,
+    normalizeSocialType,
+    fetchUniLoginProfile
+}

+ 39 - 37
plugin/Email/Email.js

@@ -1,65 +1,67 @@
 const nodemailer = require('nodemailer')
-const config = require('../../config.json')
 const path = require('path')
 const Logger = require('../../lib/Logger')
+const { getRuntimeConfig } = require('../../lib/RuntimeConfig')
 
 const logger = new Logger(path.join(__dirname, '../../logs/Email.log'), 'INFO')
 
 /**
- * 尝试用多个邮箱配置发送邮件;失败时仅记录日志并 resolve(false),不抛错、不 reject。
+ * 尝试用多个邮箱配置发送邮件
  * @param {string} email - 收件人邮箱
  * @param {string} subject - 邮件主题
  * @param {string} content - 邮件内容(支持 HTML)
- * @returns {Promise<boolean>} 是否至少有一个账号发送成功
  */
 async function sendEmail(email, subject, content) {
-    const mail = {
-        to: email,
-        subject,
-        html: content
-    }
-
-    if (!Array.isArray(config.email) || config.email.length === 0) {
-        logger.error('邮件发送失败:未配置可用邮箱账号')
-        return false
-    }
-
-    for (let i = 0; i < config.email.length; i++) {
-        const currentConfig = config.email[i]
-        if (!currentConfig?.host || !currentConfig?.user) {
-            logger.error(`邮件发送失败:第 ${i + 1} 个邮箱配置不完整,已跳过`)
-            continue
+    return new Promise(async (resolve, reject) => {
+        const mail = {
+            to: email,
+            subject: subject,
+            html: content
         }
 
-        let transporter
+        let emailConfigs = []
         try {
-            transporter = nodemailer.createTransport({
+            emailConfigs = await getRuntimeConfig('email', { required: false, defaultValue: [] })
+        } catch (error) {
+            logger.error(`邮件配置读取失败:${error.stack || error}`)
+            return resolve(false)
+        }
+        if (!Array.isArray(emailConfigs) || emailConfigs.length === 0) {
+            logger.error('邮件发送失败:未配置可用邮箱账号')
+            return resolve(false)
+        }
+
+        // 遍历所有邮箱配置
+        for (let i = 0; i < emailConfigs.length; i++) {
+            const currentConfig = emailConfigs[i];
+
+            const transporter = nodemailer.createTransport({
                 host: currentConfig.host,
                 port: currentConfig.port,
-                secure: currentConfig.secure === true,
                 auth: {
                     user: currentConfig.user,
                     pass: currentConfig.password
                 }
             })
-        } catch (error) {
-            logger.error(`邮件传输器创建失败 (${currentConfig.user}):${error.stack || error.message || error}`)
-            continue
-        }
 
-        mail.from = currentConfig.from || currentConfig.user
+            mail.from = currentConfig.from
 
-        try {
-            await transporter.sendMail(mail)
-            logger.info(`${email} 邮件发送成功,使用账号: ${currentConfig.user}`)
-            return true
-        } catch (error) {
-            logger.error(`邮件发送失败 (${currentConfig.user}),错误: ${error.stack || error.message || error}`)
+            try {
+                await transporter.sendMail(mail)
+                logger.info(`${email}邮件发送成功,使用账号: ${currentConfig.user}`)
+                return resolve()
+            } catch (error) {
+                logger.error(`邮件发送失败 (${currentConfig.user}),错误:`, error.stack)
+                if (i === emailConfigs.length - 1) {
+                    // 最后一个配置也失败了
+                    logger.error(`邮件发送失败,所有邮箱配置均不可用`)
+                    return resolve(false)
+                }
+                // 否则继续下一个配置
+            }
         }
-    }
-
-    logger.error('邮件发送失败,所有邮箱配置均不可用')
-    return false
+        return resolve(false)
+    })
 }
 
 module.exports = sendEmail

+ 55 - 106
plugin/mq/orderPaymentWorker.js

@@ -1,127 +1,79 @@
-const db = require('../DataBase/db')
-const config = require('../../config.json')
 const mq = require('./index')
-const { ORDER_PAYMENT_QUEUE } = require('./jkesMqNames')
-const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
-const { releaseUsageForOrder } = require('../../lib/CouponService')
-const { queryPaymentOrder } = require('../../lib/PaymentClient')
+const { mq: mqName } = require('./mqPrefix')
+const { queryPaymentOrder, getPaymentConfig } = require('../../lib/PaymentClient')
+const { getGatewayOrderNos } = require('../../lib/OrderPaymentAttempt')
+const { closePendingOrder, completePaidOrder } = require('../../lib/OrderSettlement')
+const db = require('../DataBase/db')
 
+const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
 let orderPaymentWorkerStarted = false
 
-async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
-    const delta = Number(addCount || 0)
-    if (!orderId || !userUuid || delta === 0) return
-    try {
-        const userRows = await db.query(
-            'SELECT lepao_count FROM users WHERE uuid = ?',
-            [userUuid]
-        )
-        if (!userRows || userRows.length !== 1) return
-        const afterCount = Number(userRows[0].lepao_count || 0)
-        const beforeCount = afterCount - delta
-        await insertLedgerRecord({
-            userUuid,
-            delta,
-            balanceBefore: beforeCount,
-            balanceAfter: afterCount,
-            bizType: 'purchase',
-            bizId: orderId,
-            remark: `订单号:${orderId}`
-        })
-    } catch (error) {
-        logger?.error?.(`写入购买里程流水失败 ${orderId}: ${error.stack || error}`)
-    }
+async function isOrderStillPending(orderId) {
+    const rows = await db.query('SELECT state FROM orders WHERE orderId = ? LIMIT 1', [orderId])
+    if (!rows?.length) return { exists: false, pending: false }
+    return { exists: true, pending: Number(rows[0].state) === 0, state: Number(rows[0].state) }
 }
 
-async function pollOrderPaymentStatus(orderId, logger) {
-    const paymentConfig = config.pay || {}
+function sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms))
+}
 
+async function pollOrderPaymentStatus(orderId, logger) {
+    const paymentConfig = await getPaymentConfig()
     if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key) {
         logger.error('支付配置错误,无法轮询易支付状态')
         return
     }
 
-    const MAX_RETRIES = 120
-    const DELAY = 2500
-
-    const pollOrderStatus = async (retry = 0) => {
-        if (retry >= MAX_RETRIES) {
-            const closeRes = await db.query(
-                'UPDATE orders SET state = 3 WHERE orderId = ? AND state = 0',
-                [orderId]
-            )
-            if (closeRes?.affectedRows > 0) {
-                await releaseUsageForOrder(orderId)
-                logger.info(`订单超时未支付,自动取消,订单号:${orderId}`)
-            }
+    const maxRetries = 120
+    const delayMs = 2500
+
+    logger.info(`开始轮询订单支付状态,订单号:${orderId}`)
+
+    for (let retry = 0; retry <= maxRetries; retry++) {
+        const status = await isOrderStillPending(orderId)
+        if (!status.exists) {
+            logger.warn(`订单不存在,停止轮询:${orderId}`)
+            return
+        }
+        if (!status.pending) {
+            logger.info(`订单已处理(state=${status.state}),停止轮询:${orderId}`)
             return
         }
 
-        try {
-            const existing = await db.query('SELECT state FROM orders WHERE orderId = ?', [orderId])
-            if (!existing?.length) {
-                logger.warn(`订单不存在,停止轮询:${orderId}`)
-                return
-            }
-            if (Number(existing[0].state) !== 0) {
-                logger.info(`订单已处理(state=${existing[0].state}),停止轮询:${orderId}`)
-                return
-            }
+        if (retry >= maxRetries) {
+            const closeResult = await closePendingOrder({ orderId, logger })
+            if (closeResult.closed) logger.info(`订单超时未支付,自动取消:${orderId}`)
+            return
+        }
 
-            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
-                const time = Date.now()
-
-                let sql = 'UPDATE orders SET state = 1, pay_type = ?, pay_id = ?, pay_time = ? WHERE orderId = ? AND state = 0'
-                const result = await db.query(sql, [type, trade_no, time, out_trade_no])
-
-                if (result.affectedRows > 0) {
-                    sql = `
-                        SELECT g.lepao_count, a.create_user
-                        FROM orders a
-                        LEFT JOIN goods g ON a.goods_id = g.id
-                        WHERE a.orderId = ?
-                    `
-                    const rows = await db.query(sql, [out_trade_no])
-                    if (!rows || rows.length !== 1) {
-                        logger.error(`订单商品信息异常,订单号:${out_trade_no}`)
-                        await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
-                        return
-                    }
-
-                    const { lepao_count, create_user } = rows[0]
-                    sql = 'UPDATE users SET lepao_count = lepao_count + ? WHERE uuid = ?'
-                    const updateUser = await db.query(sql, [lepao_count, create_user])
-
-                    if (!updateUser || updateUser.affectedRows !== 1) {
-                        logger.error(`更新用户失败,UUID: ${create_user}`)
-                        await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
-                    }
-
-                    sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
-                    await db.query(sql, [out_trade_no])
-                    await writePurchaseLedger(out_trade_no, create_user, lepao_count, logger)
-
-                    logger.info(`订单处理成功:${out_trade_no}`)
+        try {
+            const gatewayOrderNos = await getGatewayOrderNos(orderId)
+            for (const gatewayOrderNo of gatewayOrderNos) {
+                const queryData = await queryPaymentOrder(gatewayOrderNo, logger)
+                logger.info(`轮询支付状态,订单号:${orderId},网关订单号:${gatewayOrderNo},次数:${retry + 1},结果:${JSON.stringify(queryData)}`)
+
+                if (Number(queryData.code) === 1 && Number(queryData.status) === 1) {
+                    const result = await completePaidOrder({
+                        orderId,
+                        payType: queryData.type,
+                        payId: queryData.trade_no,
+                        payTime: Date.now(),
+                        paidAmount: queryData.money,
+                        logger
+                    })
+                    if (result.completed || result.reason === 'state_changed') return
+
+                    logger.error(`网关已支付但订单完成失败,订单号:${orderId},原因:${result.reason}`)
                     return
                 }
-
-                logger.info(`支付网关已确认付款,订单已由其他进程/回调处理,停止轮询:${out_trade_no}`)
-                return
             }
-
-            setTimeout(() => pollOrderStatus(retry + 1), DELAY)
         } catch (error) {
             logger.warn(`轮询支付状态失败,订单号:${orderId},原因:${error.message || error}`)
-            setTimeout(() => pollOrderStatus(retry + 1), DELAY)
         }
-    }
 
-    logger.info(`开始轮询订单支付状态,订单号:${orderId}`)
-    pollOrderStatus()
+        await sleep(delayMs)
+    }
 }
 
 async function enqueueOrderPaymentCheck(orderId) {
@@ -135,9 +87,7 @@ async function enqueueOrderPaymentCheck(orderId) {
 }
 
 async function startOrderPaymentWorker(logger) {
-    if (orderPaymentWorkerStarted) {
-        return
-    }
+    if (orderPaymentWorkerStarted) return
 
     try {
         const ch = await mq.getChannel('order_payment')
@@ -145,7 +95,6 @@ async function startOrderPaymentWorker(logger) {
         await ch.prefetch(1)
 
         logger.info(`订单支付结果轮询消费者已启动,队列:${ORDER_PAYMENT_QUEUE}`)
-
         orderPaymentWorkerStarted = true
 
         ch.consume(ORDER_PAYMENT_QUEUE, async (msg) => {
@@ -155,7 +104,7 @@ async function startOrderPaymentWorker(logger) {
             const { orderId } = content
 
             if (!orderId) {
-                logger.warn('收到无效的订单支付检查消息(缺少 orderId)')
+                logger.warn('收到无效的订单支付检查消息:缺少 orderId')
                 ch.ack(msg)
                 return
             }