fix(coupon): 修复订单超时取消时金额券未退还的bug
问题:
commit b9a40df 修复了 CancelOrder()(用户/管理员主动取消)的券退回逻辑,
去掉了 `AND status = 4` 条件,但遗漏了 order_timeout.go 中超时取消的
同一逻辑,导致次数卡包(game_pass_package)订单超时取消时金额券余额丢失。
根因:
game_pass_package 下单时,金额券(type=1)通过 applyCouponToGamePassOrder()
直接扣减 balance_amount 并保持 status=1(有余额)或 status=2(用完),
不会设置 status=4(预扣中)。而 cancelExpiredOrder() 的 UPDATE 语句带有
`WHERE id = ? AND status = 4` 条件,导致匹配不到行,退券静默失败。
生产已确认影响:用户9110的券1690(订单28229)和券1532(订单26743)
因此bug各丢失10元余额。
修复:
- 去掉 `AND status = 4` 条件,改为 `WHERE id = ?`,兼容所有券状态
- 新增幂等校验:先查 timeout_refund 流水是否已存在,防止重复退还
- 新增兜底逻辑:order_coupons 无记录时,从 user_coupon_ledger 流水
回推预扣金额,与 CancelOrder() 的修复方案完全对齐
This commit is contained in:
parent
9cb4aaa511
commit
535106f158
@ -65,41 +65,52 @@ func (s *service) cleanupExpiredOrders() {
|
||||
func (s *service) cancelExpiredOrder(ctx context.Context, orderID int64, userID int64, couponID int64, pointsAmount int64) {
|
||||
// 1. 恢复优惠券
|
||||
if couponID > 0 {
|
||||
type couponRow struct {
|
||||
AppliedAmount int64
|
||||
DiscountType int32
|
||||
}
|
||||
var cr couponRow
|
||||
s.readDB.OrderCoupons.WithContext(ctx).UnderlyingDB().Raw(`
|
||||
SELECT oc.applied_amount, sc.discount_type
|
||||
FROM order_coupons oc
|
||||
JOIN user_coupons uc ON uc.id = oc.user_coupon_id
|
||||
JOIN system_coupons sc ON sc.id = uc.coupon_id
|
||||
WHERE oc.order_id = ? AND oc.user_coupon_id = ?
|
||||
`, orderID, couponID).Scan(&cr)
|
||||
// 幂等校验:若已记录过 timeout_refund 流水则跳过
|
||||
var refundCount int64
|
||||
s.readDB.UserCouponLedger.WithContext(ctx).UnderlyingDB().Raw(`
|
||||
SELECT COUNT(*) FROM user_coupon_ledger
|
||||
WHERE user_coupon_id = ? AND order_id = ? AND action = 'timeout_refund'
|
||||
`, couponID, orderID).Scan(&refundCount)
|
||||
|
||||
if cr.AppliedAmount > 0 {
|
||||
// 统一回退逻辑:无论券种,统统将预扣金额加回余额,并重置状态为 1 (未使用/有余额)
|
||||
res := s.writeDB.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(`
|
||||
UPDATE user_coupons
|
||||
SET balance_amount = balance_amount + ?,
|
||||
status = 1,
|
||||
used_order_id = NULL,
|
||||
used_at = NULL
|
||||
WHERE id = ? AND status = 4
|
||||
`, cr.AppliedAmount, couponID)
|
||||
if refundCount == 0 {
|
||||
// 优先从 order_coupons 获取实际抵扣金额
|
||||
var appliedAmount int64
|
||||
s.readDB.OrderCoupons.WithContext(ctx).UnderlyingDB().Raw(`
|
||||
SELECT applied_amount FROM order_coupons
|
||||
WHERE order_id = ? AND user_coupon_id = ?
|
||||
`, orderID, couponID).Scan(&appliedAmount)
|
||||
|
||||
if res.RowsAffected > 0 {
|
||||
// 记录流水
|
||||
s.writeDB.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: couponID,
|
||||
ChangeAmount: cr.AppliedAmount,
|
||||
BalanceAfter: 0, // 异步流水无法实时算最新,标记 0 或查询后填入,这里暂保持 Action
|
||||
OrderID: orderID,
|
||||
Action: "timeout_refund",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
// 兜底:order_coupons 无记录时,从流水中回推预扣金额
|
||||
if appliedAmount <= 0 {
|
||||
s.readDB.UserCouponLedger.WithContext(ctx).UnderlyingDB().Raw(`
|
||||
SELECT COALESCE(SUM(CASE WHEN change_amount < 0 THEN -change_amount ELSE 0 END), 0)
|
||||
FROM user_coupon_ledger
|
||||
WHERE user_id = ? AND user_coupon_id = ? AND order_id = ? AND action IN ('reserve', 'usage')
|
||||
`, userID, couponID, orderID).Scan(&appliedAmount)
|
||||
}
|
||||
|
||||
if appliedAmount > 0 {
|
||||
// 恢复余额 + 重置状态(不依赖 status 条件,兼容金额券 status=1/2 和冻结券 status=4)
|
||||
res := s.writeDB.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(`
|
||||
UPDATE user_coupons
|
||||
SET balance_amount = balance_amount + ?,
|
||||
status = 1,
|
||||
used_order_id = NULL,
|
||||
used_at = NULL
|
||||
WHERE id = ?
|
||||
`, appliedAmount, couponID)
|
||||
|
||||
if res.RowsAffected > 0 {
|
||||
// 记录流水
|
||||
s.writeDB.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: couponID,
|
||||
ChangeAmount: appliedAmount,
|
||||
OrderID: orderID,
|
||||
Action: "timeout_refund",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user