CreateOrder.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. const API = require('../../lib/API')
  2. const db = require('../../plugin/DataBase/db')
  3. const Redis = require('../../plugin/DataBase/Redis')
  4. const { BaseStdResponse } = require('../../BaseStdResponse')
  5. const AccessControl = require('../../lib/AccessControl')
  6. const { validateCoupon, recordUsage, roundMoney } = require('../../lib/CouponService')
  7. const { buildPaymentSession } = require('../../lib/OrderPayment')
  8. const { createPaymentAttempt, generateGatewayOrderNo } = require('../../lib/OrderPaymentAttempt')
  9. const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker')
  10. function generateOrderId() {
  11. const now = new Date()
  12. const pad = (n, w = 2) => n.toString().padStart(w, '0')
  13. return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
  14. `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
  15. `${pad(now.getMilliseconds(), 3)}`
  16. }
  17. async function acquireCouponUsageLock(couponId) {
  18. const lockKey = `coupon:usage:${couponId}`
  19. const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey])
  20. return { lockKey, ok: Number(rows?.[0]?.ok || 0) === 1 }
  21. }
  22. async function releaseCouponUsageLock(lockKey) {
  23. if (!lockKey) return
  24. try { await db.query('SELECT RELEASE_LOCK(?)', [lockKey]) } catch (_) { }
  25. }
  26. class CreateOrder extends API {
  27. constructor() {
  28. super()
  29. this.setPath('/Order/CreateOrder')
  30. this.setMethod('POST')
  31. }
  32. async onRequest(req, res) {
  33. const { uuid, session, goods_id, pay_type, coupon_code } = req.body
  34. if ([uuid, session, goods_id, pay_type].some(v => v === '' || v === null || v === undefined)) {
  35. return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
  36. }
  37. if (!await AccessControl.checkSession(uuid, session)) {
  38. return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
  39. }
  40. let couponLockKey = null
  41. try {
  42. const goodsRows = await db.query(
  43. 'SELECT name, price, num, state FROM goods WHERE id = ? LIMIT 1',
  44. [goods_id]
  45. )
  46. if (!goodsRows || goodsRows.length !== 1) {
  47. return res.json({ ...BaseStdResponse.ERR, msg: '商品不存在' })
  48. }
  49. const goods = goodsRows[0]
  50. if (Number(goods.num) < 1 || Number(goods.state) !== 1) {
  51. return res.json({ ...BaseStdResponse.ERR, msg: '商品已下架或库存不足' })
  52. }
  53. const createTime = Date.now()
  54. const orderId = generateOrderId()
  55. const normalizedPayType = String(pay_type).trim()
  56. const originalPrice = roundMoney(goods.price)
  57. let finalPrice = originalPrice
  58. let discountAmount = 0
  59. let couponId = null
  60. let appliedCouponCode = null
  61. if (coupon_code && String(coupon_code).trim()) {
  62. let couponResult = await validateCoupon({
  63. code: coupon_code,
  64. userUuid: uuid,
  65. goodsId: goods_id,
  66. goodsPrice: goods.price
  67. })
  68. if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  69. const lockRet = await acquireCouponUsageLock(couponResult.couponId)
  70. if (!lockRet.ok) {
  71. return res.json({ ...BaseStdResponse.ERR, msg: '优惠码校验繁忙,请稍后重试' })
  72. }
  73. couponLockKey = lockRet.lockKey
  74. couponResult = await validateCoupon({
  75. code: coupon_code,
  76. userUuid: uuid,
  77. goodsId: goods_id,
  78. goodsPrice: goods.price
  79. })
  80. if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  81. finalPrice = couponResult.finalPrice
  82. discountAmount = couponResult.discountAmount
  83. couponId = couponResult.couponId
  84. appliedCouponCode = couponResult.code
  85. }
  86. const gatewayOrderNo = generateGatewayOrderNo(orderId)
  87. const payment = await buildPaymentSession({
  88. orderId,
  89. gatewayOrderNo,
  90. payType: normalizedPayType,
  91. goodsName: goods.name,
  92. price: finalPrice,
  93. deviceType: req.headers['device-type']
  94. })
  95. const conn = await db.connect()
  96. let insertResult
  97. try {
  98. await conn.beginTransaction()
  99. const [stockRes] = await conn.execute(
  100. 'UPDATE goods SET num = num - 1 WHERE id = ? AND num > 0 AND state = 1',
  101. [goods_id]
  102. )
  103. if (!stockRes || stockRes.affectedRows !== 1) {
  104. await conn.rollback()
  105. return res.json({ ...BaseStdResponse.ERR, msg: '商品库存不足或已下架' })
  106. }
  107. const [orderRes] = await conn.execute(
  108. `INSERT INTO orders (
  109. orderId, create_user, create_time, goods_id, price, pay_type,
  110. original_price, discount_amount, coupon_id, coupon_code
  111. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  112. [
  113. orderId,
  114. uuid,
  115. createTime,
  116. goods_id,
  117. finalPrice,
  118. normalizedPayType,
  119. originalPrice,
  120. discountAmount,
  121. couponId,
  122. appliedCouponCode
  123. ]
  124. )
  125. insertResult = orderRes
  126. await createPaymentAttempt({
  127. orderId,
  128. payType: normalizedPayType,
  129. gatewayOrderNo,
  130. executor: conn
  131. })
  132. if (couponId) {
  133. await recordUsage(couponId, orderId, uuid, discountAmount, conn)
  134. }
  135. await conn.commit()
  136. } catch (writeError) {
  137. try { await conn.rollback() } catch (_) { }
  138. throw writeError
  139. }
  140. if (!insertResult || insertResult.affectedRows < 1) {
  141. return res.json({ ...BaseStdResponse.ERR, msg: '创建订单失败' })
  142. }
  143. await Redis.set(`payData:${orderId}`, JSON.stringify(payment.payData), { EX: 300 })
  144. try {
  145. await enqueueOrderPaymentCheck(orderId)
  146. } catch (error) {
  147. this.logger.error(`推送订单支付检查消息失败,订单号:${orderId},错误:${error.stack || error}`)
  148. }
  149. return res.json({
  150. ...BaseStdResponse.OK,
  151. id: orderId,
  152. pay: payment
  153. })
  154. } catch (err) {
  155. this.logger.error(`创建订单失败:${err.stack || err}`)
  156. return res.json({ ...BaseStdResponse.ERR, msg: err.message || '创建订单异常,请联系管理员' })
  157. } finally {
  158. await releaseCouponUsageLock(couponLockKey)
  159. }
  160. }
  161. }
  162. module.exports.CreateOrder = CreateOrder