bindbox-game/internal/service/user/orders_action.go
Zuncle b9a40df5c5 fix(coupon): 修复订单取消时金额券未退还的bug
订单取消退券逻辑依赖 used_order_id 匹配,但金额券在下单时
不设置 used_order_id(仅在支付确认后设置),导致未支付订单
取消时 WHERE 条件匹配不到行,退券静默失败。

修复:去掉 used_order_id 条件,按券 ID 直接退还,增加幂等
校验和错误处理,兜底从流水回推预扣金额。
2026-03-17 21:15:33 +08:00

144 lines
4.3 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"errors"
"time"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
"gorm.io/gorm/clause"
)
// CancelOrder 取消订单
func (s *service) CancelOrder(ctx context.Context, userID int64, orderID int64, reason string) (*model.Orders, error) {
var updatedOrder *model.Orders
err := s.writeDB.Transaction(func(tx *dao.Query) error {
// 1. 查询订单
order, err := tx.Orders.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.Orders.ID.Eq(orderID), tx.Orders.UserID.Eq(userID)).First()
if err != nil {
return err
}
if order == nil {
return errors.New("order not found")
}
// 2. 校验状态
if order.Status != 1 {
return errors.New("order cannot be cancelled")
}
// 3. 退还积分
if order.PointsAmount > 0 {
refundReason := "cancel_order"
if reason != "" {
refundReason = refundReason + ":" + reason
}
// order.PointsAmount 已经是分单位,直接退还
pointsToRefund := order.PointsAmount
// Update User Points
existing, _ := tx.UserPoints.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).First()
if existing == nil {
if err := tx.UserPoints.WithContext(ctx).Omit(tx.UserPoints.ValidEnd).Create(&model.UserPoints{UserID: userID, Points: pointsToRefund}); err != nil {
return err
}
} else {
if _, err := tx.UserPoints.WithContext(ctx).Where(tx.UserPoints.ID.Eq(existing.ID)).Updates(map[string]any{"points": existing.Points + pointsToRefund}); err != nil {
return err
}
}
// Log
led := &model.UserPointsLedger{UserID: userID, Action: "refund_points", Points: pointsToRefund, RefTable: "orders", RefID: order.OrderNo, Remark: refundReason}
if err := tx.UserPointsLedger.WithContext(ctx).Create(led); err != nil {
return err
}
}
// 4. 退还优惠券(恢复预扣的余额和状态)
if order.CouponID > 0 {
// 幂等校验:若已记录过 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
}
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
}
// 兜底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
}
}
}
}
}
// 5. 更新订单状态
updates := map[string]any{
tx.Orders.Status.ColumnName().String(): 3,
tx.Orders.CancelledAt.ColumnName().String(): time.Now(),
}
if _, err := tx.Orders.WithContext(ctx).Where(tx.Orders.ID.Eq(order.ID)).Updates(updates); err != nil {
return err
}
updatedOrder = order
updatedOrder.Status = 3
return nil
})
if err != nil {
return nil, err
}
return updatedOrder, nil
}