docs(moderation): 完善设计 - 序列起始值/状态机迁移/限流/孤儿清理

This commit is contained in:
claude 2026-06-11 21:46:52 +08:00 committed by zheng020
parent 5320eceb32
commit 16e8eb55dd

View File

@ -118,9 +118,15 @@ TopFans 是粉丝/明星数字藏品平台。当前平台缺少:
| `FeedbackService` (Go) | 接收反馈、查询我的反馈 |
| `CategoryService` (Go+FastAPI) | 维护分类(动态配置),客户端读取 + 后台 CRUD |
| `AutoHideExecutor` (Go) | Redis 计数 + 触发自动隐藏(写 reports + 状态表) |
| 后台 Moderation Admin (FastAPI) | 审核员认领/动作/分类管理 |
| 后台 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`
@ -150,6 +156,8 @@ admin_audit_logs 管理员操作日志
### 3.2 `report_categories` 举报分类
```sql
CREATE SEQUENCE report_categories_id_seq START WITH 10000;
CREATE TABLE report_categories (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
@ -179,6 +187,8 @@ SELECT setval('report_categories_id_seq', (SELECT MAX(id) FROM report_categories
### 3.3 `feedback_categories` 反馈分类
```sql
CREATE SEQUENCE feedback_categories_id_seq START WITH 10000;
CREATE TABLE feedback_categories (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
@ -202,6 +212,8 @@ SELECT setval('feedback_categories_id_seq', (SELECT MAX(id) FROM feedback_catego
### 3.4 `reports` 举报工单
```sql
CREATE SEQUENCE reports_id_seq START WITH 10000;
CREATE TABLE reports (
id BIGSERIAL PRIMARY KEY,
reporter_id BIGINT NOT NULL,
@ -242,6 +254,8 @@ CREATE UNIQUE INDEX uk_reports_reporter_target_pending
### 3.5 `report_evidence` 举报证据图
```sql
CREATE SEQUENCE report_evidence_id_seq START WITH 10000;
CREATE TABLE report_evidence (
id BIGSERIAL PRIMARY KEY,
report_id BIGINT NOT NULL,
@ -258,6 +272,8 @@ CREATE INDEX idx_report_evidence_report ON report_evidence(report_id, sort_order
### 3.6 `feedbacks` 反馈工单
```sql
CREATE SEQUENCE feedbacks_id_seq START WITH 10000;
CREATE TABLE feedbacks (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
@ -289,6 +305,8 @@ CREATE INDEX idx_feedbacks_category ON feedbacks(category_code, status);
### 3.7 `feedback_evidence` 反馈截图
```sql
CREATE SEQUENCE feedback_evidence_id_seq START WITH 10000;
CREATE TABLE feedback_evidence (
id BIGSERIAL PRIMARY KEY,
feedback_id BIGINT NOT NULL,
@ -305,6 +323,8 @@ CREATE INDEX idx_feedback_evidence_feedback ON feedback_evidence(feedback_id, so
### 3.8 `moderation_actions` 审核动作流水
```sql
CREATE SEQUENCE moderation_actions_id_seq START WITH 10000;
CREATE TABLE moderation_actions (
id BIGSERIAL PRIMARY KEY,
report_id BIGINT,
@ -328,6 +348,8 @@ CREATE INDEX idx_moderation_actions_admin ON moderation_actions(admin_id, create
### 3.9 `moderation_target_status` 共享"对象受管控状态"
```sql
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
@ -360,9 +382,32 @@ CREATE INDEX idx_mts_banned ON moderation_target_status(is_banned) WHERE is_bann
- `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` 主库可不同库)。提交举报时 `moderationService``aiChatService.GetDescriptionRPC` 验证存在性;状态标志位 `moderation_target_status` 仍写在 `topfans` 主库。读路径由 `aiChatService` 反向 join 或通过 Redis 缓存检查。
**孤儿记录清理target 被删除时)**
- `moderation_target_status` 与业务表assets/users/ai_descriptions跨服务无外键约束应用层负责清理
- 在 `assetService` / `userService` / `aiChatService` 的删除逻辑中加清理钩子:
```go
// 伪代码
defer func() {
moderationService.DeleteTargetStatus(ctx, targetType, targetID)
}()
```
- 定期清理脚本(每日 1 次):扫描 `moderation_target_status``target_type='asset'``target_id``assets` 表不存在的记录,软标记 `is_hidden=false` 并写 admin_audit_logs
- 详情/历史记录保留在 `moderation_actions` 流水表中供审计追溯
### 3.10 `admin_audit_logs` 管理员操作日志
```sql
CREATE SEQUENCE admin_audit_logs_id_seq START WITH 10000;
CREATE TABLE admin_audit_logs (
id BIGSERIAL PRIMARY KEY,
admin_id BIGINT NOT NULL,
@ -429,6 +474,20 @@ CREATE INDEX idx_admin_audit_logs_resource ON admin_audit_logs(resource_type, re
| `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 | (终态) | - | - | - |
**并发认领防护**
```sql
UPDATE reports
@ -437,6 +496,16 @@ WHERE id = ? AND status IN ('pending', 'auto_hidden')
```
用 affected rows = 1 判断是否抢锁成功。
**自动隐藏幂等性**
- 自动隐藏触发时,上层 SQL 加幂等保护:
```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_status``UPSERT ... ON CONFLICT (target_type, target_id) DO UPDATE`,天然幂等
### 4.2 反馈状态机
```
@ -660,8 +729,8 @@ return {0, n}
| 码 | 含义 | HTTP |
|----|------|------|
| 50001 | 举报分类不存在 | 400 |
| 50002 | 反馈分类不存在 | 400 |
| 50001 | 举报分类不存在或已停用 | 400 |
| 50002 | 反馈分类不存在或已停用 | 400 |
| 50003 | 目标对象不存在 | 404 |
| 50004 | 同一对象已举报过(未结案)| 429 |
| 50005 | 描述过长(>500 字)| 400 |
@ -670,6 +739,11 @@ return {0, n}
| 50008 | 工单已被他人认领 | 409 |
| 50009 | 工单已结案 | 400 |
| 50010 | 管理员权限不足 | 403 |
| 50011 | 不能举报自己 | 400 |
| 50012 | 提交过于频繁(限流)| 429 |
| 50013 | 反馈每日提交超限 | 429 |
| 50014 | 状态机非法迁移(如对已结案工单再次操作)| 409 |
| 50015 | 管理员未登录 / Token 失效 | 401 |
---
@ -697,11 +771,14 @@ return {0, n}
|------|------|
| 防重复举报 | DB 局部唯一索引 `uk_reports_reporter_target_pending`(结案后允许再次举报)|
| 防 Redis 计数重复 | Lua 脚本用 `user_marker` SETNX 保证独立用户才 INCR |
| 防自动隐藏并发 | Redis 短锁 `mod:report:lock:*` 5s |
| 防自动隐藏并发 | 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 数据隔离
@ -757,6 +834,25 @@ return {0, n}
- `D:\shanghai\topfans\backend\gateway\dto\moderation_converter.go`
- `D:\shanghai\topfans\backend\gateway\router\router.go` 注册 `/api/v1/moderation/*`
**Gateway 路由配置清单**(在 `router.go` 新增):
```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/Nacos`gateway` 端通过 `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 模型