CouponService.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. const db = require('../plugin/DataBase/db')
  2. function roundMoney(n) {
  3. return Math.round(Number(n) * 100) / 100
  4. }
  5. function calcDiscount(price, discountType, discountValue) {
  6. const p = Number(price)
  7. let discount = 0
  8. if (discountType === 'percent') {
  9. const pct = Math.min(100, Math.max(0, Number(discountValue)))
  10. discount = roundMoney((p * pct) / 100)
  11. } else {
  12. discount = roundMoney(Math.min(p, Math.max(0, Number(discountValue))))
  13. }
  14. let finalPrice = roundMoney(p - discount)
  15. if (finalPrice < 0.01) finalPrice = 0.01
  16. if (finalPrice > p) finalPrice = p
  17. discount = roundMoney(p - finalPrice)
  18. return { discountAmount: discount, finalPrice }
  19. }
  20. async function countActiveUsage(couponId, userUuid) {
  21. const totalRows = await db.query(
  22. `SELECT COUNT(*) AS cnt FROM coupon_usage cu
  23. INNER JOIN orders o ON o.orderId = cu.order_id
  24. WHERE cu.coupon_id = ? AND o.state IN (0, 1, 2)`,
  25. [couponId]
  26. )
  27. const userRows = await db.query(
  28. `SELECT COUNT(*) AS cnt FROM coupon_usage cu
  29. INNER JOIN orders o ON o.orderId = cu.order_id
  30. WHERE cu.coupon_id = ? AND cu.user_uuid = ? AND o.state IN (0, 1, 2)`,
  31. [couponId, userUuid]
  32. )
  33. return {
  34. total: Number(totalRows?.[0]?.cnt || 0),
  35. perUser: Number(userRows?.[0]?.cnt || 0)
  36. }
  37. }
  38. /**
  39. * 校验优惠码并计算优惠后价格
  40. */
  41. async function validateCoupon({ code, userUuid, goodsId, goodsPrice }) {
  42. const normalizedCode = String(code || '').trim().toUpperCase()
  43. if (!normalizedCode) {
  44. return { ok: false, msg: '请输入优惠码' }
  45. }
  46. const rows = await db.query(
  47. `SELECT * FROM coupons WHERE code = ? AND state = 1 LIMIT 1`,
  48. [normalizedCode]
  49. )
  50. if (!rows || rows.length !== 1) {
  51. return { ok: false, msg: '优惠码不存在或已失效' }
  52. }
  53. const coupon = rows[0]
  54. const now = Date.now()
  55. if (coupon.start_time && now < Number(coupon.start_time)) {
  56. return { ok: false, msg: '优惠码尚未生效' }
  57. }
  58. if (coupon.end_time && now > Number(coupon.end_time)) {
  59. return { ok: false, msg: '优惠码已过期' }
  60. }
  61. const price = Number(goodsPrice)
  62. const minAmount = Number(coupon.min_amount || 0)
  63. if (minAmount > 0 && price < minAmount) {
  64. return { ok: false, msg: `订单满 ¥${minAmount} 才可使用该优惠码` }
  65. }
  66. if (Number(coupon.goods_scope) === 1) {
  67. const goodsRows = await db.query(
  68. 'SELECT 1 FROM coupon_goods WHERE coupon_id = ? AND goods_id = ? LIMIT 1',
  69. [coupon.id, goodsId]
  70. )
  71. if (!goodsRows || goodsRows.length === 0) {
  72. return { ok: false, msg: '该优惠码不适用于当前商品' }
  73. }
  74. }
  75. if (Number(coupon.user_scope) === 1) {
  76. const userRows = await db.query(
  77. 'SELECT 1 FROM coupon_users WHERE coupon_id = ? AND user_uuid = ? LIMIT 1',
  78. [coupon.id, userUuid]
  79. )
  80. if (!userRows || userRows.length === 0) {
  81. return { ok: false, msg: '您暂无使用该优惠码的权限' }
  82. }
  83. }
  84. const usage = await countActiveUsage(coupon.id, userUuid)
  85. const totalLimit = Number(coupon.total_limit || 0)
  86. const perUserLimit = Number(coupon.per_user_limit || 1)
  87. if (totalLimit > 0 && usage.total >= totalLimit) {
  88. return { ok: false, msg: '优惠码已达使用上限' }
  89. }
  90. if (usage.perUser >= perUserLimit) {
  91. return { ok: false, msg: '您已达到该优惠码的使用次数上限' }
  92. }
  93. const { discountAmount, finalPrice } = calcDiscount(
  94. price,
  95. coupon.discount_type,
  96. coupon.discount_value
  97. )
  98. if (discountAmount <= 0) {
  99. return { ok: false, msg: '优惠码无效,未产生优惠' }
  100. }
  101. return {
  102. ok: true,
  103. couponId: coupon.id,
  104. code: coupon.code,
  105. name: coupon.name,
  106. discountType: coupon.discount_type,
  107. discountValue: Number(coupon.discount_value),
  108. originalPrice: roundMoney(price),
  109. discountAmount,
  110. finalPrice,
  111. displayDiscount:
  112. coupon.discount_type === 'percent'
  113. ? `${coupon.discount_value}% 折扣`
  114. : `立减 ¥${coupon.discount_value}`
  115. }
  116. }
  117. async function recordUsage(couponId, orderId, userUuid, discountAmount) {
  118. const time = Date.now()
  119. await db.query(
  120. `INSERT INTO coupon_usage (coupon_id, order_id, user_uuid, discount_amount, create_time)
  121. VALUES (?, ?, ?, ?, ?)`,
  122. [couponId, orderId, userUuid, discountAmount, time]
  123. )
  124. await db.query('UPDATE coupons SET used_count = used_count + 1 WHERE id = ?', [couponId])
  125. }
  126. async function releaseUsageForOrder(orderId) {
  127. const rows = await db.query(
  128. 'SELECT coupon_id FROM orders WHERE orderId = ? AND coupon_id IS NOT NULL LIMIT 1',
  129. [orderId]
  130. )
  131. if (!rows || !rows[0]?.coupon_id) return
  132. const couponId = rows[0].coupon_id
  133. const del = await db.query('DELETE FROM coupon_usage WHERE order_id = ?', [orderId])
  134. if (del?.affectedRows > 0) {
  135. await db.query(
  136. 'UPDATE coupons SET used_count = GREATEST(used_count - 1, 0) WHERE id = ?',
  137. [couponId]
  138. )
  139. }
  140. }
  141. module.exports = {
  142. roundMoney,
  143. calcDiscount,
  144. validateCoupon,
  145. recordUsage,
  146. releaseUsageForOrder
  147. }