From 16e8eb55dd6e8067e191ce29a90a5e583ded1649 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 11 Jun 2026 21:46:52 +0800 Subject: [PATCH] =?UTF-8?q?docs(moderation):=20=E5=AE=8C=E5=96=84=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=20-=20=E5=BA=8F=E5=88=97=E8=B5=B7=E5=A7=8B=E5=80=BC/?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E8=BF=81=E7=A7=BB/=E9=99=90?= =?UTF-8?q?=E6=B5=81/=E5=AD=A4=E5=84=BF=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06-11-moderation-report-feedback-design.md | 104 +++++++++++++++++- 1 file changed, 100 insertions(+), 4 deletions(-) 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 129b137..c33f2e0 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 @@ -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 模型