From b9a40df5c5fb0a36aa1bee8287a50a6c91867476 Mon Sep 17 00:00:00 2001 From: Zuncle <34310384@qq.com> Date: Tue, 17 Mar 2026 21:15:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(coupon):=20=E4=BF=AE=E5=A4=8D=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=8F=96=E6=B6=88=E6=97=B6=E9=87=91=E9=A2=9D=E5=88=B8?= =?UTF-8?q?=E6=9C=AA=E9=80=80=E8=BF=98=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 订单取消退券逻辑依赖 used_order_id 匹配,但金额券在下单时 不设置 used_order_id(仅在支付确认后设置),导致未支付订单 取消时 WHERE 条件匹配不到行,退券静默失败。 修复:去掉 used_order_id 条件,按券 ID 直接退还,增加幂等 校验和错误处理,兜底从流水回推预扣金额。 --- internal/service/user/orders_action.go | 80 ++++++++++++++++++-------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/internal/service/user/orders_action.go b/internal/service/user/orders_action.go index 9f61d0e..2ef69c5 100755 --- a/internal/service/user/orders_action.go +++ b/internal/service/user/orders_action.go @@ -59,32 +59,66 @@ func (s *service) CancelOrder(ctx context.Context, userID int64, orderID int64, // 4. 退还优惠券(恢复预扣的余额和状态) if order.CouponID > 0 { - var oc struct { - AppliedAmount int64 + // 幂等校验:若已记录过 cancel_refund 流水则跳过 + refundExists, err := tx.UserCouponLedger.WithContext(ctx).Where( + tx.UserCouponLedger.UserCouponID.Eq(order.CouponID), + tx.UserCouponLedger.OrderID.Eq(order.ID), + tx.UserCouponLedger.Action.Eq("cancel_refund"), + ).Count() + if err != nil { + return err } - // 获取该订单实际扣减的优惠券金额 - _ = tx.OrderCoupons.WithContext(ctx).Where(tx.OrderCoupons.OrderID.Eq(order.ID), tx.OrderCoupons.UserCouponID.Eq(order.CouponID)).Scan(&oc) - // 执行原子回退:增加余额 + 重置状态 + 清除占用订单 - res := tx.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(` - UPDATE user_coupons - SET balance_amount = balance_amount + ?, - status = 1, - used_order_id = 0, - used_at = NULL - WHERE id = ? AND used_order_id = ? - `, oc.AppliedAmount, order.CouponID, order.ID) + if refundExists == 0 { + var oc struct { + AppliedAmount int64 + } + // 优先从 order_coupons 获取实际抵扣金额 + if err := tx.OrderCoupons.WithContext(ctx).Where( + tx.OrderCoupons.OrderID.Eq(order.ID), + tx.OrderCoupons.UserCouponID.Eq(order.CouponID), + ).Scan(&oc); err != nil { + return err + } - if res.RowsAffected > 0 { - // 记录退还流水 - _ = tx.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{ - UserID: userID, - UserCouponID: order.CouponID, - ChangeAmount: oc.AppliedAmount, - OrderID: order.ID, - Action: "cancel_refund", - CreatedAt: time.Now(), - }) + // 兜底:order_coupons 无记录时,从流水中回推预扣金额 + if oc.AppliedAmount <= 0 { + if err := tx.UserCouponLedger.WithContext(ctx).UnderlyingDB().Raw(` + SELECT COALESCE(SUM(CASE WHEN change_amount < 0 THEN -change_amount ELSE 0 END), 0) AS applied_amount + FROM user_coupon_ledger + WHERE user_id = ? AND user_coupon_id = ? AND order_id = ? AND action IN ('reserve', 'usage') + `, userID, order.CouponID, order.ID).Scan(&oc).Error; err != nil { + return err + } + } + + if oc.AppliedAmount > 0 { + // 恢复余额 + 重置状态(不依赖 used_order_id) + res := tx.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(` + UPDATE user_coupons + SET balance_amount = balance_amount + ?, + status = 1, + used_order_id = 0, + used_at = NULL + WHERE id = ? + `, oc.AppliedAmount, order.CouponID) + if res.Error != nil { + return res.Error + } + + if res.RowsAffected > 0 { + if err := tx.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{ + UserID: userID, + UserCouponID: order.CouponID, + ChangeAmount: oc.AppliedAmount, + OrderID: order.ID, + Action: "cancel_refund", + CreatedAt: time.Now(), + }); err != nil { + return err + } + } + } } }