This commit is contained in:
tamakyi
2026-10-19 01:58:00 +08:00
parent bd613a6f5a
commit 4592b6ef24
14 changed files with 608 additions and 244 deletions
+14 -2
View File
@@ -114,7 +114,19 @@ func runHidePublic(ctx *cli.Context) error {
}
}
result := build().Update("is_private", true)
// ⚠️ 必须先快照目标 ID 再更新:build() 的条件里有 is_private = false,
// 一旦执行了 Update,再拿 build() 当子查询就恒为空集,举报闭环会静默失效
// (实测:隐藏成功但 content_reports 仍是 pending)。
var targetIDs []uint
if err := build().Pluck("id", &targetIDs).Error; err != nil {
return errors.Wrap(err, "collect target question ids")
}
if len(targetIDs) == 0 {
fmt.Println("没有需要隐藏的内容")
return nil
}
result := database.Model(&db.Question{}).Where("id IN ?", targetIDs).Update("is_private", true)
if result.Error != nil {
return errors.Wrap(result.Error, "hide public questions")
}
@@ -129,7 +141,7 @@ func runHidePublic(ctx *cli.Context) error {
// 否则这些举报会继续计入告警阈值(详见 internal/db/report.go 的说明)。
accepted := database.WithContext(ctx.Context).
Model(&db.ContentReport{}).
Where("question_id IN (?)", build().Select("id")).
Where("question_id IN ?", targetIDs).
Where("status = ?", db.ReportStatusPending).
Update("status", db.ReportStatusAccepted)
if accepted.Error != nil {
+39 -19
View File
@@ -141,8 +141,13 @@ func runRestrict(ctx *cli.Context) error {
fmt.Printf("箱主: %s (@%s) id=%d\n", user.Name, user.Domain, user.ID)
if activeLevel != "" {
fmt.Printf("当前处置: %s(至 %s,原因:%s)→ 本次将替换该记录\n",
activeLevel, active.ExpiresAt.Format("2006-01-02 15:04"), active.Reason)
// active 理论上不会为 nil(有级别就有记录),但脏数据/并发下宁可少打一行也不要 panic。
until, reason := "-", "-"
if active != nil {
until = active.ExpiresAt.Format("2006-01-02 15:04")
reason = active.Reason
}
fmt.Printf("当前处置: %s(至 %s,原因:%s)→ 本次将替换该记录\n", activeLevel, until, reason)
} else {
fmt.Println("当前处置: 无")
}
@@ -208,10 +213,11 @@ func runUnrestrict(ctx *cli.Context) error {
// reportedQuestionRow 证据包里的一行:被举报的提问及其举报数。
type reportedQuestionRow struct {
ID uint
Content string
CreatedAt time.Time
ReportCount int64
ID uint
Content string
CreatedAt time.Time
ReportCount int64
PendingCount int64 // 其中仍为 pending 的条数(已处理的举报仍保留在证据里)
}
// pendingReportRow 全站待处理举报的一行(按箱主聚合展示)。
@@ -261,8 +267,12 @@ func printUserReport(database *gorm.DB, ctx *cli.Context) error {
if level == "" {
fmt.Println("当前处置: 无")
} else {
fmt.Printf("当前处置: %s(至 %s,原因:%s)\n",
level, active.ExpiresAt.Format("2006-01-02 15:04"), active.Reason)
until, reason := "-", "-"
if active != nil {
until = active.ExpiresAt.Format("2006-01-02 15:04")
reason = active.Reason
}
fmt.Printf("当前处置: %s(至 %s,原因:%s)\n", level, until, reason)
}
// 公开回答总数:用于计算「被举报内容占其公开产出」的比例,
@@ -278,19 +288,21 @@ func printUserReport(database *gorm.DB, ctx *cli.Context) error {
var rows []*reportedQuestionRow
if err := database.WithContext(ctx.Context).
Raw(`SELECT q.id AS id, q.content AS content, q.created_at AS created_at,
COUNT(r.id) AS report_count
COUNT(r.id) AS report_count,
SUM(CASE WHEN r.status = ? THEN 1 ELSE 0 END) AS pending_count
FROM questions q
JOIN content_reports r ON r.question_id = q.id
WHERE q.user_id = ? AND r.created_at >= ?
WHERE q.user_id = ? AND q.deleted_at IS NULL AND r.created_at >= ?
GROUP BY q.id, q.content, q.created_at
ORDER BY report_count DESC, q.created_at DESC
LIMIT ?`, user.ID, since, ctx.Int("limit")).Scan(&rows).Error; err != nil {
LIMIT ?`, db.ReportStatusPending, user.ID, since, ctx.Int("limit")).Scan(&rows).Error; err != nil {
return errors.Wrap(err, "query reported questions")
}
var totalReports int64
var totalReports, totalPending int64
for _, row := range rows {
totalReports += row.ReportCount
totalPending += row.PendingCount
}
// 比例的分母是「公开回答总数」,分子必须是「被举报的条目数」——
// ⚠️ 不能拿举报次数当分子:同一条内容可被多人举报,次数可以超过条目数,
@@ -300,16 +312,24 @@ func printUserReport(database *gorm.DB, ctx *cli.Context) error {
ratio = float64(len(rows)) / float64(publicAnswered) * 100
}
fmt.Printf("近 %d 天: 被举报 %d 条 / 公开回答 %d 条(%.1f%%),累计 %d 次举报\n",
days, len(rows), publicAnswered, ratio, totalReports)
// 分母是「当前累计公开回答」,不是窗口内的产出 —— 文案必须写清「累计」,
// 否则读者会以为分子分母同窗口(实测箱主被 L2 收窄后分母骤降,比例会超过 100%)。
fmt.Printf("近 %d 天: 被举报 %d 条 / 公开回答 %d 条(累计,%.1f%%);举报 %d 次,其中待处理 %d 次\n",
days, len(rows), publicAnswered, ratio, totalReports, totalPending)
if len(rows) == 0 {
fmt.Println("没有举报记录")
} else {
fmt.Println("被举报条目(按举报数降序):")
for _, row := range rows {
fmt.Printf(" 举报 %-3d #%-6d %s %s\n",
// 明确区分「总举报」与「仍待处理」:箱主处理过内容后举报会自动变 accepted,
// 只报总数会让人误以为还有 3 条挂着。
state := "已处理"
if row.PendingCount > 0 {
state = fmt.Sprintf("待处理 %d", row.PendingCount)
}
fmt.Printf(" 举报 %-3d #%-6d %s %s(%s)\n",
row.ReportCount, row.ID, row.CreatedAt.Format("2006-01-02 15:04"),
truncateForCLI(row.Content, 48))
truncateForCLI(row.Content, 48), state)
}
}
@@ -349,9 +369,9 @@ func printPendingReports(ctx context.Context, database *gorm.DB, limit int) erro
FROM content_reports r
JOIN questions q ON q.id = r.question_id
JOIN users u ON u.id = q.user_id
WHERE r.status = ?
WHERE r.status = ? AND q.deleted_at IS NULL
GROUP BY u.domain, q.id, q.content
ORDER BY report_count DESC, last_at DESC
ORDER BY u.domain, report_count DESC, last_at DESC
LIMIT ?`, db.ReportStatusPending, limit).Scan(&rows).Error; err != nil {
return errors.Wrap(err, "query pending reports")
}
@@ -395,7 +415,7 @@ func printReportStats(ctx context.Context, database *gorm.DB, days int) error {
return errors.Wrap(err, "query top reporters")
}
fmt.Printf("统计窗口:近 %d 天(仅待处理举报)\n", days)
fmt.Printf("统计窗口:近 %d 天(仅待处理举报;待处理最长回溯 %d 天)\n", days, 30)
fmt.Println(strings.Repeat("-", 60))
fmt.Println("被举报最多的箱主:")