diff --git a/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md b/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md new file mode 100644 index 0000000..129b137 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md @@ -0,0 +1,883 @@ +# 举报与反馈系统设计 + +> **状态**:设计已确认 ✅ +> **创建时间**:2026-06-11 +> **作者**:Claude +> **目标**:在 TopFans 平台新增内容举报与用户反馈两大工单系统,支撑运营/审核员通过独立 web 后台对违规内容(数字藏品/用户/AI 描述词)和用户反馈(BUG/咨询/合作/建议)进行审核处理 + +--- + +## 目录 + +1. [背景与目标](#1-背景与目标) +2. [架构概览](#2-架构概览) +3. [数据模型](#3-数据模型) +4. [状态机](#4-状态机) +5. [API 设计](#5-api-设计) +6. [关键流程](#6-关键流程) +7. [错误码](#7-错误码) +8. [通知机制](#8-通知机制) +9. [安全与防护](#9-安全与防护) +10. [实施步骤](#10-实施步骤) +11. [附录](#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) | 审核员认领/动作/分类管理 | +| `moderation_target_status` 表 | 业务服务读路径 JOIN,呈现"隐藏/封禁/警告"状态 | + +### 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` 举报分类 + +```sql +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` 反馈分类 + +```sql +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` 举报工单 + +```sql +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` 举报证据图 + +```sql +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` 反馈工单 + +```sql +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` 反馈截图 + +```sql +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` 审核动作流水 + +```sql +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` 共享"对象受管控状态" + +```sql +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 + +### 3.10 `admin_audit_logs` 管理员操作日志 + +```sql +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 | 终态 | + +**并发认领防护**: +```sql +UPDATE reports +SET status = 'reviewing', updated_at = ? +WHERE id = ? AND status IN ('pending', 'auto_hidden') +``` +用 affected rows = 1 判断是否抢锁成功。 + +### 4.2 反馈状态机 + +``` + ┌────────────┐ + │ pending │ + └─────┬──────┘ + │ 认领 + ▼ + ┌────────────────┐ + │ reviewing │ + └─┬──────┬────┬──┘ + │ │ │ + 已回复 │ │关闭│ 归档 + ▼ ▼ ▼ + ┌────────┐ ┌────────┐ ┌────────┐ + │replied │ │closed │ │archived│ + └────────┘ └────────┘ └────────┘ +``` + +| 状态 | 进入 | 备注 | +|------|------|------| +| `pending` | 创建 | 初始 | +| `reviewing` | 认领 | | +| `replied` | 填写 `reply_content` | 终态,触发用户通知 | +| `closed` | 点击关闭 | 终态,重复/无效 | +| `archived` | 归档 | 终态,已处理保留记录 | + +--- + +## 5. API 设计 + +### 5.1 客户端 API(uni-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}` | 查看反馈详情(含回复)| + +**提交举报请求体**: +```json +{ + "target_type": "asset", + "target_id": 12345, + "category_code": "pornographic", + "description": "图片含有裸露内容", + "is_anonymous": true, + "evidence_keys": ["report/2026/06/11/uuid1.png"] +} +``` + +**响应**: +```json +{ + "code": 200, + "data": { + "report_id": 67890, + "status": "pending", + "auto_hidden": true, + "target_hidden": true, + "created_at": 1718123456789 + } +} +``` + +**错误响应**: +```json +{ "code": 50004, "message": "您已举报过该对象,请等待处理" } +``` + +> **证据上传**:客户端先调用 `GET /api/v1/assets/oss/signature?type=asset` 直传 OSS,再把返回的 `key` 提交给本接口。 + +### 5.2 后台 API(Vue3 后台调用,路径前缀 `/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` | 看板:今日待处理/平均处理时长/分类分布 | + +**执行审核动作请求**: +```json +{ + "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_evidence(OSS 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_hidden:UPSERT 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 脚本 + +```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 | + +--- + +## 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 | +| 证据图校验 | OSS 签名接口限定目录 `report/` 和 `feedback/` 前缀 | +| 描述长度 | 服务端校验 ≤ 500 字(DB 字段 VARCHAR(500))| +| 证据图数量 | 服务端校验 ≤ 5 张 | +| 匿名保护 | `is_anonymous=true` 时:`reporter_id` 仍记录但 API 响应中不返回给被举报方;后台仅对超级管理员可见 | + +### 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 脚本生成种子数据(带序列重置) + +### 阶段 2:Go moderationService(topfans) +- 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/*` + +### 阶段 3:TopFans-Activity 后台 API(FastAPI) +- 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\` 藏品查询 + - 详情/列表 SQL:`LEFT 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 通知模板注册到 `notificationService`(5 个模板) +- 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`: +```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 | 初始设计 | + +--- + +**📝 设计方案已确认,可以开始实现!** ✅