diff --git a/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md b/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md index c33f2e0..e5f0b17 100644 --- a/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md +++ b/docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md @@ -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.id(AI 描述词,存在于 `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_evidence(OSS keys) + │ 有 → 返回 50004 + ├─ 4. 写入 reports 表(status='pending',snapshot 复用步骤 2 拿到的对象信息) + ├─ 5. 写 report_evidence(OSS keys,oss_url 由 OSS 签名接口的 response 预填) ├─ 6. Redis Lua 计数(独立用户才 INCR): │ 首次计数 → 检查 counter >= threshold (默认 5) │ └─ 达到 → 触发自动隐藏: - │ ① UPDATE reports WHERE target=... AND status='pending' + │ ① 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_hidden:UPSERT 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 流程 | ---