package service import ( "context" "errors" "fmt" "strings" "affiliate_dash/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( rockTokenBatchDeleteMax = 500 rockTokenDeleteAllMax = 5000 ) var rockTokenDangerousStatuses = map[string]bool{ model.RockStatusUsing: true, model.RockStatusBound: true, model.RockStatusMismatch: true, } // RockTokenDeleteStatusCount 是删除预览中的状态分布。 type RockTokenDeleteStatusCount struct { Status string `json:"status"` Count int64 `json:"count"` } // RockTokenDeletePreview 是删除前的风险预览。 type RockTokenDeletePreview struct { Total int64 `json:"total"` Dangerous int64 `json:"dangerous"` Available int64 `json:"available"` RequiresForce bool `json:"requires_force"` StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"` DangerousItems []RockTokenDeleteItem `json:"dangerous_items"` } // RockTokenDeleteItem 是删除风险提示中的单条记录。 type RockTokenDeleteItem struct { ID uint `json:"id"` Status string `json:"status"` AssignedOrderNo string `json:"assigned_order_no,omitempty"` } // RockTokenDeleteResult 是软删除结果。 type RockTokenDeleteResult struct { Requested int `json:"requested"` Deleted int `json:"deleted"` Forced bool `json:"forced"` StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"` } // RockTokenForceRequiredError 表示本次删除包含使用中/已绑定/绑定错误 Token,需要强制确认。 type RockTokenForceRequiredError struct { Dangerous int64 `json:"dangerous"` StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"` DangerousItems []RockTokenDeleteItem `json:"dangerous_items"` } func (e *RockTokenForceRequiredError) Error() string { return fmt.Sprintf("本次操作包含 %d 条使用中/已绑定/绑定错误的 Token,必须二次确认后强制删除", e.Dangerous) } // RockTokenDeleteConflictError 表示预览后数据已变化。 type RockTokenDeleteConflictError struct { Message string } func (e *RockTokenDeleteConflictError) Error() string { if strings.TrimSpace(e.Message) != "" { return e.Message } return "删除条件对应的数据已变化,请刷新后重试" } func isKnownRockTokenStatus(status string) bool { switch status { case "", model.RockStatusAvailable, model.RockStatusUsing, model.RockStatusBound, model.RockStatusRedeemed, model.RockStatusMismatch, model.RockStatusAbnormal: return true default: return false } } // applyRockTokenListFilters 与 Token 列表使用同一套筛选条件,避免“看到多少、删多少”不一致。 func applyRockTokenListFilters(db *gorm.DB, status, search string) *gorm.DB { if value := strings.TrimSpace(status); value != "" { db = db.Where("status = ?", value) } if value := strings.TrimSpace(search); value != "" { term := "%" + value + "%" db = db.Where("CAST(id AS TEXT) = ? OR upstream_token_id = ? OR upstream_token_id LIKE ? OR assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR remark LIKE ? OR CAST(package_id AS TEXT) LIKE ? OR token LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)", value, value, term, term, term, term, term, term, term, term, term, term) } return db } func rockTokenStatusCountsFromRows(rows []model.RockToken) []RockTokenDeleteStatusCount { counts := map[string]int64{} for _, row := range rows { counts[row.Status]++ } order := []string{ model.RockStatusAvailable, model.RockStatusUsing, model.RockStatusBound, model.RockStatusRedeemed, model.RockStatusMismatch, model.RockStatusAbnormal, } result := make([]RockTokenDeleteStatusCount, 0, len(counts)) for _, status := range order { if count := counts[status]; count > 0 { result = append(result, RockTokenDeleteStatusCount{Status: status, Count: count}) } } return result } func rockTokenDangerousCount(rows []model.RockToken) int64 { var total int64 for _, row := range rows { if rockTokenDangerousStatuses[row.Status] { total++ } } return total } func rockTokenDangerousItems(rows []model.RockToken, limit int) []RockTokenDeleteItem { items := make([]RockTokenDeleteItem, 0, limit) for _, row := range rows { if !rockTokenDangerousStatuses[row.Status] { continue } items = append(items, RockTokenDeleteItem{ ID: row.ID, Status: row.Status, AssignedOrderNo: row.AssignedOrderNo, }) if len(items) >= limit { break } } return items } // PreviewRockTokenDelete 返回筛选条件下的删除风险预览,不修改任何数据。 func (s *RockService) PreviewRockTokenDelete(ctx context.Context, status, search string) (*RockTokenDeletePreview, error) { status = strings.TrimSpace(status) if !isKnownRockTokenStatus(status) { return nil, errors.New("无效的 Token 状态筛选") } var rows []model.RockToken query := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search) if err := query.Order("id DESC").Find(&rows).Error; err != nil { return nil, err } var available int64 for _, row := range rows { if row.Status == model.RockStatusAvailable { available++ } } dangerous := rockTokenDangerousCount(rows) return &RockTokenDeletePreview{ Total: int64(len(rows)), Dangerous: dangerous, Available: available, RequiresForce: dangerous > 0, StatusCounts: rockTokenStatusCountsFromRows(rows), DangerousItems: rockTokenDangerousItems(rows, 10), }, nil } // DeleteRockTokens 软删除指定 Token;包含危险状态时必须 force=true。 func (s *RockService) DeleteRockTokens(ctx context.Context, ids []uint, force bool, actorUserID uint) (*RockTokenDeleteResult, 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) > rockTokenBatchDeleteMax { return nil, fmt.Errorf("单次最多删除 %d 条 Token", rockTokenBatchDeleteMax) } result := &RockTokenDeleteResult{Requested: len(unique), Forced: force} err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var rows []model.RockToken if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id IN ?", unique). Order("id ASC"). Find(&rows).Error; err != nil { return err } if len(rows) != len(unique) { return &RockTokenDeleteConflictError{Message: "部分 Token 不存在或已被删除,请刷新列表后重试"} } result.StatusCounts = rockTokenStatusCountsFromRows(rows) dangerous := rockTokenDangerousCount(rows) if dangerous > 0 && !force { return &RockTokenForceRequiredError{ Dangerous: dangerous, StatusCounts: result.StatusCounts, DangerousItems: rockTokenDangerousItems(rows, 10), } } deleteResult := tx.Where("id IN ?", unique).Delete(&model.RockToken{}) if deleteResult.Error != nil { return deleteResult.Error } result.Deleted = int(deleteResult.RowsAffected) metadata := map[string]interface{}{ "requested": result.Requested, "deleted": result.Deleted, "forced": force, "status_counts": result.StatusCounts, "ids": unique, } return writeAudit(tx, nil, &actorUserID, nil, "rock_token.batch_delete", "rock_token", fmt.Sprintf("batch:%d", len(unique)), metadata) }) if err != nil { return nil, err } return result, nil } // DeleteAllRockTokens 按当前筛选条件软删除全部 Token;包含危险状态时必须 force=true。 func (s *RockService) DeleteAllRockTokens(ctx context.Context, status, search string, expectedCount int64, force bool, actorUserID uint) (*RockTokenDeleteResult, error) { status = strings.TrimSpace(status) if !isKnownRockTokenStatus(status) { return nil, errors.New("无效的 Token 状态筛选") } result := &RockTokenDeleteResult{Forced: force} err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var rows []model.RockToken query := applyRockTokenListFilters(tx.Model(&model.RockToken{}), status, search) if err := query.Clauses(clause.Locking{Strength: "UPDATE"}).Order("id ASC").Find(&rows).Error; err != nil { return err } if len(rows) > rockTokenDeleteAllMax { return fmt.Errorf("单次最多删除 %d 条 Token,请缩小筛选范围", rockTokenDeleteAllMax) } if expectedCount > 0 && int64(len(rows)) != expectedCount { return &RockTokenDeleteConflictError{Message: fmt.Sprintf("筛选结果已变化(预览 %d 条,当前 %d 条),请刷新后重试", expectedCount, len(rows))} } if len(rows) == 0 { return errors.New("当前筛选条件下没有可删除的 Token") } result.Requested = len(rows) result.StatusCounts = rockTokenStatusCountsFromRows(rows) dangerous := rockTokenDangerousCount(rows) if dangerous > 0 && !force { return &RockTokenForceRequiredError{ Dangerous: dangerous, StatusCounts: result.StatusCounts, DangerousItems: rockTokenDangerousItems(rows, 10), } } ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ID) } deleteResult := tx.Where("id IN ?", ids).Delete(&model.RockToken{}) if deleteResult.Error != nil { return deleteResult.Error } result.Deleted = int(deleteResult.RowsAffected) metadata := map[string]interface{}{ "requested": result.Requested, "deleted": result.Deleted, "forced": force, "status_filter": status, "search": strings.TrimSpace(search), "status_counts": result.StatusCounts, } return writeAudit(tx, nil, &actorUserID, nil, "rock_token.delete_all", "rock_token", fmt.Sprintf("filter:%s:%s", status, strings.TrimSpace(search)), metadata) }) if err != nil { return nil, err } return result, nil }