const API = require('../../lib/API') const db = require('../../plugin/DataBase/db') const Redis = require('../../plugin/DataBase/Redis') const { BaseStdResponse } = require('../../BaseStdResponse') const AccessControl = require('../../lib/AccessControl') const { validateCoupon, recordUsage, roundMoney } = require('../../lib/CouponService') const { buildPaymentSession } = require('../../lib/OrderPayment') const { createPaymentAttempt, generateGatewayOrderNo } = require('../../lib/OrderPaymentAttempt') const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker') function generateOrderId() { const now = new Date() const pad = (n, w = 2) => n.toString().padStart(w, '0') return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` + `${pad(now.getMilliseconds(), 3)}` } async function acquireCouponUsageLock(couponId) { const lockKey = `coupon:usage:${couponId}` const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey]) return { lockKey, ok: Number(rows?.[0]?.ok || 0) === 1 } } async function releaseCouponUsageLock(lockKey) { if (!lockKey) return try { await db.query('SELECT RELEASE_LOCK(?)', [lockKey]) } catch (_) { } } class CreateOrder extends API { constructor() { super() this.setPath('/Order/CreateOrder') this.setMethod('POST') } async onRequest(req, res) { const { uuid, session, goods_id, pay_type, coupon_code } = req.body if ([uuid, session, goods_id, pay_type].some(v => v === '' || v === null || v === undefined)) { return res.json({ ...BaseStdResponse.MISSING_PARAMETER }) } if (!await AccessControl.checkSession(uuid, session)) { return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED }) } let couponLockKey = null try { const goodsRows = await db.query( 'SELECT name, price, num, state FROM goods WHERE id = ? LIMIT 1', [goods_id] ) if (!goodsRows || goodsRows.length !== 1) { return res.json({ ...BaseStdResponse.ERR, msg: '商品不存在' }) } const goods = goodsRows[0] if (Number(goods.num) < 1 || Number(goods.state) !== 1) { return res.json({ ...BaseStdResponse.ERR, msg: '商品已下架或库存不足' }) } const createTime = Date.now() const orderId = generateOrderId() const normalizedPayType = String(pay_type).trim() const originalPrice = roundMoney(goods.price) let finalPrice = originalPrice let discountAmount = 0 let couponId = null let appliedCouponCode = null if (coupon_code && String(coupon_code).trim()) { let couponResult = await validateCoupon({ code: coupon_code, userUuid: uuid, goodsId: goods_id, goodsPrice: goods.price }) if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg }) const lockRet = await acquireCouponUsageLock(couponResult.couponId) if (!lockRet.ok) { return res.json({ ...BaseStdResponse.ERR, msg: '优惠码校验繁忙,请稍后重试' }) } couponLockKey = lockRet.lockKey couponResult = await validateCoupon({ code: coupon_code, userUuid: uuid, goodsId: goods_id, goodsPrice: goods.price }) if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg }) finalPrice = couponResult.finalPrice discountAmount = couponResult.discountAmount couponId = couponResult.couponId appliedCouponCode = couponResult.code } const gatewayOrderNo = generateGatewayOrderNo(orderId) const payment = await buildPaymentSession({ orderId, gatewayOrderNo, payType: normalizedPayType, goodsName: goods.name, price: finalPrice, deviceType: req.headers['device-type'] }) const conn = await db.connect() let insertResult try { await conn.beginTransaction() const [stockRes] = await conn.execute( 'UPDATE goods SET num = num - 1 WHERE id = ? AND num > 0 AND state = 1', [goods_id] ) if (!stockRes || stockRes.affectedRows !== 1) { await conn.rollback() return res.json({ ...BaseStdResponse.ERR, msg: '商品库存不足或已下架' }) } const [orderRes] = await conn.execute( `INSERT INTO orders ( orderId, create_user, create_time, goods_id, price, pay_type, original_price, discount_amount, coupon_id, coupon_code ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ orderId, uuid, createTime, goods_id, finalPrice, normalizedPayType, originalPrice, discountAmount, couponId, appliedCouponCode ] ) insertResult = orderRes await createPaymentAttempt({ orderId, payType: normalizedPayType, gatewayOrderNo, executor: conn }) if (couponId) { await recordUsage(couponId, orderId, uuid, discountAmount, conn) } await conn.commit() } catch (writeError) { try { await conn.rollback() } catch (_) { } throw writeError } if (!insertResult || insertResult.affectedRows < 1) { return res.json({ ...BaseStdResponse.ERR, msg: '创建订单失败' }) } await Redis.set(`payData:${orderId}`, JSON.stringify(payment.payData), { EX: 300 }) try { await enqueueOrderPaymentCheck(orderId) } catch (error) { this.logger.error(`推送订单支付检查消息失败,订单号:${orderId},错误:${error.stack || error}`) } return res.json({ ...BaseStdResponse.OK, id: orderId, pay: payment }) } catch (err) { this.logger.error(`创建订单失败:${err.stack || err}`) return res.json({ ...BaseStdResponse.ERR, msg: '创建订单异常,请联系管理员' }) } finally { await releaseCouponUsageLock(couponLockKey) } } } module.exports.CreateOrder = CreateOrder