OrderRefundService.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. const axios = require('axios')
  2. const config = require('../config.json')
  3. const db = require('../plugin/DataBase/db')
  4. const { insertLedgerRecord } = require('./Lepao/CountLedger')
  5. const REFUND_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
  6. const ORDER_STATE_COMPLETED = 2
  7. const ORDER_STATE_REFUNDED = 5
  8. function evaluateRefundEligibility({
  9. state,
  10. payTime,
  11. userLepaoCount,
  12. goodsLepaoCount,
  13. skipTimeLimit = false
  14. }) {
  15. if (Number(state) === ORDER_STATE_REFUNDED) {
  16. return { canRefund: false, reason: '订单已退款' }
  17. }
  18. if (Number(state) !== ORDER_STATE_COMPLETED) {
  19. return { canRefund: false, reason: '仅已完成订单可申请退款' }
  20. }
  21. if (!payTime) {
  22. return { canRefund: false, reason: '订单支付时间异常' }
  23. }
  24. if (!skipTimeLimit && Date.now() - Number(payTime) > REFUND_WINDOW_MS) {
  25. return { canRefund: false, reason: '已超过7天退款期限' }
  26. }
  27. const purchasedCount = Number(goodsLepaoCount || 0)
  28. const remainingCount = Number(userLepaoCount || 0)
  29. if (purchasedCount > 0 && remainingCount <= purchasedCount) {
  30. return { canRefund: false, reason: '账户剩余次数需大于订单购买次数才可退款' }
  31. }
  32. return { canRefund: true, reason: '' }
  33. }
  34. function parsePaymentResponseBody(data) {
  35. if (data && typeof data === 'object') return data
  36. if (typeof data !== 'string') return null
  37. const text = data.trim()
  38. if (!text) return null
  39. try {
  40. return JSON.parse(text)
  41. } catch (_) {
  42. return null
  43. }
  44. }
  45. function formatPaymentRefundError(response, fallbackMessage) {
  46. const status = Number(response?.status || 0)
  47. const parsed = parsePaymentResponseBody(response?.data)
  48. if (parsed?.msg) return String(parsed.msg)
  49. if (status === 503) return '支付平台暂时不可用,请稍后重试'
  50. if (status >= 500) return `支付平台异常(${status}),请稍后重试`
  51. if (status >= 400) return `支付平台拒绝退款(${status})`
  52. return fallbackMessage || '支付平台退款失败'
  53. }
  54. async function requestPaymentRefund({ orderId, tradeNo, money, logger }) {
  55. const paymentConfig = config.pay || {}
  56. if (!paymentConfig.url || !paymentConfig.pid || !paymentConfig.key) {
  57. throw new Error('支付配置错误')
  58. }
  59. const params = new URLSearchParams()
  60. params.append('pid', String(paymentConfig.pid))
  61. params.append('key', paymentConfig.key)
  62. if (tradeNo) {
  63. params.append('trade_no', tradeNo)
  64. } else {
  65. params.append('out_trade_no', orderId)
  66. }
  67. params.append('money', String(money))
  68. const refundUrl = `${paymentConfig.url}/api.php?act=refund`
  69. let response
  70. try {
  71. response = await axios.post(refundUrl, params.toString(), {
  72. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  73. timeout: 30000,
  74. validateStatus: () => true
  75. })
  76. } catch (error) {
  77. logger?.error?.(`易支付退款请求失败 订单号:${orderId}:${error.stack || error}`)
  78. if (error.code === 'ECONNABORTED') {
  79. throw new Error('支付平台响应超时,请稍后重试')
  80. }
  81. throw new Error('无法连接支付平台,请稍后重试')
  82. }
  83. const result = parsePaymentResponseBody(response.data)
  84. logger?.info?.(`易支付退款响应 订单号:${orderId},HTTP ${response.status},结果:${JSON.stringify(result ?? response.data)}`)
  85. if (response.status >= 400) {
  86. throw new Error(formatPaymentRefundError(response, '支付平台退款失败'))
  87. }
  88. if (!result || Number(result.code) !== 1) {
  89. throw new Error(result?.msg || '支付平台退款失败')
  90. }
  91. return result
  92. }
  93. async function loadRefundContext(orderId) {
  94. const rows = await db.query(
  95. `SELECT
  96. o.orderId,
  97. o.state,
  98. o.price,
  99. o.pay_id,
  100. o.pay_time,
  101. o.create_user,
  102. g.lepao_count,
  103. g.ic_count
  104. FROM orders o
  105. LEFT JOIN goods g ON o.goods_id = g.id
  106. WHERE o.orderId = ?
  107. LIMIT 1`,
  108. [orderId]
  109. )
  110. if (!rows || rows.length !== 1) {
  111. return null
  112. }
  113. const order = rows[0]
  114. const userRows = await db.query(
  115. 'SELECT lepao_count, ic_count FROM users WHERE uuid = ? LIMIT 1',
  116. [order.create_user]
  117. )
  118. if (!userRows || userRows.length !== 1) {
  119. return null
  120. }
  121. return { order, user: userRows[0] }
  122. }
  123. async function executeOrderRefund({
  124. orderId,
  125. operatorUuid = null,
  126. skipTimeLimit = false,
  127. logger
  128. }) {
  129. try {
  130. const context = await loadRefundContext(orderId)
  131. if (!context) {
  132. return { ok: false, msg: '订单或用户不存在' }
  133. }
  134. const { order, user } = context
  135. const eligibility = evaluateRefundEligibility({
  136. state: order.state,
  137. payTime: order.pay_time,
  138. userLepaoCount: user.lepao_count,
  139. goodsLepaoCount: order.lepao_count,
  140. skipTimeLimit
  141. })
  142. if (!eligibility.canRefund) {
  143. return { ok: false, msg: eligibility.reason }
  144. }
  145. const deductLepao = Number(order.lepao_count || 0)
  146. const deductIc = Number(order.ic_count || 0)
  147. if (deductLepao > 0 && Number(user.lepao_count || 0) < deductLepao) {
  148. return { ok: false, msg: '账户乐跑次数不足,无法完成退款' }
  149. }
  150. await requestPaymentRefund({
  151. orderId: order.orderId,
  152. tradeNo: order.pay_id,
  153. money: order.price,
  154. logger
  155. })
  156. const conn = await db.connect()
  157. try {
  158. await conn.beginTransaction()
  159. const [orderRows] = await conn.execute(
  160. `SELECT
  161. o.orderId,
  162. o.state,
  163. o.create_user,
  164. g.lepao_count,
  165. g.ic_count
  166. FROM orders o
  167. LEFT JOIN goods g ON o.goods_id = g.id
  168. WHERE o.orderId = ?
  169. FOR UPDATE`,
  170. [orderId]
  171. )
  172. if (!orderRows || orderRows.length !== 1) {
  173. await conn.rollback()
  174. return { ok: false, msg: '订单不存在' }
  175. }
  176. const lockedOrder = orderRows[0]
  177. if (Number(lockedOrder.state) !== ORDER_STATE_COMPLETED) {
  178. await conn.rollback()
  179. return { ok: false, msg: '订单状态已变更,请刷新后重试' }
  180. }
  181. const [userRows] = await conn.execute(
  182. 'SELECT lepao_count, ic_count FROM users WHERE uuid = ? FOR UPDATE',
  183. [lockedOrder.create_user]
  184. )
  185. if (!userRows || userRows.length !== 1) {
  186. await conn.rollback()
  187. return { ok: false, msg: '用户不存在' }
  188. }
  189. const lockedUser = userRows[0]
  190. const lockedDeductLepao = Number(lockedOrder.lepao_count || 0)
  191. const lockedDeductIc = Number(lockedOrder.ic_count || 0)
  192. const beforeLepao = Number(lockedUser.lepao_count || 0)
  193. const beforeIc = Number(lockedUser.ic_count || 0)
  194. if (lockedDeductLepao > 0 && beforeLepao < lockedDeductLepao) {
  195. await conn.rollback()
  196. logger?.error?.(`退款支付已成功但扣次失败,需人工处理,订单号:${orderId}`)
  197. return { ok: false, msg: '支付已退款但扣减次数失败,请联系客服处理' }
  198. }
  199. const afterLepao = beforeLepao - lockedDeductLepao
  200. const afterIc = Math.max(0, beforeIc - lockedDeductIc)
  201. const [updateUserRes] = await conn.execute(
  202. 'UPDATE users SET lepao_count = ?, ic_count = ? WHERE uuid = ?',
  203. [afterLepao, afterIc, lockedOrder.create_user]
  204. )
  205. if (!updateUserRes || updateUserRes.affectedRows !== 1) {
  206. await conn.rollback()
  207. logger?.error?.(`退款支付已成功但更新用户失败,需人工处理,订单号:${orderId}`)
  208. return { ok: false, msg: '支付已退款但更新账户失败,请联系客服处理' }
  209. }
  210. const [updateOrderRes] = await conn.execute(
  211. 'UPDATE orders SET state = ? WHERE orderId = ? AND state = ?',
  212. [ORDER_STATE_REFUNDED, orderId, ORDER_STATE_COMPLETED]
  213. )
  214. if (!updateOrderRes || updateOrderRes.affectedRows !== 1) {
  215. await conn.rollback()
  216. logger?.error?.(`退款支付已成功但更新订单失败,需人工处理,订单号:${orderId}`)
  217. return { ok: false, msg: '支付已退款但更新订单失败,请联系客服处理' }
  218. }
  219. if (lockedDeductLepao !== 0) {
  220. await insertLedgerRecord({
  221. executor: conn,
  222. userUuid: lockedOrder.create_user,
  223. delta: -lockedDeductLepao,
  224. balanceBefore: beforeLepao,
  225. balanceAfter: afterLepao,
  226. bizType: 'purchase_refund',
  227. bizId: orderId,
  228. operatorUuid,
  229. remark: `订单退款:${orderId}`
  230. })
  231. }
  232. await conn.commit()
  233. return { ok: true, msg: '退款成功' }
  234. } catch (dbError) {
  235. try { await conn.rollback() } catch (_) { }
  236. logger?.error?.(`退款入账失败 ${orderId}: ${dbError.stack || dbError}`)
  237. return { ok: false, msg: '支付已退款但入账失败,请联系客服处理' }
  238. }
  239. } catch (error) {
  240. logger?.error?.(`订单退款失败 ${orderId}: ${error.stack || error}`)
  241. return { ok: false, msg: error.message || '退款失败,请稍后再试' }
  242. }
  243. }
  244. module.exports = {
  245. REFUND_WINDOW_MS,
  246. ORDER_STATE_REFUNDED,
  247. evaluateRefundEligibility,
  248. executeOrderRefund
  249. }