restrict v8
This commit is contained in:
@@ -25,8 +25,41 @@ type ReportsStore interface {
|
||||
QuestionIDsByUser(ctx context.Context, userID uint) (map[uint]int64, error)
|
||||
// CountByUserSince 统计某箱主自 since 起收到的待处理举报总数(邮件告警的触发依据)。
|
||||
CountByUserSince(ctx context.Context, userID uint, since time.Time) (int64, error)
|
||||
// MarkAcceptedByQuestion 把某条提问下所有「待处理」举报标记为「成立」。
|
||||
//
|
||||
// 箱主删除或设为私密时调用。举报一旦被处理就应当离开待处理池,否则告警会被
|
||||
// 「早就处理完」的旧举报持续推高,几周后这个告警就没人看了(狼来了)。
|
||||
// 返回受影响行数,便于调用方记日志。
|
||||
MarkAcceptedByQuestion(ctx context.Context, questionID uint) (int64, error)
|
||||
// CountByQuestion 统计某条内容当前的待处理举报数(用于「该条被多人举报,提醒箱主自查」)。
|
||||
CountByQuestion(ctx context.Context, questionID uint) (int64, error)
|
||||
// TopReportedUsers 按待处理举报数返回被举报最多的箱主(运维视图,用于识别组织化挂人)。
|
||||
TopReportedUsers(ctx context.Context, since time.Time, limit int) ([]*ReportedUserStat, error)
|
||||
// TopReporters 按举报数返回最活跃的举报来源 IP(用于识别「举报被武器化」)。
|
||||
TopReporters(ctx context.Context, since time.Time, limit int) ([]*ReporterStat, error)
|
||||
}
|
||||
|
||||
// ReportedUserStat 被举报箱主的聚合行。
|
||||
type ReportedUserStat struct {
|
||||
UserID uint
|
||||
Domain string
|
||||
Name string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// ReporterStat 举报来源聚合行。
|
||||
type ReporterStat struct {
|
||||
ReporterIP string
|
||||
Count int64
|
||||
}
|
||||
|
||||
// reportPendingTTL 待处理举报的统计有效期。
|
||||
//
|
||||
// 超过这个窗口仍未处理的举报不再计入箱主侧标记与告警阈值:它们既没有被箱主处理,
|
||||
// 也没有被运维处置,继续累计只会让阈值失去意义。用「查询时按时间过滤」而不是
|
||||
// 定时任务批量改状态 —— 无后台的项目里不需要额外的调度器,语义也足够清晰。
|
||||
const reportPendingTTL = 30 * 24 * time.Hour
|
||||
|
||||
// Reports 举报记录全局 store,由 db.Init 装配。
|
||||
var Reports ReportsStore
|
||||
|
||||
@@ -96,6 +129,7 @@ func (s *reports) QuestionIDsByUser(ctx context.Context, userID uint) (map[uint]
|
||||
Joins("JOIN questions q ON q.id = r.question_id").
|
||||
Where("q.user_id = ? AND q.deleted_at IS NULL", userID).
|
||||
Where("r.status = ?", ReportStatusPending).
|
||||
Where("r.created_at >= ?", time.Now().Add(-reportPendingTTL)).
|
||||
Group("r.question_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "count reports by user")
|
||||
@@ -109,6 +143,11 @@ func (s *reports) QuestionIDsByUser(ctx context.Context, userID uint) (map[uint]
|
||||
}
|
||||
|
||||
func (s *reports) CountByUserSince(ctx context.Context, userID uint, since time.Time) (int64, error) {
|
||||
// 与 QuestionIDsByUser 保持一致的「待处理有效期」:since 更早时以 TTL 为准。
|
||||
if ttlStart := time.Now().Add(-reportPendingTTL); since.Before(ttlStart) {
|
||||
since = ttlStart
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("content_reports AS r").
|
||||
@@ -121,3 +160,77 @@ func (s *reports) CountByUserSince(ctx context.Context, userID uint, since time.
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *reports) MarkAcceptedByQuestion(ctx context.Context, questionID uint) (int64, error) {
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&ContentReport{}).
|
||||
Where("question_id = ?", questionID).
|
||||
Where("status = ?", ReportStatusPending).
|
||||
Update("status", ReportStatusAccepted)
|
||||
if result.Error != nil {
|
||||
return 0, errors.Wrap(result.Error, "mark reports accepted")
|
||||
}
|
||||
return result.RowsAffected, nil
|
||||
}
|
||||
|
||||
func (s *reports) CountByQuestion(ctx context.Context, questionID uint) (int64, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&ContentReport{}).
|
||||
Where("question_id = ?", questionID).
|
||||
Where("status = ?", ReportStatusPending).
|
||||
Where("created_at >= ?", time.Now().Add(-reportPendingTTL)).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, errors.Wrap(err, "count reports by question")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *reports) TopReportedUsers(ctx context.Context, since time.Time, limit int) ([]*ReportedUserStat, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if ttlStart := time.Now().Add(-reportPendingTTL); since.Before(ttlStart) {
|
||||
since = ttlStart
|
||||
}
|
||||
|
||||
var rows []*ReportedUserStat
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("content_reports AS r").
|
||||
Select("q.user_id AS user_id, u.domain AS domain, u.name AS name, COUNT(r.id) AS count").
|
||||
Joins("JOIN questions q ON q.id = r.question_id").
|
||||
Joins("JOIN users u ON u.id = q.user_id").
|
||||
Where("q.deleted_at IS NULL").
|
||||
Where("r.status = ?", ReportStatusPending).
|
||||
Where("r.created_at >= ?", since).
|
||||
Group("q.user_id, u.domain, u.name").
|
||||
Order("count DESC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "top reported users")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (s *reports) TopReporters(ctx context.Context, since time.Time, limit int) ([]*ReporterStat, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if ttlStart := time.Now().Add(-reportPendingTTL); since.Before(ttlStart) {
|
||||
since = ttlStart
|
||||
}
|
||||
|
||||
var rows []*ReporterStat
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&ContentReport{}).
|
||||
Select("reporter_ip, COUNT(*) AS count").
|
||||
Where("status = ?", ReportStatusPending).
|
||||
Where("created_at >= ?", since).
|
||||
Group("reporter_ip").
|
||||
Order("count DESC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "top reporters")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user