|
|
@@ -1,12 +1,11 @@
|
|
|
-const API = require("../../lib/API.js")
|
|
|
-const db = require("../../plugin/DataBase/db.js")
|
|
|
+const API = require('../../lib/API')
|
|
|
+const db = require('../../plugin/DataBase/db')
|
|
|
const Redis = require('../../plugin/DataBase/Redis')
|
|
|
-const { BaseStdResponse } = require("../../BaseStdResponse.js")
|
|
|
-const AccessControl = require("../../lib/AccessControl.js")
|
|
|
-const crypto = require('crypto')
|
|
|
-const config = require('../../config.json')
|
|
|
+const { BaseStdResponse } = require('../../BaseStdResponse')
|
|
|
+const AccessControl = require('../../lib/AccessControl')
|
|
|
const { validateCoupon, recordUsage, roundMoney } = require('../../lib/CouponService')
|
|
|
-const { normalizePayBaseUrl } = require('../../lib/PaymentClient')
|
|
|
+const { buildPaymentSession } = require('../../lib/OrderPayment')
|
|
|
+const { createPaymentAttempt, generateGatewayOrderNo } = require('../../lib/OrderPaymentAttempt')
|
|
|
const { enqueueOrderPaymentCheck } = require('../../plugin/mq/orderPaymentWorker')
|
|
|
|
|
|
function generateOrderId() {
|
|
|
@@ -17,13 +16,6 @@ function generateOrderId() {
|
|
|
`${pad(now.getMilliseconds(), 3)}`
|
|
|
}
|
|
|
|
|
|
-function generatePaymentSign(params, key) {
|
|
|
- const sorted = Object.keys(params).sort()
|
|
|
- const query = sorted.map(k => `${k}=${params[k]}`).join('&') + key
|
|
|
-
|
|
|
- return crypto.createHash('md5').update(query, 'utf8').digest('hex')
|
|
|
-}
|
|
|
-
|
|
|
async function acquireCouponUsageLock(couponId) {
|
|
|
const lockKey = `coupon:usage:${couponId}`
|
|
|
const rows = await db.query('SELECT GET_LOCK(?, 5) AS ok', [lockKey])
|
|
|
@@ -32,11 +24,7 @@ async function acquireCouponUsageLock(couponId) {
|
|
|
|
|
|
async function releaseCouponUsageLock(lockKey) {
|
|
|
if (!lockKey) return
|
|
|
- try {
|
|
|
- await db.query('SELECT RELEASE_LOCK(?)', [lockKey])
|
|
|
- } catch (e) {
|
|
|
- // 释放失败仅记录,不影响主流程
|
|
|
- }
|
|
|
+ try { await db.query('SELECT RELEASE_LOCK(?)', [lockKey]) } catch (_) { }
|
|
|
}
|
|
|
|
|
|
class CreateOrder extends API {
|
|
|
@@ -50,41 +38,31 @@ class CreateOrder extends API {
|
|
|
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
|
|
|
- })
|
|
|
+ return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
|
|
|
}
|
|
|
|
|
|
- const sessionValid = await AccessControl.checkSession(uuid, session)
|
|
|
- if (!sessionValid) {
|
|
|
- return res.status(401).json({
|
|
|
- ...BaseStdResponse.ACCESS_DENIED
|
|
|
- })
|
|
|
+ if (!await AccessControl.checkSession(uuid, session)) {
|
|
|
+ return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
|
|
|
}
|
|
|
|
|
|
let couponLockKey = null
|
|
|
try {
|
|
|
- const goodsSql = 'SELECT name, price, num, state FROM goods WHERE id = ?'
|
|
|
- const goodsRows = await db.query(goodsSql, [goods_id])
|
|
|
-
|
|
|
+ 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: '商品不存在'
|
|
|
- })
|
|
|
+ return res.json({ ...BaseStdResponse.ERR, msg: '商品不存在' })
|
|
|
}
|
|
|
|
|
|
const goods = goodsRows[0]
|
|
|
- if (goods.num < 1 || goods.state !== 1) {
|
|
|
- return res.json({
|
|
|
- ...BaseStdResponse.ERR,
|
|
|
- msg: '商品已下架或库存不足'
|
|
|
- })
|
|
|
+ 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
|
|
|
@@ -98,20 +76,11 @@ class CreateOrder extends API {
|
|
|
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
|
|
|
+ if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
|
|
|
|
|
|
- const lockRet = await acquireCouponUsageLock(couponId)
|
|
|
+ const lockRet = await acquireCouponUsageLock(couponResult.couponId)
|
|
|
if (!lockRet.ok) {
|
|
|
- return res.json({
|
|
|
- ...BaseStdResponse.ERR,
|
|
|
- msg: '优惠码校验繁忙,请稍后重试'
|
|
|
- })
|
|
|
+ return res.json({ ...BaseStdResponse.ERR, msg: '优惠码校验繁忙,请稍后重试' })
|
|
|
}
|
|
|
couponLockKey = lockRet.lockKey
|
|
|
|
|
|
@@ -121,9 +90,7 @@ class CreateOrder extends API {
|
|
|
goodsId: goods_id,
|
|
|
goodsPrice: goods.price
|
|
|
})
|
|
|
- if (!couponResult.ok) {
|
|
|
- return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
|
|
|
- }
|
|
|
+ if (!couponResult.ok) return res.json({ ...BaseStdResponse.ERR, msg: couponResult.msg })
|
|
|
|
|
|
finalPrice = couponResult.finalPrice
|
|
|
discountAmount = couponResult.discountAmount
|
|
|
@@ -131,97 +98,87 @@ class CreateOrder extends API {
|
|
|
appliedCouponCode = couponResult.code
|
|
|
}
|
|
|
|
|
|
- const insertSql = `
|
|
|
- INSERT INTO orders (
|
|
|
- orderId, create_user, create_time, goods_id, price, pay_type,
|
|
|
- original_price, discount_amount, coupon_id, coupon_code
|
|
|
- )
|
|
|
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
- `
|
|
|
- const result = await db.query(insertSql, [
|
|
|
+ const gatewayOrderNo = generateGatewayOrderNo(orderId)
|
|
|
+ const payment = await buildPaymentSession({
|
|
|
orderId,
|
|
|
- uuid,
|
|
|
- createTime,
|
|
|
- goods_id,
|
|
|
- finalPrice,
|
|
|
- pay_type,
|
|
|
- originalPrice,
|
|
|
- discountAmount,
|
|
|
- couponId,
|
|
|
- appliedCouponCode
|
|
|
- ])
|
|
|
-
|
|
|
- const updateSql = 'UPDATE goods SET num = num - 1 WHERE id = ?'
|
|
|
- await db.query(updateSql, [goods_id])
|
|
|
-
|
|
|
- if (result && result.affectedRows > 0) {
|
|
|
- if (couponId) {
|
|
|
- await recordUsage(couponId, orderId, uuid, discountAmount)
|
|
|
- }
|
|
|
-
|
|
|
- const paymentConfig = config.pay || {}
|
|
|
+ gatewayOrderNo,
|
|
|
+ payType: normalizedPayType,
|
|
|
+ goodsName: goods.name,
|
|
|
+ price: finalPrice,
|
|
|
+ deviceType: req.headers['device-type']
|
|
|
+ })
|
|
|
|
|
|
- if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key || !paymentConfig.return_url) {
|
|
|
- return res.json({
|
|
|
- ...BaseStdResponse.ERR,
|
|
|
- msg: '支付配置错误,请联系管理员'
|
|
|
- })
|
|
|
- }
|
|
|
+ const conn = await db.connect()
|
|
|
+ let insertResult
|
|
|
+ try {
|
|
|
+ await conn.beginTransaction()
|
|
|
|
|
|
- const payBaseUrl = normalizePayBaseUrl(paymentConfig.url)
|
|
|
-
|
|
|
- const deviceType = req.headers['device-type'] ?? '浏览器'
|
|
|
- let return_url
|
|
|
- if (deviceType === 'RunForge Uniapp Client')
|
|
|
- return_url = paymentConfig.uni_return_url + orderId
|
|
|
- else
|
|
|
- return_url = paymentConfig.return_url + orderId
|
|
|
-
|
|
|
- const payParams = {
|
|
|
- pid: paymentConfig.pid,
|
|
|
- type: pay_type,
|
|
|
- out_trade_no: orderId,
|
|
|
- notify_url: `${config.url}/Order/CallBack`,
|
|
|
- return_url,
|
|
|
- name: goods.name,
|
|
|
- money: String(finalPrice)
|
|
|
+ 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 sign = generatePaymentSign(payParams, paymentConfig.key)
|
|
|
- payParams.sign = sign
|
|
|
- payParams.sign_type = 'MD5'
|
|
|
+ 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 Redis.set(`payData:${orderId}`, JSON.stringify(payParams), {
|
|
|
- EX: 300
|
|
|
+ await createPaymentAttempt({
|
|
|
+ orderId,
|
|
|
+ payType: normalizedPayType,
|
|
|
+ gatewayOrderNo,
|
|
|
+ executor: conn
|
|
|
})
|
|
|
|
|
|
- try {
|
|
|
- await enqueueOrderPaymentCheck(orderId)
|
|
|
- } catch (error) {
|
|
|
- this.logger.error(`推送订单支付检查消息到 MQ 失败,订单号:${orderId},错误:${error.stack || error}`)
|
|
|
+ if (couponId) {
|
|
|
+ await recordUsage(couponId, orderId, uuid, discountAmount, conn)
|
|
|
}
|
|
|
|
|
|
- res.json({
|
|
|
- ...BaseStdResponse.OK,
|
|
|
- id: orderId,
|
|
|
- pay: {
|
|
|
- payUrl: `${payBaseUrl}/submit.php`,
|
|
|
- payData: payParams
|
|
|
- }
|
|
|
- })
|
|
|
+ await conn.commit()
|
|
|
+ } catch (writeError) {
|
|
|
+ try { await conn.rollback() } catch (_) { }
|
|
|
+ throw writeError
|
|
|
+ }
|
|
|
|
|
|
- } else {
|
|
|
- return res.json({
|
|
|
- ...BaseStdResponse.ERR,
|
|
|
- msg: '创建订单失败'
|
|
|
- })
|
|
|
+ if (!insertResult || insertResult.affectedRows < 1) {
|
|
|
+ return res.json({ ...BaseStdResponse.ERR, msg: '创建订单失败' })
|
|
|
}
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(`创建订单失败!${err.stack}`)
|
|
|
+
|
|
|
+ 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.ERR,
|
|
|
- msg: "创建订单异常,请联系管理员"
|
|
|
+ ...BaseStdResponse.OK,
|
|
|
+ id: orderId,
|
|
|
+ pay: payment
|
|
|
})
|
|
|
+ } catch (err) {
|
|
|
+ this.logger.error(`创建订单失败:${err.stack || err}`)
|
|
|
+ return res.json({ ...BaseStdResponse.ERR, msg: '创建订单异常,请联系管理员' })
|
|
|
} finally {
|
|
|
await releaseCouponUsageLock(couponLockKey)
|
|
|
}
|