topfans/backend/services/moderationService/repository/moderation_repository.go
2026-06-22 17:19:48 +08:00

341 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() }