package snapshot import ( "context" "encoding/json" "errors" "fmt" "time" "bindbox-game/internal/pkg/pay" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/model" "gorm.io/gorm" ) // RollbackResult 回滚结果 type RollbackResult struct { Success bool `json:"success"` RollbackLogID int64 `json:"rollback_log_id"` PointsRestored int64 `json:"points_restored"` CouponsRestored int `json:"coupons_restored"` ItemCardsRestored int `json:"item_cards_restored"` InventoryRevoked int `json:"inventory_revoked"` RefundAmount int64 `json:"refund_amount"` SubsequentOrdersCount int `json:"subsequent_orders_count"` SubsequentOrders []string `json:"subsequent_orders"` RedemptionsReverted int `json:"redemptions_reverted"` ErrorMsg string `json:"error_msg,omitempty"` } // RollbackService 回滚服务 type RollbackService interface { // ExecuteRollback 执行时间点完整回滚 ExecuteRollback(ctx context.Context, orderID int64, operatorID int64, operatorName string, reason string) (*RollbackResult, error) } type rollbackService struct { db mysql.Repo snapshot Service } // NewRollbackService 创建回滚服务 func NewRollbackService(db mysql.Repo, snapshot Service) RollbackService { return &rollbackService{ db: db, snapshot: snapshot, } } // ExecuteRollback 执行时间点完整回滚 // 回滚到目标订单消费前的状态,撤销该时间点之后的所有变化 func (s *rollbackService) ExecuteRollback(ctx context.Context, orderID int64, operatorID int64, operatorName string, reason string) (*RollbackResult, error) { result := &RollbackResult{} // 1. 查询目标订单 var order model.Orders if err := s.db.GetDbR().WithContext(ctx).Where("id = ?", orderID).First(&order).Error; err != nil { return nil, fmt.Errorf("订单不存在: %w", err) } // 2. 检查订单状态(只有已支付状态可以回滚) if order.Status != 2 { return nil, errors.New("订单状态不允许回滚,只有已支付订单可以回滚") } // 3. 检查是否已有回滚记录 var existingRollback model.AuditRollbackLogs if err := s.db.GetDbR().WithContext(ctx).Where("order_id = ? AND status = 1", orderID).First(&existingRollback).Error; err == nil { return nil, errors.New("该订单已经执行过回滚") } // 4. 获取快照 beforeSnap, afterSnap, err := s.snapshot.GetSnapshotsByOrderID(ctx, orderID) if err != nil { return nil, fmt.Errorf("获取快照失败: %w", err) } if beforeSnap == nil { return nil, errors.New("未找到消费前快照") } // 5. 解析目标状态 var targetState UserStateSnapshot if err := json.Unmarshal([]byte(beforeSnap.SnapshotData), &targetState); err != nil { return nil, fmt.Errorf("解析快照失败: %w", err) } // 获取快照时间点 snapshotTime := beforeSnap.CreatedAt // 6. 开始事务执行时间点完整回滚 var rollbackLogID int64 changesApplied := map[string]any{} var totalRefundAmount int64 err = s.db.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error { userID := order.UserID // ============ 步骤1: 查询并回滚该时间点之后的所有订单 ============ var subsequentOrders []model.Orders tx.Where("user_id = ? AND id > ? AND status = 2", userID, orderID).Order("id DESC").Find(&subsequentOrders) subsequentOrderNos := []string{} for _, subOrder := range subsequentOrders { subsequentOrderNos = append(subsequentOrderNos, subOrder.OrderNo) // 作废后续订单产生的资产 tx.Model(&model.UserInventory{}).Where("order_id = ? AND status IN (1,3)", subOrder.ID).Update("status", 2) // 恢复后续订单使用的优惠券 if subOrder.CouponID > 0 { tx.Model(&model.UserCoupons{}).Where("id = ?", subOrder.CouponID).Updates(map[string]any{ "status": 1, "used_order_id": 0, "used_at": nil, }) } // 恢复后续订单使用的道具卡 if subOrder.ItemCardID > 0 { tx.Model(&model.UserItemCards{}).Where("id = ?", subOrder.ItemCardID).Updates(map[string]any{ "status": 1, "used_draw_log_id": 0, "used_activity_id": 0, "used_issue_id": 0, "used_at": nil, }) } // 取消发货记录 tx.Model(&model.ShippingRecords{}).Where("order_id = ?", subOrder.ID).Update("status", 5) // 更新订单状态为已回滚 tx.Model(&subOrder).Update("status", 5) // 累计退款金额 if subOrder.ActualAmount > 0 { totalRefundAmount += subOrder.ActualAmount } } result.SubsequentOrdersCount = len(subsequentOrders) result.SubsequentOrders = subsequentOrderNos changesApplied["subsequent_orders"] = subsequentOrderNos // ============ 步骤2: 撤销该时间点之后的积分兑换记录 ============ // 查询资产兑换记录(status=3表示已兑换) var redeemedInventories []model.UserInventory tx.Where("user_id = ? AND status = 3 AND updated_at > ?", userID, snapshotTime).Find(&redeemedInventories) for _, inv := range redeemedInventories { // 恢复资产状态为持有 tx.Model(&inv).Update("status", 1) result.RedemptionsReverted++ } changesApplied["redemptions_reverted"] = result.RedemptionsReverted // ============ 步骤3: 撤销该时间点之后的优惠券兑换 ============ // 查询该时间点之后获取的优惠券(积分兑换的) var redeemedCoupons []model.UserCoupons tx.Where("user_id = ? AND created_at > ? AND status = 1 AND source LIKE '%redeem%'", userID, snapshotTime).Find(&redeemedCoupons) for _, coupon := range redeemedCoupons { // 作废兑换的优惠券 tx.Model(&coupon).Update("status", 3) // 过期/作废 } changesApplied["coupons_voided"] = len(redeemedCoupons) // ============ 步骤4: 撤销该时间点之后的道具卡兑换 ============ var redeemedCards []model.UserItemCards tx.Where("user_id = ? AND created_at > ? AND status = 1 AND source LIKE '%redeem%'", userID, snapshotTime).Find(&redeemedCards) for _, card := range redeemedCards { // 作废兑换的道具卡 tx.Model(&card).Update("status", 3) } changesApplied["item_cards_voided"] = len(redeemedCards) // ============ 步骤5: 恢复目标订单的优惠券 ============ if order.CouponID > 0 { for _, c := range targetState.Coupons { if c.UserCouponID == order.CouponID { updates := map[string]any{ "status": c.Status, "balance_amount": c.BalanceAmount, "used_order_id": 0, "used_at": nil, } if err := tx.Model(&model.UserCoupons{}).Where("id = ?", order.CouponID).Updates(updates).Error; err != nil { return fmt.Errorf("恢复优惠券失败: %w", err) } result.CouponsRestored++ break } } if result.CouponsRestored == 0 { updates := map[string]any{ "status": 1, "used_order_id": 0, "used_at": nil, } if err := tx.Model(&model.UserCoupons{}).Where("id = ?", order.CouponID).Updates(updates).Error; err == nil { result.CouponsRestored++ } } } changesApplied["coupons_restored"] = result.CouponsRestored // ============ 步骤6: 恢复目标订单的道具卡 ============ if order.ItemCardID > 0 { for _, c := range targetState.ItemCards { if c.UserItemCardID == order.ItemCardID { updates := map[string]any{ "status": c.Status, "used_draw_log_id": 0, "used_activity_id": 0, "used_issue_id": 0, "used_at": nil, } if err := tx.Model(&model.UserItemCards{}).Where("id = ?", order.ItemCardID).Updates(updates).Error; err != nil { return fmt.Errorf("恢复道具卡失败: %w", err) } result.ItemCardsRestored++ break } } if result.ItemCardsRestored == 0 { updates := map[string]any{ "status": 1, "used_draw_log_id": 0, "used_activity_id": 0, "used_issue_id": 0, "used_at": nil, } if err := tx.Model(&model.UserItemCards{}).Where("id = ?", order.ItemCardID).Updates(updates).Error; err == nil { result.ItemCardsRestored++ } } } changesApplied["item_cards_restored"] = result.ItemCardsRestored // ============ 步骤7: 作废目标订单产生的资产 ============ updateRes := tx.Model(&model.UserInventory{}).Where("order_id = ? AND status IN (1,3)", orderID).Update("status", 2) if updateRes.Error != nil { return fmt.Errorf("作废资产失败: %w", updateRes.Error) } result.InventoryRevoked = int(updateRes.RowsAffected) changesApplied["inventory_revoked"] = result.InventoryRevoked // ============ 步骤8: 取消目标订单发货记录 ============ tx.Model(&model.ShippingRecords{}).Where("order_id = ?", orderID).Update("status", 5) // ============ 步骤9: 恢复积分到目标状态 ============ if targetState.Points != nil { var currentPoints model.UserPoints if err := tx.Where("user_id = ?", userID).First(¤tPoints).Error; err == nil { pointsDiff := targetState.Points.Balance - currentPoints.Points if pointsDiff != 0 { if err := tx.Model(¤tPoints).Update("points", targetState.Points.Balance).Error; err != nil { return fmt.Errorf("恢复积分失败: %w", err) } // 记录积分流水 ledger := &model.UserPointsLedger{ UserID: userID, Action: "rollback_restore", Points: pointsDiff, RefTable: "orders", RefID: order.OrderNo, Remark: fmt.Sprintf("时间点回滚恢复积分(含后续%d单): %s", len(subsequentOrders), reason), } if err := tx.Create(ledger).Error; err != nil { return fmt.Errorf("记录积分流水失败: %w", err) } result.PointsRestored = pointsDiff changesApplied["points_restored"] = pointsDiff } } } // ============ 步骤10: 更新目标订单状态为已回滚 ============ if err := tx.Model(&order).Update("status", 5).Error; err != nil { return fmt.Errorf("更新订单状态失败: %w", err) } // 累计目标订单退款金额 if order.ActualAmount > 0 { totalRefundAmount += order.ActualAmount } // ============ 步骤11: 记录回滚日志 ============ changesJSON, _ := json.Marshal(changesApplied) var beforeSnapID, afterSnapID int64 if beforeSnap != nil { beforeSnapID = beforeSnap.ID } if afterSnap != nil { afterSnapID = afterSnap.ID } rollbackLog := &model.AuditRollbackLogs{ OrderID: orderID, OrderNo: order.OrderNo, UserID: userID, OperatorID: operatorID, OperatorName: operatorName, RollbackType: 2, // 2=时间点完整回滚 BeforeSnapshotID: afterSnapID, AfterSnapshotID: beforeSnapID, ChangesApplied: string(changesJSON), Reason: reason, Status: 1, } if err := tx.Create(rollbackLog).Error; err != nil { return fmt.Errorf("记录回滚日志失败: %w", err) } rollbackLogID = rollbackLog.ID return nil }) if err != nil { result.Success = false result.ErrorMsg = err.Error() return result, err } // ============ 步骤12: 调用微信退款(事务外执行) ============ if totalRefundAmount > 0 { // 为每个需要退款的订单发起退款 s.refundOrders(ctx, order.UserID, orderID, reason, result) } result.Success = true result.RollbackLogID = rollbackLogID return result, nil } // refundOrders 为回滚的订单发起退款 func (s *rollbackService) refundOrders(ctx context.Context, userID int64, startOrderID int64, reason string, result *RollbackResult) { // 查询需要退款的订单(状态已变为5=已回滚) var ordersToRefund []model.Orders s.db.GetDbR().WithContext(ctx).Where("user_id = ? AND id >= ? AND status = 5 AND actual_amount > 0", userID, startOrderID).Find(&ordersToRefund) wc, err := pay.NewWechatPayClient(ctx) if err != nil { return } for _, refundOrder := range ordersToRefund { refundNo := fmt.Sprintf("RB%s-%d", refundOrder.OrderNo, time.Now().Unix()) _, status, err := wc.RefundOrder(ctx, refundOrder.OrderNo, refundNo, refundOrder.ActualAmount, refundOrder.ActualAmount, fmt.Sprintf("时间点回滚: %s", reason)) if err == nil { refundRecord := &model.PaymentRefunds{ OrderID: refundOrder.ID, OrderNo: refundOrder.OrderNo, RefundNo: refundNo, Channel: "wechat_jsapi", Status: status, AmountRefund: refundOrder.ActualAmount, Reason: fmt.Sprintf("时间点回滚: %s", reason), } s.db.GetDbW().WithContext(ctx).Create(refundRecord) result.RefundAmount += refundOrder.ActualAmount } } }