docs(moderation): 举报与反馈系统设计

This commit is contained in:
claude 2026-06-11 21:38:19 +08:00 committed by zheng020
parent a1bb302be8
commit 5320eceb32

View File

@ -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 客户端 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}` | 查看反馈详情(含回复)|
**提交举报请求体**
```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 后台 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` | 看板:今日待处理/平均处理时长/分类分布 |
**执行审核动作请求**
```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_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 脚本
```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 脚本生成种子数据(带序列重置)
### 阶段 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/*`
### 阶段 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\` 藏品查询
- 详情/列表 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 | 初始设计 |
---
**📝 设计方案已确认,可以开始实现!** ✅