topfans/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md

43 KiB
Raw Blame History

举报与反馈系统设计

状态:设计已确认 创建时间2026-06-11 作者Claude 目标:在 TopFans 平台新增内容举报与用户反馈两大工单系统,支撑运营/审核员通过独立 web 后台对违规内容(数字藏品/用户/AI 描述词和用户反馈BUG/咨询/合作/建议)进行审核处理


目录

  1. 背景与目标
  2. 架构概览
  3. 数据模型
  4. 状态机
  5. API 设计
  6. 关键流程
  7. 错误码
  8. 通知机制
  9. 安全与防护
  10. 实施步骤
  11. 附录

1. 背景与目标

1.1 背景

TopFans 是粉丝/明星数字藏品平台。当前平台缺少:

  • 用户对违规内容(藏品/用户/AI 描述词)的举报通道
  • 用户对平台体验问题的反馈通道
  • 运营/审核员对举报和反馈的审核处理后台

1.2 目标

  • 提供客户端uni-app举报与反馈的提交入口
  • 提供运营 web 后台(基于 D:\shanghai\TopFans-activity)的工单审核界面
  • 支持"举报达到阈值自动隐藏 + 管理员审核动作(标记状态)"的闭环
  • 设计可扩展、易维护、易观测

1.3 范围

包含

  • 客户端提交接口uni-app 端)
  • 审核员后台Vue3 + Element Plus
  • 数据模型与迁移
  • 状态机、自动隐藏、动作流水
  • 通知机制

不包含YAGNI

  • 举报人/被举报人间的对话沟通
  • 申诉/上诉流程
  • 举报人积分/奖励体系
  • 复杂的多级管理员(仅一种角色)
  • 工单 SLA 监控告警

2. 架构概览

2.1 整体架构

┌──────────────────────────────────────────────────────────────────┐
│                       客户端 (uni-app)                           │
│  ┌─────────────────────────┐  ┌──────────────────────────────┐  │
│  │ 举报弹窗 (藏品/用户/描述词)│  │ 我的 → 意见反馈              │  │
│  └────────────┬────────────┘  └────────────┬─────────────────┘  │
└───────────────┼──────────────────────────────┼──────────────────┘
                │ REST                          │
                ▼                              ▼
