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 }