package repository import ( "context" "errors" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/topfans/backend/pkg/models" ) var ( ErrNotFound = errors.New("moderation record not found") ) type ModerationRepository struct { db *gorm.DB } func NewModerationRepository(db *gorm.DB) *ModerationRepository { return &ModerationRepository{db: db} } // ===== Report ===== func (r *ModerationRepository) CreateReport(ctx context.Context, report *models.Report) error { return r.db.WithContext(ctx).Create(report).Error } func (r *ModerationRepository) GetReportByID(ctx context.Context, id int64) (*models.Report, error) { var report models.Report err := r.db.WithContext(ctx).First(&report, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNotFound } return &report, err } func (r *ModerationRepository) GetReportByReporterAndTarget( ctx context.Context, reporterID int64, targetType string, targetID int64, ) (*models.Report, error) { var report models.Report err := r.db.WithContext(ctx). Where("reporter_id = ? AND target_type = ? AND target_id = ? AND status IN ?", reporterID, targetType, targetID, []string{"pending", "reviewing", "auto_hidden"}). First(&report).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNotFound } return &report, err } // ClaimReport - 用 affected rows 判定(spec §4.1 并发认领防护) func (r *ModerationRepository) ClaimReport( ctx context.Context, id int64, adminID int64, now int64, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("id = ? AND status IN ?", id, []string{"pending", "auto_hidden"}). Updates(map[string]interface{}{ "status": "reviewing", "claimed_by": adminID, "claimed_at": now, "updated_at": now, }) return res.RowsAffected, res.Error } func (r *ModerationRepository) ReleaseReport( ctx context.Context, id int64, adminID int64, now int64, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("id = ? AND status = 'reviewing' AND claimed_by = ?", id, adminID). Updates(map[string]interface{}{ "status": "pending", "claimed_by": nil, "claimed_at": nil, "updated_at": now, }) return res.RowsAffected, res.Error } // DismissPathA - pending → dismissed 快速通道(spec §4.1 路径 A) func (r *ModerationRepository) DismissPathA( ctx context.Context, id int64, adminID int64, now int64, note string, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("id = ? AND status = 'pending'", id). Updates(map[string]interface{}{ "status": "dismissed", "resolved_action": "dismiss", "claimed_by": nil, "claimed_at": nil, "resolved_by": adminID, "resolved_at": now, "resolution_note": note, "updated_at": now, }) return res.RowsAffected, res.Error } // ResolveFromReviewing - reviewing → resolved/dismissed(统一处理 path B/C/D) func (r *ModerationRepository) ResolveFromReviewing( ctx context.Context, id int64, adminID int64, now int64, newStatus, action, note string, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("id = ? AND status = 'reviewing' AND claimed_by = ?", id, adminID). Updates(map[string]interface{}{ "status": newStatus, "resolved_action": action, "resolved_by": adminID, "resolved_at": now, "claimed_by": nil, "claimed_at": nil, "resolution_note": note, "updated_at": now, }) return res.RowsAffected, res.Error } func (r *ModerationRepository) WithdrawReport( ctx context.Context, id int64, reporterID int64, now int64, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("id = ? AND reporter_id = ? AND status = 'pending'", id, reporterID). Updates(map[string]interface{}{ "status": "withdrawn", "updated_at": now, }) return res.RowsAffected, res.Error } // ListMyReports func (r *ModerationRepository) ListMyReports( ctx context.Context, reporterID int64, status string, page, pageSize int, ) ([]*models.Report, int64, error) { var reports []*models.Report var total int64 q := r.db.WithContext(ctx).Model(&models.Report{}).Where("reporter_id = ?", reporterID) if status != "" { q = q.Where("status = ?", status) } if err := q.Count(&total).Error; err != nil { return nil, 0, err } if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&reports).Error; err != nil { return nil, 0, err } return reports, total, nil } // ===== Evidence ===== func (r *ModerationRepository) CreateReportEvidence(ctx context.Context, ev *models.ReportEvidence) error { return r.db.WithContext(ctx).Create(ev).Error } func (r *ModerationRepository) GetReportEvidence(ctx context.Context, reportID int64) ([]*models.ReportEvidence, error) { var evs []*models.ReportEvidence err := r.db.WithContext(ctx).Where("report_id = ?", reportID).Order("sort_order ASC").Find(&evs).Error return evs, err } // ===== Feedback ===== func (r *ModerationRepository) CreateFeedback(ctx context.Context, fb *models.Feedback) error { return r.db.WithContext(ctx).Create(fb).Error } func (r *ModerationRepository) GetFeedbackByID(ctx context.Context, id int64) (*models.Feedback, error) { var fb models.Feedback err := r.db.WithContext(ctx).First(&fb, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNotFound } return &fb, err } func (r *ModerationRepository) ListMyFeedbacks( ctx context.Context, userID int64, status string, page, pageSize int, ) ([]*models.Feedback, int64, error) { var fbs []*models.Feedback var total int64 q := r.db.WithContext(ctx).Model(&models.Feedback{}).Where("user_id = ?", userID) if status != "" { q = q.Where("status = ?", status) } if err := q.Count(&total).Error; err != nil { return nil, 0, err } if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&fbs).Error; err != nil { return nil, 0, err } return fbs, total, nil } func (r *ModerationRepository) CreateFeedbackEvidence(ctx context.Context, ev *models.FeedbackEvidence) error { return r.db.WithContext(ctx).Create(ev).Error } func (r *ModerationRepository) GetFeedbackEvidence(ctx context.Context, feedbackID int64) ([]*models.FeedbackEvidence, error) { var evs []*models.FeedbackEvidence err := r.db.WithContext(ctx).Where("feedback_id = ?", feedbackID).Order("sort_order ASC").Find(&evs).Error return evs, err } // ===== ModerationAction ===== func (r *ModerationRepository) CreateAction(ctx context.Context, action *models.ModerationAction) error { return r.db.WithContext(ctx).Create(action).Error } // ===== ModerationTargetStatus - UPSERT ===== func (r *ModerationRepository) UpsertTargetStatus(ctx context.Context, mts *models.ModerationTargetStatus) error { return r.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "target_type"}, {Name: "target_id"}}, DoUpdates: clause.AssignmentColumns([]string{ "last_action_type", "reason", "source", "source_report_id", "operator_admin_id", "updated_at", "is_warned", "warn_count", "last_warned_at", }), }).Create(mts).Error } func (r *ModerationRepository) GetTargetStatus(ctx context.Context, targetType string, targetID int64) (*models.ModerationTargetStatus, error) { var mts models.ModerationTargetStatus err := r.db.WithContext(ctx).Where("target_type = ? AND target_id = ?", targetType, targetID).First(&mts).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrNotFound } return &mts, err } // ===== Categories ===== func (r *ModerationRepository) ListReportCategories(ctx context.Context) ([]*models.ReportCategory, error) { var cats []*models.ReportCategory err := r.db.WithContext(ctx).Where("enabled = TRUE").Order("sort_order ASC").Find(&cats).Error return cats, err } func (r *ModerationRepository) ListFeedbackCategories(ctx context.Context) ([]*models.FeedbackCategory, error) { var cats []*models.FeedbackCategory err := r.db.WithContext(ctx).Where("enabled = TRUE").Order("sort_order ASC").Find(&cats).Error return cats, err } func (r *ModerationRepository) IsReportCategoryEnabled(ctx context.Context, code string) (bool, error) { var count int64 err := r.db.WithContext(ctx).Model(&models.ReportCategory{}). Where("code = ? AND enabled = TRUE", code).Count(&count).Error return count > 0, err } func (r *ModerationRepository) IsFeedbackCategoryEnabled(ctx context.Context, code string) (bool, error) { var count int64 err := r.db.WithContext(ctx).Model(&models.FeedbackCategory{}). Where("code = ? AND enabled = TRUE", code).Count(&count).Error return count > 0, err } // ===== Asset / User - 业务表操作 ===== func (r *ModerationRepository) SoftDeleteAsset(ctx context.Context, id int64, now int64) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Asset{}). Where("id = ? AND is_active = TRUE", id). Updates(map[string]interface{}{"is_active": false, "deleted_at": now}) return res.RowsAffected, res.Error } func (r *ModerationRepository) RestoreAsset(ctx context.Context, id int64) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Asset{}). Where("id = ? AND is_active = FALSE", id). Updates(map[string]interface{}{"is_active": true, "deleted_at": nil}) return res.RowsAffected, res.Error } func (r *ModerationRepository) SoftDeleteUser(ctx context.Context, id int64, now int64) (int64, error) { res := r.db.WithContext(ctx).Model(&models.User{}). Where("id = ? AND is_active = TRUE", id). Updates(map[string]interface{}{"is_active": false, "deleted_at": now}) return res.RowsAffected, res.Error } func (r *ModerationRepository) RestoreUser(ctx context.Context, id int64) (int64, error) { res := r.db.WithContext(ctx).Model(&models.User{}). Where("id = ? AND is_active = FALSE", id). Updates(map[string]interface{}{"is_active": true, "deleted_at": nil}) return res.RowsAffected, res.Error } func (r *ModerationRepository) GetAssetOwnerUID(ctx context.Context, id int64) (int64, error) { var a models.Asset err := r.db.WithContext(ctx).Select("owner_uid").First(&a, id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return 0, ErrNotFound } return a.OwnerUID, err } // ===== 事务内批量操作(For path C 清老 auto_hidden 工单) ===== func (r *ModerationRepository) BulkDismissAutoHidden( ctx context.Context, targetType string, targetID int64, excludeReportID int64, adminID int64, now int64, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("target_type = ? AND target_id = ? AND status = 'auto_hidden' AND id != ?", targetType, targetID, excludeReportID). Updates(map[string]interface{}{ "status": "dismissed", "resolved_action": "dismiss", "resolved_by": adminID, "resolved_at": now, "resolution_note": "bulk cleanup after path C restore", "updated_at": now, }) return res.RowsAffected, res.Error } // ===== Report AutoHide 升级 ===== func (r *ModerationRepository) UpgradePendingToAutoHidden( ctx context.Context, targetType string, targetID int64, triggerReportID int64, now int64, ) (int64, error) { res := r.db.WithContext(ctx).Model(&models.Report{}). Where("target_type = ? AND target_id = ? AND status = 'pending'", targetType, targetID). Updates(map[string]interface{}{ "status": "auto_hidden", "is_auto_hidden": true, "triggered_auto_hide": gorm.Expr("(id = ?)", triggerReportID), "updated_at": now, }) return res.RowsAffected, res.Error } // ===== Helper ===== func Now() int64 { return time.Now().UnixMilli() }