docs(moderation): 自审修复 - target_type 命名/跨schema/Lua lock/补流程

This commit is contained in:
claude 2026-06-11 21:49:34 +08:00 committed by zheng020
parent 16e8eb55dd
commit a595440915

View File

@ -218,7 +218,7 @@ 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_type VARCHAR(30) NOT NULL, -- asset | user_profile | description_word(统一以 user_profile 表示用户主页)
target_id BIGINT NOT NULL,
target_snapshot JSONB NOT NULL, -- 提交时的对象快照
@ -352,7 +352,7 @@ 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
target_type VARCHAR(30) NOT NULL, -- asset | user_profile | description_word(与 reports.target_type 统一)
target_id BIGINT NOT NULL,
is_hidden BOOLEAN NOT NULL DEFAULT FALSE, -- 隐藏/下架
@ -389,7 +389,9 @@ CREATE INDEX idx_mts_banned ON moderation_target_status(is_banned) WHERE is_bann
| `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 缓存检查。
> `description_word` 类型的举报目标存放在 `aiChatService` 服务的数据库中(与 `topfans` 主库**共享同一个 PG 实例**、不同 schema`topfans` 与 `aichat`,通过 `schema='aichat'.ai_descriptions` 跨 schema 访问)。提交举报时 `moderationService` 直连同实例,跨 schema 查询 `aichat.ai_descriptions` 验证存在性;`moderation_target_status` 写在 `topfans` schema。`aiChatService` 读路径在自己 schema 内 `SELECT ... FROM aichat.ai_descriptions d LEFT JOIN topfans.moderation_target_status mts ON mts.target_type='description_word' AND mts.target_id=d.id`
>
> **统一规则**:所有 moderation_* 表都在 `topfans` schema跨 schema JOIN 是允许的(因为在同一 PG 实例)。如果未来拆分到不同 PG 实例,则需改用 Redis 缓存做跨库状态传递(见 2.2 节的 `moderation_counter_snapshot` 模式)。
**孤儿记录清理target 被删除时)**
- `moderation_target_status` 与业务表assets/users/ai_descriptions跨服务无外键约束应用层负责清理
@ -626,19 +628,22 @@ uni-app → POST /api/v1/moderation/reports
gateway → moderationService.SubmitReport()
├─ 0. 限流检查:每用户每天 ≤ 30 次Redis INCR `mod:rl:report:user:{user_id}:{yyyymmdd}`
│ 超限 → 返回 50012
├─ 1. 验证target_type 合法、分类启用、description ≤ 500 字、evidence ≤ 5 张
├─ 2. 验证目标对象存在assetService.GetAssetRPC / userService.GetUserRPC
├─ 2. 验证目标对象存在assetService.GetAssetRPC / userService.GetUserRPC / aichat.ai_descriptions
├─ 2.5 自举报拦截reporter_id != owner_id(target) → 否则返回 50011
├─ 3. 防重复检查:同 (reporter, target, type) 在 pending/reviewing/auto_hidden 已有?
│ 有 → 返回 50004 错误
├─ 4. 写入 reports 表status='pending'snapshot 拿当前对象信息)
├─ 5. 写 report_evidenceOSS keys
│ 有 → 返回 50004
├─ 4. 写入 reports 表status='pending'snapshot 复用步骤 2 拿到的对象信息)
├─ 5. 写 report_evidenceOSS keysoss_url 由 OSS 签名接口的 response 预填
├─ 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=...)
│ ② UPSERT moderation_target_status
│ (target_type, target_id, is_hidden=true, source='auto', source_report_id=...)
│ ③ 发通知给被举报方:"您的内容被多人举报已自动隐藏"
├─ 7. 返回 {report_id, status, auto_hidden, target_hidden, created_at}
```
@ -678,9 +683,13 @@ gateway → moderationService.SubmitReport()
│ UPDATE reports SET status='resolved', resolved_action='warn', ...
│ 发站内信给用户
├─ dismiss驳回:
│ 如果当前 auto_hiddenUPSERT is_hidden=false解除隐藏
│ UPDATE reports SET status='dismissed', resolved_action='dismissed', ...
├─ dismiss驳回状态机必经 reviewing:
│ 注意auto_hidden 工单必须先被审核员认领(→ reviewing才能 dismiss
│ 路径 auto_hidden → reviewing → dismissed
│ ① UPSERT moderation_target_status (is_hidden=false, source='admin', ...) 解除隐藏
│ ② UPDATE reports SET status='dismissed', resolved_action='dismissed', ...
│ 路径 pending/reviewing → dismissed未触发自动隐藏:
│ 仅 ②,不动 moderation_target_status
│ 发通知给举报人:"您的举报已驳回"
└─ 写 moderation_actions 流水 + admin_audit_logs
@ -691,22 +700,33 @@ gateway → moderationService.SubmitReport()
```
用户提交反馈 → POST /api/v1/moderation/feedbacks
│ moderationService 写 feedbacks (status='pending')
│ 限流:每用户每天 ≤ 5 条Redis INCR `mod:rl:feedback:user:{user_id}:{yyyymmdd}`,超限返回 50013
后台列表 → GET /api/admin/moderation/feedbacks
认领 → POST /api/admin/moderation/feedbacks/{id}/claim
│ UPDATE feedbacks SET status='reviewing', updated_at=now
│ WHERE id=? AND status='pending'(并发认领防护同举报)
回复 → POST /api/admin/moderation/feedbacks/{id}/reply
├── [可选] 释放 → POST /api/admin/moderation/feedbacks/{id}/release
│ UPDATE feedbacks SET status='pending', replied_by=NULL WHERE id=? AND status='reviewing'
├── 回复 → POST /api/admin/moderation/feedbacks/{id}/reply
│ body: { content: "..." }
│ UPDATE feedbacks SET status='replied', reply_content=?, replied_by=?, replied_at=now
│ 发通知给反馈人
├── 关闭 → POST .../close → status='closed'(终态,重复/无效)
└── 归档 → POST .../archive → status='archived'(终态)
```
### 6.4 自动隐藏 Lua 脚本
```lua
-- KEYS[1]=counter, KEYS[2]=user_marker, KEYS[3]=lock
-- KEYS[1]=counter, KEYS[2]=user_marker
-- ARGV[1]=threshold, ARGV[2]=ttl_counter, ARGV[3]=ttl_marker
-- 并发防护由应用层在调用前获取 5s 短锁 `mod:report:lock:{target_type}:{target_id}`(见 9.1
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')}
@ -721,7 +741,7 @@ end
return {0, n}
```
返回 `{triggered, count}`triggered=1 时上层触发自动隐藏。
返回 `{triggered, count}`triggered=1 时上层触发自动隐藏。`lock` 不在脚本内,由应用层在调用 EVAL 之前 `SET key 1 NX EX 5` 抢占。
---
@ -953,12 +973,14 @@ type ModerationConfig struct {
| 调用方 | 被调服务 | 触发时机 | 失败处理 |
|--------|----------|----------|----------|
| moderationService | assetService.GetAssetRPC | 提交举报验证目标 | 返回 50003 |
| moderationService | userService.GetUserRPC | 提交举报验证用户 | 返回 50003 |
| moderationService | notificationService | 触发自动隐藏后通知 | 仅日志,不影响主流程 |
| TopFans-Activity | (无 — 仅读写 DB) | - | - |
| assetService | (无 — 仅读 DB) | - | - |
| userService | (无 — 仅读 DB) | - | - |
| moderationService | assetService.GetAssetRPC | 提交举报 `target_type='asset'` 验证目标 | 返回 50003 |
| moderationService | userService.GetUserRPC | 提交举报 `target_type='user_profile'` 验证用户 | 返回 50003 |
| moderationService | aichat.ai_descriptions跨 schema 直查)| 提交举报 `target_type='description_word'` 验证目标 | 返回 50003 |
| moderationService | notificationService | 自动隐藏 + 动作结果通知5 个模板)| 仅日志,不影响主流程 |
| TopFans-Activity 后台 | (无 — 仅读写 DB) | - | - |
| assetService | (无 — 仅读 DB 做 JOIN) | - | - |
| userService | (无 — 仅读 DB 做 JOIN) | - | - |
| aichat 描述词服务 | (无 — 跨 schema 读 topfans.moderation_target_status 做 JOIN) | - | - |
### 11.3 监控指标(建议)
@ -973,6 +995,7 @@ type ModerationConfig struct {
| 版本 | 日期 | 作者 | 变更内容 |
|------|------|------|----------|
| v1.0 | 2026-06-11 | Claude | 初始设计 |
| v1.1 | 2026-06-11 | Claude | 自审修复target_type 命名统一、跨 schema JOIN 明确、Lua lock 改应用层、6.1 流程补限流/自举报、6.2 dismiss 路径细化、6.3 反馈补 release 流程 |
---