┌──────────────────────────────────────────────────────────────────┐
│   topfans gateway (Go, 已有)                                    │
│   /api/v1/moderation/*  客户端 API                              │
└───────────────────────────┬──────────────────────────────────────┘
                            │ Dubbo RPC
                            ▼
┌──────────────────────────────────────────────────────────────────┐
│   topfans moderationService (Go, 新建微服务)                     │
│   - ReportService / FeedbackService / CategoryService           │
│   - 写 PostgreSQL reports/feedbacks/categories/状态表            │
│   - Redis 计数 (自动隐藏阈值)                                   │
│   - 触发自动隐藏UPDATE reports + UPSERT moderation_target_status│
└────────┬─────────────────────────────────────────┬───────────────┘
         │ 共享 PG 库                              │
         │                                         │
         ▼                                         ▼
┌────────────────────────────┐  ┌──────────────────────────────────┐
│  PostgreSQL (topfans 库)    │  │  Redis (topfans 已有实例)        │
│  - report_categories       │  │  - mod:report:counter:*          │
│  - feedback_categories     │  │  - mod:report:user:*             │
│  - reports                 │  │  - mod:report:lock:*             │
│  - report_evidence         │  └──────────────────────────────────┘
│  - feedbacks               │
│  - feedback_evidence       │
│  - moderation_target_status│  ← 业务服务读路径 JOIN 此表
│  - moderation_actions      │
│  - admin_audit_logs        │
└────────┬───────────────────┘
         │ 共享 PG 库 (直接读写,不调对方接口)
         ▼
┌──────────────────────────────────────────────────────────────────┐
│   TopFans-Activity 后台 (FastAPI + Vue3, 已有项目)                │
│   D:\shanghai\TopFans-activity                                   │
│   - backend/handlers/moderation_admin.py (后台 API)              │
│   - backend/crud/moderation_crud.py                              │
│   - backend/schemas/moderation.py                                │
│   - frontend/src/views/moderation/ (举报/反馈工单 UI)            │
└──────────────────────────────────────────────────────────────────┘

2.2 模块职责

模块 职责
ReportService (Go) 接收举报、查询我的举报、触发自动隐藏
FeedbackService (Go) 接收反馈、查询我的反馈
CategoryService (Go+FastAPI) 维护分类(动态配置),客户端读取 + 后台 CRUD
AutoHideExecutor (Go) Redis 计数 + 触发自动隐藏(写 reports + 状态表)
后台 Moderation Admin (FastAPI) 审核员认领/动作/分类管理,只读写 PostgreSQL不操作 Redis
moderation_target_status 业务服务读路径 JOIN呈现"隐藏/封禁/警告"状态

关键边界

  • TopFans-Activity 后台不调用 moderationService 的任何 RPC / Dubbo / HTTP 接口,不操作 Redis;仅通过共享 PostgreSQL 完成所有读写
  • 唯一的跨服务依赖是:客户端 API 通过 gateway → moderationServiceDubbo RPC这是项目既有的调用模式
  • 业务下架/封禁的"实际生效"完全通过读路径 LEFT JOIN moderation_target_status 实现,不需要跨服务写业务表
  • 如后台需查询"实时计数"等 Redis 状态,由 Go service 异步定时把 Redis 数据回写到 PostgreSQL 一张快照表(moderation_counter_snapshot),后台查快照表即可——这是唯一允许的"Redis → PG"数据流,且单向

2.3 复用现有资产

  • OSS 上传:举报证据图、反馈截图复用 assetService 的 OSS 签名接口(GET /api/v1/assets/oss/signature?type=asset
  • 认证:客户端 JWT 用 topfans userService后台 JWT 用 TopFans-activity 已有的 verify_token
  • 通知中心:处理结果通过 topfans notificationService 模板下发(新增 5 个模板)
  • 数据迁移:遵循 topfans 现有 backend/migrations/00x_*.sql 命名规范
  • 序列同步:手动指定 ID 时必须 SELECT setval('table_id_seq', MAX(id))CLAUDE.md 强制规范)

3. 数据模型

3.1 核心表

report_categories      举报分类(动态配置)
feedback_categories    反馈分类(动态配置)
reports                举报工单主表
report_evidence        举报证据图(一对多)
feedbacks              反馈工单主表
feedback_evidence      反馈截图(一对多)
moderation_actions     审核动作流水
moderation_target_status  共享"对象受管控状态"表(业务服务读路径 JOIN
admin_audit_logs       管理员操作日志

3.2 report_categories 举报分类

CREATE SEQUENCE report_categories_id_seq START WITH 10000;

CREATE TABLE report_categories (
    id BIGSERIAL PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200),
    severity SMALLINT NOT NULL DEFAULT 1,
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL
);
CREATE INDEX idx_report_categories_enabled ON report_categories(enabled, sort_order);

-- 初始数据
INSERT INTO report_categories (code, name, description, severity, sort_order, created_at, updated_at) VALUES
  ('pornographic', '色情低俗', '含裸露、性暗示内容', 5, 1, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('violence',    '暴力血腥',  '含暴力、血腥画面', 5, 2, ...),
  ('infringing',  '侵权盗版',  '侵犯他人著作权/商标权', 4, 3, ...),
  ('false_info',  '虚假信息',  '虚假、欺骗性内容', 3, 4, ...),
  ('political',   '政治敏感',  '涉及政治敏感话题', 5, 5, ...),
  ('ad_spam',     '广告骚扰',  '垃圾广告、骚扰信息', 2, 6, ...),
  ('other',       '其他',      '其他违规情况', 1, 99, ...);

SELECT setval('report_categories_id_seq', (SELECT MAX(id) FROM report_categories));

3.3 feedback_categories 反馈分类

CREATE SEQUENCE feedback_categories_id_seq START WITH 10000;

CREATE TABLE feedback_categories (
    id BIGSERIAL PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200),
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL
);

INSERT INTO feedback_categories (code, name, description, sort_order, created_at, updated_at) VALUES
  ('bug',        'BUG 报告',  '使用中遇到的问题', 1, ...),
  ('consult',    '使用咨询',  '不知道怎么用', 2, ...),
  ('business',   '内容合作',  '合作/商务联系', 3, ...),
  ('suggestion', '功能建议',  '希望增加什么功能', 4, ...);

SELECT setval('feedback_categories_id_seq', (SELECT MAX(id) FROM feedback_categories));

3.4 reports 举报工单

CREATE SEQUENCE reports_id_seq START WITH 10000;

CREATE TABLE reports (
    id BIGSERIAL PRIMARY KEY,
    reporter_id BIGINT NOT NULL,
    star_id BIGINT,
    target_type VARCHAR(30) NOT NULL,    -- asset | user_profile | description_word
    target_id BIGINT NOT NULL,
    target_snapshot JSONB NOT NULL,      -- 提交时的对象快照
    
    category_code VARCHAR(50) NOT NULL,
    description VARCHAR(500),
    is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
    
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
        -- pending / reviewing / auto_hidden / resolved / dismissed
    is_auto_hidden BOOLEAN NOT NULL DEFAULT FALSE,
    
    resolved_action VARCHAR(20),         -- takedown | ban | warn | dismissed
    resolved_by BIGINT,
    resolved_at BIGINT,
    resolution_note VARCHAR(500),
    
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    
    CONSTRAINT fk_reports_category FOREIGN KEY (category_code)
        REFERENCES report_categories(code) ON DELETE RESTRICT
);
CREATE INDEX idx_reports_status_created ON reports(status, created_at DESC);
CREATE INDEX idx_reports_target ON reports(target_type, target_id, status);
CREATE INDEX idx_reports_reporter ON reports(reporter_id, created_at DESC);

-- 防重复:同一举报人对同一对象在未结案前只能有一条
CREATE UNIQUE INDEX uk_reports_reporter_target_pending
    ON reports(reporter_id, target_type, target_id)
    WHERE status IN ('pending', 'reviewing', 'auto_hidden');

3.5 report_evidence 举报证据图

CREATE SEQUENCE report_evidence_id_seq START WITH 10000;

CREATE TABLE report_evidence (
    id BIGSERIAL PRIMARY KEY,
    report_id BIGINT NOT NULL,
    oss_key VARCHAR(255) NOT NULL,
    oss_url VARCHAR(500),
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    CONSTRAINT fk_report_evidence_report
        FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE
);
CREATE INDEX idx_report_evidence_report ON report_evidence(report_id, sort_order);

3.6 feedbacks 反馈工单

CREATE SEQUENCE feedbacks_id_seq START WITH 10000;

CREATE TABLE feedbacks (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    star_id BIGINT,
    category_code VARCHAR(50) NOT NULL,
    title VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    contact VARCHAR(100),
    is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
    
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
        -- pending / reviewing / replied / closed / archived
    
    replied_by BIGINT,
    replied_at BIGINT,
    reply_content TEXT,
    
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    
    CONSTRAINT fk_feedbacks_category FOREIGN KEY (category_code)
        REFERENCES feedback_categories(code) ON DELETE RESTRICT
);
CREATE INDEX idx_feedbacks_status_created ON feedbacks(status, created_at DESC);
CREATE INDEX idx_feedbacks_user ON feedbacks(user_id, created_at DESC);
CREATE INDEX idx_feedbacks_category ON feedbacks(category_code, status);

3.7 feedback_evidence 反馈截图

CREATE SEQUENCE feedback_evidence_id_seq START WITH 10000;

CREATE TABLE feedback_evidence (
    id BIGSERIAL PRIMARY KEY,
    feedback_id BIGINT NOT NULL,
    oss_key VARCHAR(255) NOT NULL,
    oss_url VARCHAR(500),
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    CONSTRAINT fk_feedback_evidence_feedback
        FOREIGN KEY (feedback_id) REFERENCES feedbacks(id) ON DELETE CASCADE
);
CREATE INDEX idx_feedback_evidence_feedback ON feedback_evidence(feedback_id, sort_order);

3.8 moderation_actions 审核动作流水

CREATE SEQUENCE moderation_actions_id_seq START WITH 10000;

CREATE TABLE moderation_actions (
    id BIGSERIAL PRIMARY KEY,
    report_id BIGINT,
    feedback_id BIGINT,
    admin_id BIGINT NOT NULL,
    action_type VARCHAR(30) NOT NULL,   -- takedown | unhide | ban | warn | dismiss | reply | close | archive
    target_type VARCHAR(30),
    target_id BIGINT,
    note VARCHAR(500),
    success BOOLEAN NOT NULL,
    error_message VARCHAR(500),
    created_at BIGINT NOT NULL,
    CONSTRAINT chk_moderation_actions_xor
        CHECK ((report_id IS NOT NULL) OR (feedback_id IS NOT NULL))
);
CREATE INDEX idx_moderation_actions_report ON moderation_actions(report_id, created_at DESC);
CREATE INDEX idx_moderation_actions_feedback ON moderation_actions(feedback_id, created_at DESC);
CREATE INDEX idx_moderation_actions_admin ON moderation_actions(admin_id, created_at DESC);

3.9 moderation_target_status 共享"对象受管控状态"

CREATE SEQUENCE moderation_target_status_id_seq START WITH 10000;

CREATE TABLE moderation_target_status (
    id BIGSERIAL PRIMARY KEY,
    target_type VARCHAR(30) NOT NULL,        -- asset | user | description_word
    target_id BIGINT NOT NULL,
    
    is_hidden BOOLEAN NOT NULL DEFAULT FALSE,   -- 隐藏/下架
    is_banned BOOLEAN NOT NULL DEFAULT FALSE,   -- 封禁(仅 user
    is_warned BOOLEAN NOT NULL DEFAULT FALSE,   -- 已警告(仅 user
    
    reason VARCHAR(200),
    source VARCHAR(30) NOT NULL,                -- auto | admin
    source_report_id BIGINT,
    operator_admin_id BIGINT,
    
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    
    CONSTRAINT uk_moderation_target UNIQUE (target_type, target_id)
);
CREATE INDEX idx_mts_hidden ON moderation_target_status(is_hidden) WHERE is_hidden = TRUE;
CREATE INDEX idx_mts_banned ON moderation_target_status(is_banned) WHERE is_banned = TRUE;

写入方

  • moderationService (Go) — 自动隐藏时 UPSERT
  • TopFans-Activity 后台 (FastAPI) — 审核员手动下架/封禁/警告时 UPSERT

读取方

  • assetService 藏品查询/详情/列表:LEFT JOIN moderation_target_status ON target_type='asset' AND target_id=assets.id WHERE COALESCE(is_hidden, false) = false
  • userService 登录/操作校验:LEFT JOIN ... WHERE COALESCE(is_banned, false) = false
  • AI 描述词服务查询时同样 JOIN

目标对象 target_type 定义与位置

target_type 对应表 target_id 含义
asset assets assets.id
user_profile users users.id用户主页违规、头像、昵称
description_word ai_descriptions ai_descriptions.idAI 描述词,存在于 aiChatService 库)

description_word 类型的举报目标存放在 aiChatService 服务的数据库中(与 topfans 主库可不同库)。提交举报时 moderationServiceaiChatService.GetDescriptionRPC 验证存在性;状态标志位 moderation_target_status 仍写在 topfans 主库。读路径由 aiChatService 反向 join 或通过 Redis 缓存检查。

孤儿记录清理target 被删除时)

  • moderation_target_status 与业务表assets/users/ai_descriptions跨服务无外键约束应用层负责清理
  • assetService / userService / aiChatService 的删除逻辑中加清理钩子:
    // 伪代码
    defer func() {
        moderationService.DeleteTargetStatus(ctx, targetType, targetID)
    }()
    
  • 定期清理脚本(每日 1 次):扫描 moderation_target_statustarget_type='asset'target_idassets 表不存在的记录,软标记 is_hidden=false 并写 admin_audit_logs
  • 详情/历史记录保留在 moderation_actions 流水表中供审计追溯

3.10 admin_audit_logs 管理员操作日志

CREATE SEQUENCE admin_audit_logs_id_seq START WITH 10000;

CREATE TABLE admin_audit_logs (
    id BIGSERIAL PRIMARY KEY,
    admin_id BIGINT NOT NULL,
    action VARCHAR(50) NOT NULL,
    resource_type VARCHAR(30),
    resource_id BIGINT,
    ip VARCHAR(50),
    user_agent VARCHAR(500),
    extra JSONB,
    created_at BIGINT NOT NULL
);
CREATE INDEX idx_admin_audit_logs_admin ON admin_audit_logs(admin_id, created_at DESC);
CREATE INDEX idx_admin_audit_logs_resource ON admin_audit_logs(resource_type, resource_id);

3.11 Redis 数据结构

Key 模式 类型 用途 TTL
mod:report:counter:{target_type}:{target_id} String (INCR) 自动隐藏阈值计数(独立用户数) 7 天
mod:report:user:{target_type}:{target_id}:{user_id} String 防重复(仅独立用户才计数) 24h
mod:report:lock:{target_type}:{target_id} String 防并发触发自动隐藏 5s
mod:auto_hide_threshold String (缓存) 自动隐藏阈值 N默认 5 永久

3.12 序列同步规范

遵循 CLAUDE.md 规范:所有手动指定 ID 的 INSERT 末尾必须 SELECT setval('table_id_seq', (SELECT MAX(id) FROM table))

新表起始值:CREATE SEQUENCE table_id_seq START WITH 10000; 预留测试数据空间。


4. 状态机

4.1 举报状态机

                    ┌──────────────────────────────────┐
                    │           pending                │
                    │         (待处理)                  │
                    └─────┬─────────────┬──────────────┘
              管理员认领   │             │  阈值 N 触发
                          ▼             ▼
              ┌──────────────────┐  ┌──────────────────┐
              │    reviewing     │  │   auto_hidden    │
              │    (审核中)       │  │  (已自动隐藏)     │
              └────┬────┬────┬───┘  └────┬─────────────┘
        下架/封禁  │    │驳回│ 解除隐藏  │ 管理员认领
        /警告     │    │    │            ▼
                   ▼    │    ▼    ┌──────────────────┐
              ┌──────┐  │  ┌──────────────┐         │
              │resolved│ │  │  dismissed   │         │
              │(已处理)│ │  │  (已驳回)    │         │
              └──────┘  │  └──────────────┘         │
                         │                          │
                         └────→ resolved / dismissed
状态 含义 进入 退出
pending 新提交待处理 创建 认领 OR 自动隐藏
reviewing 管理员已认领 认领 做出处理动作
auto_hidden 触发自动隐藏 阈值达成 管理员认领并做最终裁决
resolved 已处理 takedown/ban/warn 终态
dismissed 已驳回 dismiss 终态

状态机合法迁移矩阵

From To 触发者 触发条件 副作用
(init) pending 客户端/系统 提交举报 写 reports
pending reviewing 后台 管理员点击"认领" 抢锁 UPDATE
pending auto_hidden 系统 Redis counter ≥ threshold 写 reports + UPSERT mts(is_hidden=true)
auto_hidden reviewing 后台 管理员点击"认领" 抢锁 UPDATE被举报对象保持隐藏
reviewing resolved 后台 takedown/ban/warn 写 mts + 通知被举报方
reviewing dismissed 后台 dismiss 且非 auto_hidden 通知举报人
reviewing dismissed 后台 dismiss 且曾 auto_hidden 解除隐藏 UPSERT mts(is_hidden=false) + 通知举报人
resolved (终态) - - -
dismissed (终态) - - -

并发认领防护

UPDATE reports
SET status = 'reviewing', updated_at = ?
WHERE id = ? AND status IN ('pending', 'auto_hidden')

用 affected rows = 1 判断是否抢锁成功。

自动隐藏幂等性

  • 自动隐藏触发时,上层 SQL 加幂等保护:
    UPDATE reports
    SET status = 'auto_hidden', is_auto_hidden = true
    WHERE target_type = ? AND target_id = ? AND status = 'pending';
    
  • 仅将仍是 pending 的工单升级为 auto_hidden,避免重复 INCR 后覆盖 reviewing/resolved 等状态
  • moderation_target_statusUPSERT ... ON CONFLICT (target_type, target_id) DO UPDATE,天然幂等

4.2 反馈状态机

         ┌────────────┐
         │  pending   │
         └─────┬──────┘
               │ 认领
               ▼
       ┌────────────────┐
       │   reviewing    │
       └─┬──────┬────┬──┘
         │      │    │
   已回复 │      │关闭│ 归档
         ▼      ▼    ▼
   ┌────────┐ ┌────────┐ ┌────────┐
   │replied │ │closed  │ │archived│
   └────────┘ └────────┘ └────────┘
状态 进入 备注
pending 创建 初始
reviewing 认领
replied 填写 reply_content 终态,触发用户通知
closed 点击关闭 终态,重复/无效
archived 归档 终态,已处理保留记录

5. API 设计

5.1 客户端 APIuni-app 端调用,路径前缀 /api/v1/moderation

Method & Path 说明
GET /api/v1/moderation/report-categories 获取启用中的举报分类
POST /api/v1/moderation/reports 提交举报
GET /api/v1/moderation/reports?status=&page=&page_size= 我的举报记录
GET /api/v1/moderation/reports/{id} 查看举报进度与结果
GET /api/v1/moderation/feedback-categories 获取反馈分类
POST /api/v1/moderation/feedbacks 提交反馈
GET /api/v1/moderation/feedbacks?status=&page=&page_size= 我的反馈记录
GET /api/v1/moderation/feedbacks/{id} 查看反馈详情(含回复)

提交举报请求体

{
  "target_type": "asset",
  "target_id": 12345,
  "category_code": "pornographic",
  "description": "图片含有裸露内容",
  "is_anonymous": true,
  "evidence_keys": ["report/2026/06/11/uuid1.png"]
}

响应

{
  "code": 200,
  "data": {
    "report_id": 67890,
    "status": "pending",
    "auto_hidden": true,
    "target_hidden": true,
    "created_at": 1718123456789
  }
}

错误响应

{ "code": 50004, "message": "您已举报过该对象,请等待处理" }

证据上传:客户端先调用 GET /api/v1/assets/oss/signature?type=asset 直传 OSS再把返回的 key 提交给本接口。

5.2 后台 APIVue3 后台调用,路径前缀 /api/admin/moderation

Method & Path 说明
GET /api/admin/moderation/reports?status=&category=&target_type=&keyword=&page=&page_size= 举报工单列表
GET /api/admin/moderation/reports/{id} 举报详情
POST /api/admin/moderation/reports/{id}/claim 认领
POST /api/admin/moderation/reports/{id}/release 释放认领
POST /api/admin/moderation/reports/{id}/actions 执行审核动作
GET /api/admin/moderation/feedbacks?status=&category=&keyword=&page=&page_size= 反馈列表
GET /api/admin/moderation/feedbacks/{id} 反馈详情
POST /api/admin/moderation/feedbacks/{id}/claim 认领反馈
POST /api/admin/moderation/feedbacks/{id}/reply 回复(→ replied
POST /api/admin/moderation/feedbacks/{id}/close 关闭
POST /api/admin/moderation/feedbacks/{id}/archive 归档
GET/POST/PUT/DELETE /api/admin/moderation/report-categories 举报分类 CRUD
GET/POST/PUT/DELETE /api/admin/moderation/feedback-categories 反馈分类 CRUD
GET /api/admin/moderation/stats 看板:今日待处理/平均处理时长/分类分布

执行审核动作请求

{
  "action": "takedown",
  "note": "图片含裸露,已下架",
  "send_notification": true
}

6. 关键流程

6.1 用户提交举报(含自动隐藏)

用户点击"举报"
   │
   ▼
uni-app → POST /api/v1/moderation/reports
   │
   ▼
gateway → moderationService.SubmitReport()
   │
   ├─ 1. 验证target_type 合法、分类启用、description ≤ 500 字、evidence ≤ 5 张
   ├─ 2. 验证目标对象存在assetService.GetAssetRPC / userService.GetUserRPC
   ├─ 3. 防重复检查:同 (reporter, target, type) 在 pending/reviewing/auto_hidden 已有?
   │       有 → 返回 50004 错误
   ├─ 4. 写入 reports 表status='pending'snapshot 拿当前对象信息)
   ├─ 5. 写 report_evidenceOSS keys
   ├─ 6. Redis Lua 计数(独立用户才 INCR
   │       首次计数 → 检查 counter >= threshold (默认 5)
   │       └─ 达到 → 触发自动隐藏:
   │             ① UPDATE reports WHERE target=... AND status='pending' 
   │                SET status='auto_hidden', is_auto_hidden=true
   │             ② INSERT INTO moderation_target_status
   │                (asset, is_hidden=true, source='auto', source_report_id=...)
   │             ③ 发通知给被举报方:"您的内容被多人举报已自动隐藏"
   ├─ 7. 返回 {report_id, status, auto_hidden, target_hidden, created_at}

6.2 后台审核流程

审核员登录后台 → 列表页 (筛选 status=pending OR auto_hidden)
   │
   ▼
详情 → GET /api/admin/moderation/reports/{id}
   │
   ▼
认领 → POST /api/admin/moderation/reports/{id}/claim
   │   UPDATE reports SET status='reviewing', updated_at=now
   │   WHERE id=? AND status IN ('pending','auto_hidden')
   │   affected=1 才返回成功
   │
   ▼
查看证据、目标快照、举报人、流水
   │
   ▼
执行动作 → POST /api/admin/moderation/reports/{id}/actions
   │
   ├─ takedown藏品/描述词/用户内容):
   │     UPSERT moderation_target_status (is_hidden=true, source='admin', ...)
   │     UPDATE reports SET status='resolved', resolved_action='takedown', ...
   │     发通知给被举报方
   │
   ├─ ban用户:
   │     UPSERT moderation_target_status (is_banned=true, source='admin', ...)
   │     UPDATE reports SET status='resolved', resolved_action='ban', ...
   │     发通知给被封禁用户
   │
   ├─ warn用户:
   │     UPSERT moderation_target_status (is_warned=true, source='admin', ...)
   │     UPDATE reports SET status='resolved', resolved_action='warn', ...
   │     发站内信给用户
   │
   ├─ dismiss驳回:
   │     如果当前 auto_hiddenUPSERT is_hidden=false解除隐藏
   │     UPDATE reports SET status='dismissed', resolved_action='dismissed', ...
   │     发通知给举报人:"您的举报已驳回"
   │
   └─ 写 moderation_actions 流水 + admin_audit_logs

6.3 反馈流程

用户提交反馈 → POST /api/v1/moderation/feedbacks
   │  moderationService 写 feedbacks (status='pending')
   ▼
后台列表 → GET /api/admin/moderation/feedbacks
   ▼
认领 → POST /api/admin/moderation/feedbacks/{id}/claim
   ▼
回复 → POST /api/admin/moderation/feedbacks/{id}/reply
   │  body: { content: "..." }
   │  UPDATE feedbacks SET status='replied', reply_content=?, replied_by=?, replied_at=now
   │  发通知给反馈人

6.4 自动隐藏 Lua 脚本

-- KEYS[1]=counter, KEYS[2]=user_marker, KEYS[3]=lock
-- ARGV[1]=threshold, ARGV[2]=ttl_counter, ARGV[3]=ttl_marker
local first = redis.call('SET', KEYS[2], '1', 'NX', 'EX', ARGV[3])
if not first then
  return {0, tonumber(redis.call('GET', KEYS[1]) or '0')}
end
local n = redis.call('INCR', KEYS[1])
if n == 1 then
  redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if n >= tonumber(ARGV[1]) then
  return {1, n}
end
return {0, n}

返回 {triggered, count}triggered=1 时上层触发自动隐藏。


7. 错误码

含义 HTTP
50001 举报分类不存在或已停用 400
50002 反馈分类不存在或已停用 400
50003 目标对象不存在 404
50004 同一对象已举报过(未结案) 429
50005 描述过长(>500 字) 400
50006 证据图超限(>5 张) 400
50007 工单不存在 404
50008 工单已被他人认领 409
50009 工单已结案 400
50010 管理员权限不足 403
50011 不能举报自己 400
50012 提交过于频繁(限流) 429
50013 反馈每日提交超限 429
50014 状态机非法迁移(如对已结案工单再次操作) 409
50015 管理员未登录 / Token 失效 401

8. 通知机制

复用 notificationService 模块,注册 5 个模板:

事件 接收方 模板名 渠道
举报状态变更为 resolved/dismissed 举报人 report_resolved 站内信 + 通知中心
举报成立 + takedown 被举报方 report_takedown_notice 站内信
举报成立 + ban 被举报用户 report_ban_notice 站内信 + 短信
举报成立 + warn 被举报用户 report_warn_notice 站内信
反馈被回复 反馈人 feedback_replied 站内信 + 通知中心

模板字段:{reporter_nickname, target_type, target_id, action, reason, related_url}


9. 安全与防护

9.1 防护机制

机制 实现
防重复举报 DB 局部唯一索引 uk_reports_reporter_target_pending(结案后允许再次举报)
防 Redis 计数重复 Lua 脚本用 user_marker SETNX 保证独立用户才 INCR
防自动隐藏并发 Redis 短锁 mod:report:lock:* 5s + DB 幂等 SQL仅 pending → auto_hidden
证据图校验 OSS 签名接口限定目录 report/feedback/ 前缀
描述长度 服务端校验 ≤ 500 字DB 字段 VARCHAR(500)
证据图数量 服务端校验 ≤ 5 张
匿名保护 is_anonymous=true 时:reporter_id 仍记录但 API 响应中不返回给被举报方;后台仅对超级管理员可见
自举报拦截 提交举报时校验 reporter_id != owner_id(target),命中则返回 50011
全局限流(举报) 每用户每天 ≤ 30 次举报Redis 计数 mod:rl:report:user:{user_id}:{yyyymmdd}TTL 36h超限返回 50012
全局限流(反馈) 每用户每天 ≤ 5 条反馈Redis 计数 mod:rl:feedback:user:{user_id}:{yyyymmdd}TTL 36h超限返回 50013

9.2 数据隔离

  • 客户端 JWT 仅可看自己的举报/反馈(reporter_id = current_user_id
  • 后台 JWT 走 TopFans-activity 现有 verify_token,所有接口都需 Depends(verify_token)
  • 后续如要分级权限(仅超管可恢复隐藏等),在 JWT payload 加 role 字段

9.3 数据保留

  • reports / feedbacks 永久保留(合规需要)
  • moderation_actions 永久保留(审计)
  • moderation_target_status UPSERT 保留最新状态,历史可查 moderation_actions

10. 实施步骤

阶段 1数据库topfans 库)

  • 1.1 写迁移 D:\shanghai\topfans\backend\migrations\2026_06_11_010_moderation_tables.sql
  • 1.2 包含 8 张表 + 索引 + 默认分类种子数据
  • 1.3 末尾必须 SELECT setval('xxx_id_seq', (SELECT MAX(id) FROM xxx))CLAUDE.md 强制)
  • 1.4 在 backend/scripts/ 加 Go 脚本生成种子数据(带序列重置)

阶段 2Go moderationServicetopfans

  • 2.1 D:\shanghai\topfans\backend\services\moderationService\ 新建目录
    • main.go / config/ / repository/ / service/ / provider/ / client/ / go.mod / start.sh
    • 复用 socialService 类似的分层
  • 2.2 D:\shanghai\topfans\backend\proto\moderation\moderation.proto
    • SubmitReportRequest/Response
    • ListMyReportsRequest/Response
    • GetReportRequest/Response
    • GetReportCategoriesRequest/Response
    • SubmitFeedbackRequest/Response
    • ListMyFeedbacksRequest/Response
    • GetFeedbackRequest/Response
    • GetFeedbackCategoriesRequest/Response
  • 2.3 D:\shanghai\topfans\backend\pkg\proto\moderation\ 编译生成 .pb.go
  • 2.4 D:\shanghai\topfans\backend\pkg\models\moderation.go ORM 模型
  • 2.5 D:\shanghai\topfans\backend\services\moderationService\repository\moderation_repository.go 数据访问
  • 2.6 D:\shanghai\topfans\backend\services\moderationService\service\:
    • report_service.go
    • feedback_service.go
    • category_service.go
    • auto_hide_service.go(含 Redis Lua 脚本)
  • 2.7 D:\shanghai\topfans\backend\services\moderationService\client\:
    • asset_client.go(验证目标存在)
    • user_client.go(验证用户/封禁查询)
  • 2.8 D:\shanghai\topfans\backend\services\moderationService\provider\social_provider.go Dubbo 注册
  • 2.9 D:\shanghai\topfans\backend\go.work 加入新服务
  • 2.10 Gateway 层:
    • D:\shanghai\topfans\backend\gateway\controller\moderation_controller.go
    • D:\shanghai\topfans\backend\gateway\dto\moderation_dto.go
    • D:\shanghai\topfans\backend\gateway\dto\moderation_converter.go
    • D:\shanghai\topfans\backend\gateway\router\router.go 注册 /api/v1/moderation/*

Gateway 路由配置清单(在 router.go 新增):

// 举报分类
v1.GET("/moderation/report-categories", moderationController.GetReportCategories)
v1.GET("/moderation/feedback-categories", moderationController.GetFeedbackCategories)

// 举报
v1.POST("/moderation/reports", moderationController.SubmitReport)
v1.GET("/moderation/reports", moderationController.ListMyReports)
v1.GET("/moderation/reports/:id", moderationController.GetReportDetail)

// 反馈
v1.POST("/moderation/feedbacks", moderationController.SubmitFeedback)
v1.GET("/moderation/feedbacks", moderationController.ListMyFeedbacks)
v1.GET("/moderation/feedbacks/:id", moderationController.GetFeedbackDetail)

Dubbo 服务注册moderationService 启动时通过 provider/social_provider.go(参考 socialService 模式)注册到 ZooKeeper/Nacosgateway 端通过 client/moderation_client.go 引用。无需修改其他服务的注册配置。

阶段 3TopFans-Activity 后台 APIFastAPI

  • 3.1 D:\shanghai\TopFans-activity\backend\models\moderation.py SQLAlchemy ORM与 PG 表结构对应)
  • 3.2 D:\shanghai\TopFans-activity\backend\schemas\moderation.py Pydantic 模型
  • 3.3 D:\shanghai\TopFans-activity\backend\crud\moderation_crud.py 数据库操作
    • list_reports(filters, page) / get_report_detail(id) / claim_report(id) / execute_action(id, action, ...) / list_categories() / upsert_category() / ...
  • 3.4 D:\shanghai\TopFans-activity\backend\handlers\moderation_admin.py 路由层
    • 复用现有 verify_token 中间件
  • 3.5 D:\shanghai\TopFans-activity\backend\router\__init__.py 注册路由
  • 3.6 数据库迁移(如 ORM 与 PG 表不一致时):
    • D:\shanghai\TopFans-activity\backend\migrations\010_moderation_init.sql(如果用 SQLAlchemy create_all 可省略)

阶段 4后台前端 (Vue3)

  • 4.1 D:\shanghai\TopFans-activity\frontend\src\api\moderation.js axios 封装
    • getReports() / getReportDetail() / claimReport() / executeAction() / ...
    • getReportCategories() / createCategory() / updateCategory() / deleteCategory() / ...
  • 4.2 D:\shanghai\TopFans-activity\frontend\src\views\moderation\
    • ReportList.vue 举报工单列表(筛选:状态/分类/目标类型/关键词)
    • ReportDetail.vue 举报详情(证据图、目标快照、举报人、流水)+ 审核动作按钮
    • FeedbackList.vue 反馈列表
    • FeedbackDetail.vue 反馈详情 + 回复表单
    • ReportCategoryConfig.vue 举报分类 CRUD
    • FeedbackCategoryConfig.vue 反馈分类 CRUD
    • Dashboard.vue 审核看板(可选)
  • 4.3 D:\shanghai\TopFans-activity\frontend\src\router\index.js 加路由
    • /moderation/reports
    • /moderation/reports/:id
    • /moderation/feedbacks
    • /moderation/feedbacks/:id
    • /moderation/categories/report
    • /moderation/categories/feedback
  • 4.4 菜单注册(按 Layout.vue 现成结构)
  • 4.5 Element Plus 组件复用el-table / el-form / el-dialog / el-image / el-tag / el-pagination

阶段 5客户端 uni-app

  • 5.1 D:\shanghai\topfans\frontend\utils\api.js 加:
    • getReportCategoriesApi() / submitReportApi(payload) / getMyReportsApi() / getMyReportDetailApi(id)
    • getFeedbackCategoriesApi() / submitFeedbackApi(payload) / getMyFeedbacksApi() / getMyFeedbackDetailApi(id)
  • 5.2 D:\shanghai\topfans\frontend\components\ReportModal.vue 通用举报弹窗
    • props: targetType, targetId, targetName
    • 表单:分类单选 + 描述 textarea + 证据图上传(最多 5 张)+ 匿名开关
    • submitReportApi,成功后提示"已提交"+ 自动隐藏提示
  • 5.3 在以下位置接入 ReportModal
    • 藏品详情页 (NftDetailModal.vue 等) 长按或"..."菜单
    • 用户主页(粉丝身份下)"..."菜单
    • 描述词展示组件(如果有)的"..."菜单
  • 5.4 D:\shanghai\topfans\frontend\pages\profile\feedback.vue 意见反馈页
    • 复用 ReportModal 的 evidence 上传组件
    • 表单:分类下拉 + 标题 + 内容 + 联系方式(可选) + 截图
  • 5.5 D:\shanghai\topfans\frontend\pages\profile\myReports.vue 我的举报列表
  • 5.6 D:\shanghai\topfans\frontend\pages\profile\myFeedbacks.vue 我的反馈列表
  • 5.7 页面间跳转:从"我的" → 各项入口

阶段 6业务服务读路径加 JOIN按需展开

  • 6.1 D:\shanghai\topfans\backend\services\assetService\repository\ 藏品查询
    • 详情/列表 SQLLEFT JOIN moderation_target_status ON target_type='asset' AND target_id=assets.id WHERE COALESCE(is_hidden, false) = false
  • 6.2 D:\shanghai\topfans\backend\services\userService\repository\ 用户校验
    • 登录/操作校验:LEFT JOIN ... ON target_type='user' AND target_id=users.id WHERE COALESCE(is_banned, false) = false
  • 6.3 描述词相关服务(aiChatService 或新)查询时同样 JOIN
  • 6.4 性能优化(如果热路径压力变大):
    • Redis 缓存 mod:status:cache:{target_type}:{target_id} TTL 60s
    • 命中即跳过 JOIN

阶段 7通知与测试

  • 7.1 通知模板注册到 notificationService5 个模板)
  • 7.2 Go 单元测试
    • report_service_test.go 防重复、自动隐藏触发
    • feedback_service_test.go 状态机
    • auto_hide_service_test.go Lua 脚本逻辑
  • 7.3 FastAPI 单元测试
    • test_moderation_admin.py 列表/详情/认领/动作
    • 鉴权测试(无 token 401
  • 7.4 集成测试
    • 端到端:提交举报 → 后台审核 → 状态变更 → 用户收到通知
    • 自动隐藏5 个独立用户举报 → 自动隐藏 + 状态表写入
  • 7.5 前端联调
    • uni-app → gateway → Go service
    • Vue3 后台 → FastAPI → PG

11. 附录

11.1 配置文件

D:\shanghai\topfans\backend\services\moderationService\config\moderation_config.go

type ModerationConfig struct {
    AutoHideThreshold int           // 默认 5
    CounterTTL        time.Duration // 7 天
    UserMarkerTTL     time.Duration // 24h
    LockTTL           time.Duration // 5s
    MaxDescriptionLen int           // 500
    MaxEvidenceCount  int           // 5
}

11.2 跨服务调用清单

调用方 被调服务 触发时机 失败处理
moderationService assetService.GetAssetRPC 提交举报验证目标 返回 50003
moderationService userService.GetUserRPC 提交举报验证用户 返回 50003
moderationService notificationService 触发自动隐藏后通知 仅日志,不影响主流程
TopFans-Activity (无 — 仅读写 DB) - -
assetService (无 — 仅读 DB) - -
userService (无 — 仅读 DB) - -

11.3 监控指标(建议)

  • moderation.report.submitted.count (counter, tags: target_type, category_code)
  • moderation.report.auto_hidden.count (counter)
  • moderation.report.resolve.duration (histogram, 从 created_at 到 resolved_at)
  • moderation.feedback.reply.duration (histogram)
  • moderation.actions.failed.count (counter, tags: action_type, error)

11.4 变更历史

版本 日期 作者 变更内容
v1.0 2026-06-11 Claude 初始设计

📝 设计方案已确认,可以开始实现!