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

884 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 举报与反馈系统设计
> **状态**:设计已确认 ✅
> **创建时间**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 | 初始设计 |
---
**📝 设计方案已确认,可以开始实现!** ✅