CreateOrder.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. const API = require("../../lib/API.js")
  2. const db = require("../../plugin/DataBase/db.js")
  3. const Redis = require('../../plugin/DataBase/Redis')
  4. const { BaseStdResponse } = require("../../BaseStdResponse.js")
  5. const AccessControl = require("../../lib/AccessControl.js")
  6. const crypto = require('crypto')
  7. const config = require('../../config.json')
  8. const mq = require('../../plugin/mq')
  9. const { mq: mqName } = require('../../plugin/mq/mqPrefix')
  10. const { insertLedgerRecord } = require('../../lib/Lepao/CountLedger')
  11. const { validateCoupon, recordUsage, releaseUsageForOrder, roundMoney } = require('../../lib/CouponService')
  12. const { normalizePayBaseUrl, queryPaymentOrder } = require('../../lib/PaymentClient')
  13. const ORDER_PAYMENT_QUEUE = mqName('order_payment_check')
  14. let orderPaymentWorkerStarted = false
  15. async function writePurchaseLedger(orderId, userUuid, addCount, logger) {
  16. const delta = Number(addCount || 0)
  17. if (!orderId || !userUuid || delta === 0) return
  18. try {
  19. const userRows = await db.query(
  20. 'SELECT lepao_count FROM users WHERE uuid = ?',
  21. [userUuid]
  22. )
  23. if (!userRows || userRows.length !== 1) return
  24. const afterCount = Number(userRows[0].lepao_count || 0)
  25. const beforeCount = afterCount - delta
  26. await insertLedgerRecord({
  27. userUuid,
  28. delta,
  29. balanceBefore: beforeCount,
  30. balanceAfter: afterCount,
  31. bizType: 'purchase',
  32. bizId: orderId,
  33. remark: `订单号:${orderId}`
  34. })
  35. } catch (error) {
  36. logger?.error?.(`写入购买次数流水失败 ${orderId}: ${error.stack || error}`)
  37. }
  38. }
  39. async function startOrderPaymentWorker(logger) {
  40. if (orderPaymentWorkerStarted) {
  41. return
  42. }
  43. try {
  44. const ch = await mq.getChannel('order_payment')
  45. await ch.assertQueue(ORDER_PAYMENT_QUEUE, {
  46. durable: true
  47. })
  48. await ch.prefetch(1)
  49. logger.info(`订单支付结果轮询消费者已启动,队列:${ORDER_PAYMENT_QUEUE}`)
  50. orderPaymentWorkerStarted = true
  51. ch.consume(ORDER_PAYMENT_QUEUE, async (msg) => {
  52. if (!msg) return
  53. const content = JSON.parse(msg.content.toString() || '{}')
  54. const { orderId } = content
  55. if (!orderId) {
  56. logger.warn('收到无效的订单支付检查消息(缺少 orderId)')
  57. ch.ack(msg)
  58. return
  59. }
  60. try {
  61. await pollOrderPaymentStatus(orderId, logger)
  62. ch.ack(msg)
  63. } catch (err) {
  64. logger.error(`订单支付轮询处理失败,订单号:${orderId},错误:${err.stack || err}`)
  65. // 简单重试策略:出现异常时,Nack 并重新入队
  66. ch.nack(msg, false, true)
  67. }
  68. })
  69. } catch (e) {
  70. logger.error(`启动订单支付 MQ 消费者失败:${e.stack || e}`)
  71. }
  72. }
  73. async function pollOrderPaymentStatus(orderId, logger) {
  74. const paymentConfig = config.pay || {}
  75. if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key) {
  76. logger.error('支付配置错误,无法轮询易支付状态')
  77. return
  78. }
  79. const MAX_RETRIES = 120 // 5分钟 / 5秒
  80. const DELAY = 2500 // 5秒
  81. const pollOrderStatus = async (retry = 0) => {
  82. if (retry >= MAX_RETRIES) {
  83. const closeRes = await db.query(
  84. 'UPDATE orders SET state = 3 WHERE orderId = ? AND state = 0',
  85. [orderId]
  86. )
  87. if (closeRes?.affectedRows > 0) {
  88. await releaseUsageForOrder(orderId)
  89. logger.info(`订单超时未支付,自动取消,订单号:${orderId}`)
  90. }
  91. return
  92. }
  93. try {
  94. const existing = await db.query('SELECT state FROM orders WHERE orderId = ?', [orderId])
  95. if (!existing?.length) {
  96. logger.warn(`订单不存在,停止轮询:${orderId}`)
  97. return
  98. }
  99. if (Number(existing[0].state) !== 0) {
  100. logger.info(`订单已处理(state=${existing[0].state}),停止轮询:${orderId}`)
  101. return
  102. }
  103. const queryData = await queryPaymentOrder(orderId, logger)
  104. logger.info(`轮询订单支付状态,订单号:${orderId},尝试次数:${retry + 1},查询结果:${JSON.stringify(queryData)}`)
  105. if (queryData.code == 1 && queryData.status == 1) {
  106. const { trade_no, out_trade_no, type } = queryData
  107. const time = Date.now()
  108. let sql = 'UPDATE orders SET state = 1, pay_type = ?, pay_id = ?, pay_time = ? WHERE orderId = ? AND state = 0'
  109. const result = await db.query(sql, [type, trade_no, time, out_trade_no])
  110. if (result.affectedRows > 0) {
  111. sql = `
  112. SELECT
  113. g.lepao_count,
  114. g.ic_count,
  115. g.vip,
  116. a.create_user
  117. FROM
  118. orders a
  119. LEFT JOIN
  120. goods g
  121. ON
  122. a.goods_id = g.id
  123. WHERE
  124. a.orderId = ?
  125. `
  126. const rows = await db.query(sql, [out_trade_no])
  127. if (!rows || rows.length !== 1) {
  128. logger.error(`订单商品信息异常,订单号:${out_trade_no}`)
  129. await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
  130. return
  131. }
  132. const { lepao_count, ic_count, vip, create_user } = rows[0]
  133. sql = 'UPDATE users SET lepao_count = lepao_count + ?, ic_count = ic_count + ?, vip = ? WHERE uuid = ?'
  134. const updateUser = await db.query(sql, [lepao_count, ic_count, vip, create_user])
  135. if (!updateUser || updateUser.affectedRows !== 1) {
  136. logger.error(`更新用户失败,UUID: ${create_user}`)
  137. await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
  138. }
  139. sql = 'UPDATE orders SET state = 2 WHERE orderId = ?'
  140. await db.query(sql, [out_trade_no])
  141. await writePurchaseLedger(out_trade_no, create_user, lepao_count, logger)
  142. logger.info(`订单处理成功:${out_trade_no}`)
  143. return
  144. }
  145. logger.info(`支付网关已确认付款,订单已由其他进程/回调处理,停止轮询:${out_trade_no}`)
  146. return
  147. }
  148. // 未支付,继续轮询
  149. setTimeout(() => pollOrderStatus(retry + 1), DELAY)
  150. } catch (error) {
  151. logger.warn(`轮询支付状态失败,订单号:${orderId},原因:${error.message || error}`)
  152. setTimeout(() => pollOrderStatus(retry + 1), DELAY)
  153. }
  154. }
  155. logger.info(`开始轮询订单支付状态,订单号:${orderId}`)
  156. pollOrderStatus()
  157. }
  158. function generateOrderId() {
  159. const now = new Date()
  160. const pad = (n, w = 2) => n.toString().padStart(w, '0')
  161. return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
  162. `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
  163. `${pad(now.getMilliseconds(), 3)}`
  164. }
  165. function generatePaymentSign(params, key) {
  166. const sorted = Object.keys(params).sort()
  167. const query = sorted.map(k => `${k}=${params[k]}`).join('&') + key
  168. return crypto.createHash('md5').update(query, 'utf8').digest('hex')
  169. }
  170. async function acquireCouponUsageLock(couponId) {
  171. const lockKey = `coupon:usage:${couponId}`
  172. const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey])
  173. return { lockKey, ok: Number(rows?.[0]?.ok || 0) === 1 }
  174. }
  175. async function releaseCouponUsageLock(lockKey) {
  176. if (!lockKey) return
  177. try {
  178. await db.query('SELECT RELEASE_LOCK(?)', [lockKey])
  179. } catch (e) {
  180. // 释放失败仅记录,不影响主流程
  181. }
  182. }
  183. class CreateOrder extends API {
  184. constructor() {
  185. super()
  186. this.setPath('/Order/CreateOrder')
  187. this.setMethod('POST')
  188. }
  189. async onRequest(req, res) {
  190. const { uuid, session, goods_id, pay_type, coupon_code } = req.body
  191. if ([uuid, session, goods_id, pay_type].some(v => v === '' || v === null || v === undefined)) {
  192. return res.json({
  193. ...BaseStdResponse.MISSING_PARAMETER
  194. })
  195. }
  196. const sessionValid = await AccessControl.checkSession(uuid, session)
  197. if (!sessionValid) {
  198. return res.status(401).json({
  199. ...BaseStdResponse.ACCESS_DENIED
  200. })
  201. }
  202. let couponLockKey = null
  203. try {
  204. const goodsSql = 'SELECT name, price, num, state FROM goods WHERE id = ?'
  205. const goodsRows = await db.query(goodsSql, [goods_id])
  206. if (!goodsRows || goodsRows.length !== 1) {
  207. return res.json({
  208. ...BaseStdResponse.ERR,
  209. msg: '商品不存在'
  210. })
  211. }
  212. const goods = goodsRows[0]
  213. if (goods.num < 1 || goods.state !== 1) {
  214. return res.json({
  215. ...BaseStdResponse.ERR,
  216. msg: '商品已下架或库存不足'
  217. })
  218. }
  219. const createTime = Date.now()
  220. const orderId = generateOrderId()
  221. const originalPrice = roundMoney(goods.price)
  222. let finalPrice = originalPrice
  223. let discountAmount = 0
  224. let couponId = null
  225. let appliedCouponCode = null
  226. if (coupon_code && String(coupon_code).trim()) {
  227. let couponResult = await validateCoupon({
  228. code: coupon_code,
  229. userUuid: uuid,
  230. goodsId: goods_id,
  231. goodsPrice: goods.price
  232. })
  233. if (!couponResult.ok) {
  234. return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  235. }
  236. finalPrice = couponResult.finalPrice
  237. discountAmount = couponResult.discountAmount
  238. couponId = couponResult.couponId
  239. appliedCouponCode = couponResult.code
  240. const lockRet = await acquireCouponUsageLock(couponId)
  241. if (!lockRet.ok) {
  242. return res.json({
  243. ...BaseStdResponse.ERR,
  244. msg: '优惠码校验繁忙,请稍后重试'
  245. })
  246. }
  247. couponLockKey = lockRet.lockKey
  248. // 拿到锁后重查一次,避免并发下单绕过“每人限用次数”
  249. couponResult = await validateCoupon({
  250. code: coupon_code,
  251. userUuid: uuid,
  252. goodsId: goods_id,
  253. goodsPrice: goods.price
  254. })
  255. if (!couponResult.ok) {
  256. return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  257. }
  258. finalPrice = couponResult.finalPrice
  259. discountAmount = couponResult.discountAmount
  260. couponId = couponResult.couponId
  261. appliedCouponCode = couponResult.code
  262. }
  263. const insertSql = `
  264. INSERT INTO orders (
  265. orderId, create_user, create_time, goods_id, price, pay_type,
  266. original_price, discount_amount, coupon_id, coupon_code
  267. )
  268. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  269. `
  270. const result = await db.query(insertSql, [
  271. orderId,
  272. uuid,
  273. createTime,
  274. goods_id,
  275. finalPrice,
  276. pay_type,
  277. originalPrice,
  278. discountAmount,
  279. couponId,
  280. appliedCouponCode
  281. ])
  282. const updateSql = 'UPDATE goods SET num = num - 1 WHERE id = ?'
  283. await db.query(updateSql, [goods_id])
  284. if (result && result.affectedRows > 0) {
  285. if (couponId) {
  286. await recordUsage(couponId, orderId, uuid, discountAmount)
  287. }
  288. const paymentConfig = config.pay || {}
  289. if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key || !paymentConfig.return_url) {
  290. return res.json({
  291. ...BaseStdResponse.ERR,
  292. msg: '支付配置错误,请联系管理员'
  293. })
  294. }
  295. const payBaseUrl = normalizePayBaseUrl(paymentConfig.url)
  296. const deviceType = req.headers['device-type'] ?? '浏览器'
  297. let return_url
  298. if(deviceType === 'RunForge Uniapp Client')
  299. return_url = paymentConfig.uni_return_url + orderId
  300. else
  301. return_url = paymentConfig.return_url + orderId
  302. const payParams = {
  303. pid: paymentConfig.pid,
  304. type: pay_type,
  305. out_trade_no: orderId,
  306. notify_url: `${config.url}/Order/CallBack`,
  307. return_url,
  308. name: goods.name,
  309. money: String(finalPrice)
  310. }
  311. const sign = generatePaymentSign(payParams, paymentConfig.key)
  312. payParams.sign = sign
  313. payParams.sign_type = 'MD5'
  314. await Redis.set(`payData:${orderId}`, JSON.stringify(payParams), {
  315. EX: 300
  316. })
  317. // 下单成功后推送到订单支付检查队列,由当前模块的消费者进行轮询及超时取消
  318. try {
  319. const ch = await mq.getChannel('order_payment')
  320. await ch.assertQueue(ORDER_PAYMENT_QUEUE, {
  321. durable: true
  322. })
  323. const msg = {
  324. orderId,
  325. enqueueTime: Date.now()
  326. }
  327. ch.sendToQueue(ORDER_PAYMENT_QUEUE, Buffer.from(JSON.stringify(msg)), {
  328. persistent: true
  329. })
  330. } catch (error) {
  331. this.logger.error(`推送订单支付检查消息到 MQ 失败,订单号:${orderId},错误:${error.stack || error}`)
  332. }
  333. res.json({
  334. ...BaseStdResponse.OK,
  335. id: orderId,
  336. pay: {
  337. payUrl: `${payBaseUrl}/submit.php`,
  338. payData: payParams
  339. }
  340. })
  341. } else {
  342. return res.json({
  343. ...BaseStdResponse.ERR,
  344. msg: '创建订单失败'
  345. })
  346. }
  347. } catch (err) {
  348. this.logger.error(`创建订单失败!${err.stack}`)
  349. return res.json({
  350. ...BaseStdResponse.ERR,
  351. msg: "创建订单异常,请联系管理员"
  352. })
  353. } finally {
  354. await releaseCouponUsageLock(couponLockKey)
  355. }
  356. }
  357. }
  358. module.exports.CreateOrder = CreateOrder
  359. module.exports.startOrderPaymentWorker = startOrderPaymentWorker