CreateOrder.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 { validateCoupon, recordUsage, roundMoney } = require('../../lib/CouponService')
  9. const { normalizePayBaseUrl } = require('../../lib/PaymentClient')
  10. const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker')
  11. function generateOrderId() {
  12. const now = new Date()
  13. const pad = (n, w = 2) => n.toString().padStart(w, '0')
  14. return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
  15. `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` +
  16. `${pad(now.getMilliseconds(), 3)}`
  17. }
  18. function generatePaymentSign(params, key) {
  19. const sorted = Object.keys(params).sort()
  20. const query = sorted.map(k => `${k}=${params[k]}`).join('&') + key
  21. return crypto.createHash('md5').update(query, 'utf8').digest('hex')
  22. }
  23. async function acquireCouponUsageLock(couponId) {
  24. const lockKey = `coupon:usage:${couponId}`
  25. const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey])
  26. return { lockKey, ok: Number(rows?.[0]?.ok || 0) === 1 }
  27. }
  28. async function releaseCouponUsageLock(lockKey) {
  29. if (!lockKey) return
  30. try {
  31. await db.query('SELECT RELEASE_LOCK(?)', [lockKey])
  32. } catch (e) {
  33. // 释放失败仅记录,不影响主流程
  34. }
  35. }
  36. class CreateOrder extends API {
  37. constructor() {
  38. super()
  39. this.setPath('/Order/CreateOrder')
  40. this.setMethod('POST')
  41. }
  42. async onRequest(req, res) {
  43. const { uuid, session, goods_id, pay_type, coupon_code } = req.body
  44. if ([uuid, session, goods_id, pay_type].some(v => v === '' || v === null || v === undefined)) {
  45. return res.json({
  46. ...BaseStdResponse.MISSING_PARAMETER
  47. })
  48. }
  49. const sessionValid = await AccessControl.checkSession(uuid, session)
  50. if (!sessionValid) {
  51. return res.status(401).json({
  52. ...BaseStdResponse.ACCESS_DENIED
  53. })
  54. }
  55. let couponLockKey = null
  56. try {
  57. const goodsSql = 'SELECT name, price, num, state FROM goods WHERE id = ?'
  58. const goodsRows = await db.query(goodsSql, [goods_id])
  59. if (!goodsRows || goodsRows.length !== 1) {
  60. return res.json({
  61. ...BaseStdResponse.ERR,
  62. msg: '商品不存在'
  63. })
  64. }
  65. const goods = goodsRows[0]
  66. if (goods.num < 1 || goods.state !== 1) {
  67. return res.json({
  68. ...BaseStdResponse.ERR,
  69. msg: '商品已下架或库存不足'
  70. })
  71. }
  72. const createTime = Date.now()
  73. const orderId = generateOrderId()
  74. const originalPrice = roundMoney(goods.price)
  75. let finalPrice = originalPrice
  76. let discountAmount = 0
  77. let couponId = null
  78. let appliedCouponCode = null
  79. if (coupon_code && String(coupon_code).trim()) {
  80. let couponResult = await validateCoupon({
  81. code: coupon_code,
  82. userUuid: uuid,
  83. goodsId: goods_id,
  84. goodsPrice: goods.price
  85. })
  86. if (!couponResult.ok) {
  87. return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  88. }
  89. finalPrice = couponResult.finalPrice
  90. discountAmount = couponResult.discountAmount
  91. couponId = couponResult.couponId
  92. appliedCouponCode = couponResult.code
  93. const lockRet = await acquireCouponUsageLock(couponId)
  94. if (!lockRet.ok) {
  95. return res.json({
  96. ...BaseStdResponse.ERR,
  97. msg: '优惠码校验繁忙,请稍后重试'
  98. })
  99. }
  100. couponLockKey = lockRet.lockKey
  101. couponResult = await validateCoupon({
  102. code: coupon_code,
  103. userUuid: uuid,
  104. goodsId: goods_id,
  105. goodsPrice: goods.price
  106. })
  107. if (!couponResult.ok) {
  108. return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
  109. }
  110. finalPrice = couponResult.finalPrice
  111. discountAmount = couponResult.discountAmount
  112. couponId = couponResult.couponId
  113. appliedCouponCode = couponResult.code
  114. }
  115. const insertSql = `
  116. INSERT INTO orders (
  117. orderId, create_user, create_time, goods_id, price, pay_type,
  118. original_price, discount_amount, coupon_id, coupon_code
  119. )
  120. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  121. `
  122. const result = await db.query(insertSql, [
  123. orderId,
  124. uuid,
  125. createTime,
  126. goods_id,
  127. finalPrice,
  128. pay_type,
  129. originalPrice,
  130. discountAmount,
  131. couponId,
  132. appliedCouponCode
  133. ])
  134. const updateSql = 'UPDATE goods SET num = num - 1 WHERE id = ?'
  135. await db.query(updateSql, [goods_id])
  136. if (result && result.affectedRows > 0) {
  137. if (couponId) {
  138. await recordUsage(couponId, orderId, uuid, discountAmount)
  139. }
  140. const paymentConfig = config.pay || {}
  141. if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key || !paymentConfig.return_url) {
  142. return res.json({
  143. ...BaseStdResponse.ERR,
  144. msg: '支付配置错误,请联系管理员'
  145. })
  146. }
  147. const payBaseUrl = normalizePayBaseUrl(paymentConfig.url)
  148. const deviceType = req.headers['device-type'] ?? '浏览器'
  149. let return_url
  150. if (deviceType === 'RunForge Uniapp Client')
  151. return_url = paymentConfig.uni_return_url + orderId
  152. else
  153. return_url = paymentConfig.return_url + orderId
  154. const payParams = {
  155. pid: paymentConfig.pid,
  156. type: pay_type,
  157. out_trade_no: orderId,
  158. notify_url: `${config.url}/Order/CallBack`,
  159. return_url,
  160. name: goods.name,
  161. money: String(finalPrice)
  162. }
  163. const sign = generatePaymentSign(payParams, paymentConfig.key)
  164. payParams.sign = sign
  165. payParams.sign_type = 'MD5'
  166. await Redis.set(`payData:${orderId}`, JSON.stringify(payParams), {
  167. EX: 300
  168. })
  169. try {
  170. await enqueueOrderPaymentCheck(orderId)
  171. } catch (error) {
  172. this.logger.error(`推送订单支付检查消息到 MQ 失败,订单号:${orderId},错误:${error.stack || error}`)
  173. }
  174. res.json({
  175. ...BaseStdResponse.OK,
  176. id: orderId,
  177. pay: {
  178. payUrl: `${payBaseUrl}/submit.php`,
  179. payData: payParams
  180. }
  181. })
  182. } else {
  183. return res.json({
  184. ...BaseStdResponse.ERR,
  185. msg: '创建订单失败'
  186. })
  187. }
  188. } catch (err) {
  189. this.logger.error(`创建订单失败!${err.stack}`)
  190. return res.json({
  191. ...BaseStdResponse.ERR,
  192. msg: "创建订单异常,请联系管理员"
  193. })
  194. } finally {
  195. await releaseCouponUsageLock(couponLockKey)
  196. }
  197. }
  198. }
  199. module.exports.CreateOrder = CreateOrder