订单取消退券逻辑依赖 used_order_id 匹配,但金额券在下单时 不设置 used_order_id(仅在支付确认后设置),导致未支付订单 取消时 WHERE 条件匹配不到行,退券静默失败。 修复:去掉 used_order_id 条件,按券 ID 直接退还,增加幂等 校验和错误处理,兜底从流水回推预扣金额。
144 lines
4.3 KiB
Go
Executable File
144 lines
4.3 KiB
Go
Executable File
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
|
||
}
|