|
|
@@ -0,0 +1,282 @@
|
|
|
+const axios = require('axios')
|
|
|
+const config = require('../config.json')
|
|
|
+const db = require('../plugin/DataBase/db')
|
|
|
+const { insertLedgerRecord } = require('./Lepao/CountLedger')
|
|
|
+
|
|
|
+const REFUND_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
|
|
|
+const ORDER_STATE_COMPLETED = 2
|
|
|
+const ORDER_STATE_REFUNDED = 5
|
|
|
+
|
|
|
+function evaluateRefundEligibility({
|
|
|
+ state,
|
|
|
+ payTime,
|
|
|
+ userLepaoCount,
|
|
|
+ goodsLepaoCount,
|
|
|
+ skipTimeLimit = false
|
|
|
+}) {
|
|
|
+ if (Number(state) === ORDER_STATE_REFUNDED) {
|
|
|
+ return { canRefund: false, reason: '订单已退款' }
|
|
|
+ }
|
|
|
+ if (Number(state) !== ORDER_STATE_COMPLETED) {
|
|
|
+ return { canRefund: false, reason: '仅已完成订单可申请退款' }
|
|
|
+ }
|
|
|
+ if (!payTime) {
|
|
|
+ return { canRefund: false, reason: '订单支付时间异常' }
|
|
|
+ }
|
|
|
+ if (!skipTimeLimit && Date.now() - Number(payTime) > REFUND_WINDOW_MS) {
|
|
|
+ return { canRefund: false, reason: '已超过7天退款期限' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const purchasedCount = Number(goodsLepaoCount || 0)
|
|
|
+ const remainingCount = Number(userLepaoCount || 0)
|
|
|
+ if (purchasedCount > 0 && remainingCount <= purchasedCount) {
|
|
|
+ return { canRefund: false, reason: '账户剩余次数需大于订单购买次数才可退款' }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { canRefund: true, reason: '' }
|
|
|
+}
|
|
|
+
|
|
|
+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 formatPaymentRefundError(response, fallbackMessage) {
|
|
|
+ const status = Number(response?.status || 0)
|
|
|
+ const parsed = parsePaymentResponseBody(response?.data)
|
|
|
+
|
|
|
+ if (parsed?.msg) return String(parsed.msg)
|
|
|
+ if (status === 503) return '支付平台暂时不可用,请稍后重试'
|
|
|
+ if (status >= 500) return `支付平台异常(${status}),请稍后重试`
|
|
|
+ if (status >= 400) return `支付平台拒绝退款(${status})`
|
|
|
+ return fallbackMessage || '支付平台退款失败'
|
|
|
+}
|
|
|
+
|
|
|
+async function requestPaymentRefund({ orderId, tradeNo, money, logger }) {
|
|
|
+ const paymentConfig = config.pay || {}
|
|
|
+ if (!paymentConfig.url || !paymentConfig.pid || !paymentConfig.key) {
|
|
|
+ throw new Error('支付配置错误')
|
|
|
+ }
|
|
|
+
|
|
|
+ const params = new URLSearchParams()
|
|
|
+ params.append('pid', String(paymentConfig.pid))
|
|
|
+ params.append('key', paymentConfig.key)
|
|
|
+ if (tradeNo) {
|
|
|
+ params.append('trade_no', tradeNo)
|
|
|
+ } else {
|
|
|
+ params.append('out_trade_no', orderId)
|
|
|
+ }
|
|
|
+ params.append('money', String(money))
|
|
|
+
|
|
|
+ const refundUrl = `${paymentConfig.url}/api.php?act=refund`
|
|
|
+ let response
|
|
|
+ try {
|
|
|
+ response = await axios.post(refundUrl, params.toString(), {
|
|
|
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
+ timeout: 30000,
|
|
|
+ validateStatus: () => true
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ logger?.error?.(`易支付退款请求失败 订单号:${orderId}:${error.stack || error}`)
|
|
|
+ if (error.code === 'ECONNABORTED') {
|
|
|
+ throw new Error('支付平台响应超时,请稍后重试')
|
|
|
+ }
|
|
|
+ throw new Error('无法连接支付平台,请稍后重试')
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = parsePaymentResponseBody(response.data)
|
|
|
+ logger?.info?.(`易支付退款响应 订单号:${orderId},HTTP ${response.status},结果:${JSON.stringify(result ?? response.data)}`)
|
|
|
+
|
|
|
+ if (response.status >= 400) {
|
|
|
+ throw new Error(formatPaymentRefundError(response, '支付平台退款失败'))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!result || Number(result.code) !== 1) {
|
|
|
+ throw new Error(result?.msg || '支付平台退款失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+async function loadRefundContext(orderId) {
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT
|
|
|
+ o.orderId,
|
|
|
+ o.state,
|
|
|
+ o.price,
|
|
|
+ o.pay_id,
|
|
|
+ o.pay_time,
|
|
|
+ o.create_user,
|
|
|
+ g.lepao_count,
|
|
|
+ g.ic_count
|
|
|
+ FROM orders o
|
|
|
+ LEFT JOIN goods g ON o.goods_id = g.id
|
|
|
+ WHERE o.orderId = ?
|
|
|
+ LIMIT 1`,
|
|
|
+ [orderId]
|
|
|
+ )
|
|
|
+ if (!rows || rows.length !== 1) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const order = rows[0]
|
|
|
+ const userRows = await db.query(
|
|
|
+ 'SELECT lepao_count, ic_count FROM users WHERE uuid = ? LIMIT 1',
|
|
|
+ [order.create_user]
|
|
|
+ )
|
|
|
+ if (!userRows || userRows.length !== 1) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ return { order, user: userRows[0] }
|
|
|
+}
|
|
|
+
|
|
|
+async function executeOrderRefund({
|
|
|
+ orderId,
|
|
|
+ operatorUuid = null,
|
|
|
+ skipTimeLimit = false,
|
|
|
+ logger
|
|
|
+}) {
|
|
|
+ try {
|
|
|
+ const context = await loadRefundContext(orderId)
|
|
|
+ if (!context) {
|
|
|
+ return { ok: false, msg: '订单或用户不存在' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const { order, user } = context
|
|
|
+ const eligibility = evaluateRefundEligibility({
|
|
|
+ state: order.state,
|
|
|
+ payTime: order.pay_time,
|
|
|
+ userLepaoCount: user.lepao_count,
|
|
|
+ goodsLepaoCount: order.lepao_count,
|
|
|
+ skipTimeLimit
|
|
|
+ })
|
|
|
+ if (!eligibility.canRefund) {
|
|
|
+ return { ok: false, msg: eligibility.reason }
|
|
|
+ }
|
|
|
+
|
|
|
+ const deductLepao = Number(order.lepao_count || 0)
|
|
|
+ const deductIc = Number(order.ic_count || 0)
|
|
|
+ if (deductLepao > 0 && Number(user.lepao_count || 0) < deductLepao) {
|
|
|
+ return { ok: false, msg: '账户乐跑次数不足,无法完成退款' }
|
|
|
+ }
|
|
|
+
|
|
|
+ await requestPaymentRefund({
|
|
|
+ orderId: order.orderId,
|
|
|
+ tradeNo: order.pay_id,
|
|
|
+ money: order.price,
|
|
|
+ logger
|
|
|
+ })
|
|
|
+
|
|
|
+ const conn = await db.connect()
|
|
|
+ try {
|
|
|
+ await conn.beginTransaction()
|
|
|
+
|
|
|
+ const [orderRows] = await conn.execute(
|
|
|
+ `SELECT
|
|
|
+ o.orderId,
|
|
|
+ o.state,
|
|
|
+ o.create_user,
|
|
|
+ g.lepao_count,
|
|
|
+ g.ic_count
|
|
|
+ FROM orders o
|
|
|
+ LEFT JOIN goods g ON o.goods_id = g.id
|
|
|
+ WHERE o.orderId = ?
|
|
|
+ FOR UPDATE`,
|
|
|
+ [orderId]
|
|
|
+ )
|
|
|
+ if (!orderRows || orderRows.length !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return { ok: false, msg: '订单不存在' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const lockedOrder = orderRows[0]
|
|
|
+ if (Number(lockedOrder.state) !== ORDER_STATE_COMPLETED) {
|
|
|
+ await conn.rollback()
|
|
|
+ return { ok: false, msg: '订单状态已变更,请刷新后重试' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const [userRows] = await conn.execute(
|
|
|
+ 'SELECT lepao_count, ic_count FROM users WHERE uuid = ? FOR UPDATE',
|
|
|
+ [lockedOrder.create_user]
|
|
|
+ )
|
|
|
+ if (!userRows || userRows.length !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ return { ok: false, msg: '用户不存在' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const lockedUser = userRows[0]
|
|
|
+ const lockedDeductLepao = Number(lockedOrder.lepao_count || 0)
|
|
|
+ const lockedDeductIc = Number(lockedOrder.ic_count || 0)
|
|
|
+ const beforeLepao = Number(lockedUser.lepao_count || 0)
|
|
|
+ const beforeIc = Number(lockedUser.ic_count || 0)
|
|
|
+
|
|
|
+ if (lockedDeductLepao > 0 && beforeLepao < lockedDeductLepao) {
|
|
|
+ await conn.rollback()
|
|
|
+ logger?.error?.(`退款支付已成功但扣次失败,需人工处理,订单号:${orderId}`)
|
|
|
+ return { ok: false, msg: '支付已退款但扣减次数失败,请联系客服处理' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const afterLepao = beforeLepao - lockedDeductLepao
|
|
|
+ const afterIc = Math.max(0, beforeIc - lockedDeductIc)
|
|
|
+
|
|
|
+ const [updateUserRes] = await conn.execute(
|
|
|
+ 'UPDATE users SET lepao_count = ?, ic_count = ? WHERE uuid = ?',
|
|
|
+ [afterLepao, afterIc, lockedOrder.create_user]
|
|
|
+ )
|
|
|
+ if (!updateUserRes || updateUserRes.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ logger?.error?.(`退款支付已成功但更新用户失败,需人工处理,订单号:${orderId}`)
|
|
|
+ return { ok: false, msg: '支付已退款但更新账户失败,请联系客服处理' }
|
|
|
+ }
|
|
|
+
|
|
|
+ const [updateOrderRes] = await conn.execute(
|
|
|
+ 'UPDATE orders SET state = ? WHERE orderId = ? AND state = ?',
|
|
|
+ [ORDER_STATE_REFUNDED, orderId, ORDER_STATE_COMPLETED]
|
|
|
+ )
|
|
|
+ if (!updateOrderRes || updateOrderRes.affectedRows !== 1) {
|
|
|
+ await conn.rollback()
|
|
|
+ logger?.error?.(`退款支付已成功但更新订单失败,需人工处理,订单号:${orderId}`)
|
|
|
+ return { ok: false, msg: '支付已退款但更新订单失败,请联系客服处理' }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lockedDeductLepao !== 0) {
|
|
|
+ await insertLedgerRecord({
|
|
|
+ executor: conn,
|
|
|
+ userUuid: lockedOrder.create_user,
|
|
|
+ delta: -lockedDeductLepao,
|
|
|
+ balanceBefore: beforeLepao,
|
|
|
+ balanceAfter: afterLepao,
|
|
|
+ bizType: 'purchase_refund',
|
|
|
+ bizId: orderId,
|
|
|
+ operatorUuid,
|
|
|
+ remark: `订单退款:${orderId}`
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ await conn.commit()
|
|
|
+ return { ok: true, msg: '退款成功' }
|
|
|
+ } catch (dbError) {
|
|
|
+ try { await conn.rollback() } catch (_) { }
|
|
|
+ logger?.error?.(`退款入账失败 ${orderId}: ${dbError.stack || dbError}`)
|
|
|
+ return { ok: false, msg: '支付已退款但入账失败,请联系客服处理' }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ logger?.error?.(`订单退款失败 ${orderId}: ${error.stack || error}`)
|
|
|
+ return { ok: false, msg: error.message || '退款失败,请稍后再试' }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ REFUND_WINDOW_MS,
|
|
|
+ ORDER_STATE_REFUNDED,
|
|
|
+ evaluateRefundEligibility,
|
|
|
+ executeOrderRefund
|
|
|
+}
|