341 lines
11 KiB
Go
341 lines
11 KiB
Go
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() }
|