package service import ( "context" "errors" "fmt" "strings" "time" "affiliate_dash/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) const rockTokenBatchRestoreMax = 500 // 恢复被跳过的原因。 const ( RockRestoreSkipNotDeleted = "not_deleted" RockRestoreSkipDuplicateUpstream = "duplicate_upstream" RockRestoreSkipOrderConflict = "order_conflict" ) // RockTokenRestoreSkipItem 表示恢复时被跳过的记录及原因。 type RockTokenRestoreSkipItem struct { ID uint `json:"id"` Status string `json:"status,omitempty"` Reason string `json:"reason"` } // RockTokenRestoreResult 是批量恢复结果。 type RockTokenRestoreResult struct { Requested int `json:"requested"` Restored int `json:"restored"` Skipped int `json:"skipped"` StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"` SkippedItems []RockTokenRestoreSkipItem `json:"skipped_items"` } // ListDeletedRockTokens 分页查询回收站(deleted_at 非空)中的 Token。 func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize int, status, search string) ([]model.RockToken, int64, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } status = strings.TrimSpace(status) if !isKnownRockTokenStatus(status) { return nil, 0, errors.New("无效的 Token 状态筛选") } db := applyRockTokenListFilters( s.db.WithContext(ctx).Unscoped().Model(&model.RockToken{}).Where("deleted_at IS NOT NULL"), status, search, ) var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var tokens []model.RockToken if err := db.Order("deleted_at DESC, id DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(&tokens).Error; err != nil { return nil, 0, err } for i := range tokens { tokens[i].TokenCode = tokens[i].Token if tokens[i].DeletedAt.Valid { tokens[i].DeletedAtText = tokens[i].DeletedAt.Time.Format(time.RFC3339) } } return tokens, total, nil } // RestoreRockTokens 恢复已软删除的 Token。 // 同一上游编号已在用、或同一订单已有在用 Token 时跳过该条,其余正常恢复(部分成功)。 func (s *RockService) RestoreRockTokens(ctx context.Context, ids []uint, actorUserID uint) (*RockTokenRestoreResult, error) { unique := make([]uint, 0, len(ids)) seen := make(map[uint]struct{}, len(ids)) for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} unique = append(unique, id) } if len(unique) == 0 { return nil, errors.New("请选择要恢复的 Token") } if len(unique) > rockTokenBatchRestoreMax { return nil, fmt.Errorf("单次最多恢复 %d 条 Token", rockTokenBatchRestoreMax) } result := &RockTokenRestoreResult{Requested: len(unique), SkippedItems: []RockTokenRestoreSkipItem{}} err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var rows []model.RockToken if err := tx.Unscoped().Clauses(clause.Locking{Strength: "UPDATE"}). Where("id IN ?", unique). Order("id ASC"). Find(&rows).Error; err != nil { return err } found := make(map[uint]model.RockToken, len(rows)) for _, row := range rows { found[row.ID] = row } // 先按请求顺序过滤出仍在回收站里的记录。 candidates := make([]model.RockToken, 0, len(unique)) for _, id := range unique { row, ok := found[id] if !ok || !row.DeletedAt.Valid { reason := RockRestoreSkipNotDeleted item := RockTokenRestoreSkipItem{ID: id, Reason: reason} if ok { item.Status = row.Status } result.SkippedItems = append(result.SkippedItems, item) continue } candidates = append(candidates, row) } // 冲突 1:同一 upstream_token_id 已存在未删除记录(部分唯一索引会拒绝恢复)。 upstreamIDs := make([]string, 0, len(candidates)) for _, row := range candidates { if value := strings.TrimSpace(row.UpstreamTokenID); value != "" { upstreamIDs = append(upstreamIDs, value) } } activeUpstream := make(map[string]struct{}, len(upstreamIDs)) if len(upstreamIDs) > 0 { var values []string if err := tx.Model(&model.RockToken{}). Where("upstream_token_id IN ?", upstreamIDs). Pluck("upstream_token_id", &values).Error; err != nil { return err } for _, value := range values { activeUpstream[value] = struct{}{} } } // 冲突 2:同一订单已经有其它在用/已发货 Token,恢复会造成一单两号。 orderNos := make([]string, 0, len(candidates)) for _, row := range candidates { if value := strings.TrimSpace(row.AssignedOrderNo); value != "" { orderNos = append(orderNos, value) } } activeOrders := make(map[string]struct{}, len(orderNos)) if len(orderNos) > 0 { var values []string if err := tx.Model(&model.RockToken{}). Where("assigned_order_no IN ?", orderNos). Pluck("assigned_order_no", &values).Error; err != nil { return err } for _, value := range values { activeOrders[value] = struct{}{} } } restorable := make([]model.RockToken, 0, len(candidates)) for _, row := range candidates { if upstream := strings.TrimSpace(row.UpstreamTokenID); upstream != "" { if _, conflict := activeUpstream[upstream]; conflict { result.SkippedItems = append(result.SkippedItems, RockTokenRestoreSkipItem{ ID: row.ID, Status: row.Status, Reason: RockRestoreSkipDuplicateUpstream, }) continue } } if orderNo := strings.TrimSpace(row.AssignedOrderNo); orderNo != "" { if _, conflict := activeOrders[orderNo]; conflict { result.SkippedItems = append(result.SkippedItems, RockTokenRestoreSkipItem{ ID: row.ID, Status: row.Status, Reason: RockRestoreSkipOrderConflict, }) continue } } restorable = append(restorable, row) } if len(restorable) > 0 { restoreIDs := make([]uint, 0, len(restorable)) for _, row := range restorable { restoreIDs = append(restoreIDs, row.ID) } if err := tx.Unscoped().Model(&model.RockToken{}). Where("id IN ?", restoreIDs). Updates(map[string]interface{}{"deleted_at": nil}).Error; err != nil { return err } result.Restored = len(restorable) result.StatusCounts = rockTokenStatusCountsFromRows(restorable) metadata := map[string]interface{}{ "requested": result.Requested, "restored": result.Restored, "skipped": len(result.SkippedItems), "status_counts": result.StatusCounts, "ids": restoreIDs, "skipped_items": result.SkippedItems, } if err := writeAudit(tx, nil, &actorUserID, nil, "rock_token.batch_restore", "rock_token", fmt.Sprintf("batch:%d", len(restoreIDs)), metadata); err != nil { return err } } result.Skipped = len(result.SkippedItems) return nil }) if err != nil { return nil, err } return result, nil }