docs(moderation): 完善设计 - 序列起始值/状态机迁移/限流/孤儿清理
This commit is contained in:
parent
5320eceb32
commit
16e8eb55dd
@ -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 → moderationService(Dubbo 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.id(AI 描述词,存在于 `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` 引用。无需修改其他服务的注册配置。
|
||||
|
||||
### 阶段 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 模型
|
||||
|
||||
Loading…
Reference in New Issue
Block a user