# 举报与反馈系统设计 > **状态**:设计已确认 ✅ > **创建时间**:2026-06-11 > **作者**:Claude > **目标**:在 TopFans 平台新增内容举报与用户反馈两大工单系统,支撑运营/审核员通过独立 web 后台对违规内容(数字藏品/用户/AI 描述词)和用户反馈(BUG/咨询/合作/建议)进行审核处理 --- ## 目录 1. [背景与目标](#1-背景与目标) 2. [架构概览](#2-架构概览) 3. [数据模型](#3-数据模型) 4. [状态机](#4-状态机) 5. [API 设计](#5-api-设计) 6. [关键流程](#6-关键流程) 7. [错误码](#7-错误码) 8. [通知机制](#8-通知机制) 9. [安全与防护](#9-安全与防护) 10. [实施步骤](#10-实施步骤) 11. [附录](#11-附录) --- ## 1. 背景与目标 ### 1.1 背景 TopFans 是粉丝/明星数字藏品平台。当前平台缺少: - 用户对违规内容(藏品/用户/AI 描述词)的举报通道 - 用户对平台体验问题的反馈通道 - 运营/审核员对举报和反馈的审核处理后台 ### 1.2 目标 - 提供客户端(uni-app)举报与反馈的提交入口 - 提供运营 web 后台(基于 `D:\shanghai\TopFans-activity`)的工单审核界面 - 支持"举报达到阈值自动隐藏 + 管理员审核动作(标记状态)"的闭环 - 设计可扩展、易维护、易观测 ### 1.3 范围 **包含**: - 客户端提交接口(uni-app 端) - 审核员后台(Vue3 + Element Plus) - 数据模型与迁移 - 状态机、自动隐藏、动作流水 - 通知机制 **不包含(YAGNI)**: - 举报人/被举报人间的对话沟通 - 申诉/上诉流程 - 举报人积分/奖励体系 - 复杂的多级管理员(仅一种角色) - 工单 SLA 监控告警 --- ## 2. 架构概览 ### 2.1 整体架构 ``` ┌──────────────────────────────────────────────────────────────────┐ │ 客户端 (uni-app) │ │ ┌─────────────────────────┐ ┌──────────────────────────────┐ │ │ │ 举报弹窗 (藏品/用户/描述词)│ │ 我的 → 意见反馈 │ │ │ └────────────┬────────────┘ └────────────┬─────────────────┘ │ └───────────────┼──────────────────────────────┼──────────────────┘ │ REST │ ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ topfans gateway (Go, 已有) │ │ /api/v1/moderation/* 客户端 API │ └───────────────────────────┬──────────────────────────────────────┘ │ Dubbo RPC ▼ ┌──────────────────────────────────────────────────────────────────┐ │ topfans moderationService (Go, 新建微服务) │ │ - ReportService / FeedbackService / CategoryService │ │ - 写 PostgreSQL reports/feedbacks/categories/状态表 │ │ - Redis 计数 (自动隐藏阈值) │ │ - 触发自动隐藏:UPDATE reports + UPSERT moderation_target_status│ └────────┬─────────────────────────────────────────┬───────────────┘ │ 共享 PG 库 │ │ │ ▼ ▼ ┌────────────────────────────┐ ┌──────────────────────────────────┐ │ PostgreSQL (topfans 库) │ │ Redis (topfans 已有实例) │ │ - report_categories │ │ - mod:report:counter:* │ │ - feedback_categories │ │ - mod:report:user:* │ │ - reports │ │ - mod:report:lock:* │ │ - report_evidence │ └──────────────────────────────────┘ │ - feedbacks │ │ - feedback_evidence │ │ - moderation_target_status│ ← 业务服务读路径 JOIN 此表 │ - moderation_actions │ │ - admin_audit_logs │ └────────┬───────────────────┘ │ 共享 PG 库 (直接读写,不调对方接口) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ TopFans-Activity 后台 (FastAPI + Vue3, 已有项目) │ │ D:\shanghai\TopFans-activity │ │ - backend/handlers/moderation_admin.py (后台 API) │ │ - backend/crud/moderation_crud.py │ │ - backend/schemas/moderation.py │ │ - frontend/src/views/moderation/ (举报/反馈工单 UI) │ └──────────────────────────────────────────────────────────────────┘ ``` ### 2.2 模块职责 | 模块 | 职责 | |------|------| | `ReportService` (Go) | 接收举报、查询我的举报、触发自动隐藏 | | `FeedbackService` (Go) | 接收反馈、查询我的反馈 | | `CategoryService` (Go+FastAPI) | 维护分类(动态配置),客户端读取 + 后台 CRUD | | `AutoHideExecutor` (Go) | Redis 计数 + 触发自动隐藏(写 reports + 状态表) | | 后台 Moderation Admin (FastAPI) | 审核员认领/动作/分类管理 | | `moderation_target_status` 表 | 业务服务读路径 JOIN,呈现"隐藏/封禁/警告"状态 | ### 2.3 复用现有资产 - **OSS 上传**:举报证据图、反馈截图复用 `assetService` 的 OSS 签名接口(`GET /api/v1/assets/oss/signature?type=asset`) - **认证**:客户端 JWT 用 topfans userService;后台 JWT 用 `TopFans-activity` 已有的 `verify_token` - **通知中心**:处理结果通过 topfans `notificationService` 模板下发(新增 5 个模板) - **数据迁移**:遵循 topfans 现有 `backend/migrations/00x_*.sql` 命名规范 - **序列同步**:手动指定 ID 时必须 `SELECT setval('table_id_seq', MAX(id))`(CLAUDE.md 强制规范) --- ## 3. 数据模型 ### 3.1 核心表 ``` report_categories 举报分类(动态配置) feedback_categories 反馈分类(动态配置) reports 举报工单主表 report_evidence 举报证据图(一对多) feedbacks 反馈工单主表 feedback_evidence 反馈截图(一对多) moderation_actions 审核动作流水 moderation_target_status 共享"对象受管控状态"表(业务服务读路径 JOIN) admin_audit_logs 管理员操作日志 ``` ### 3.2 `report_categories` 举报分类 ```sql CREATE TABLE report_categories ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, description VARCHAR(200), severity SMALLINT NOT NULL DEFAULT 1, enabled BOOLEAN NOT NULL DEFAULT TRUE, sort_order INT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); CREATE INDEX idx_report_categories_enabled ON report_categories(enabled, sort_order); -- 初始数据 INSERT INTO report_categories (code, name, description, severity, sort_order, created_at, updated_at) VALUES ('pornographic', '色情低俗', '含裸露、性暗示内容', 5, 1, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000), ('violence', '暴力血腥', '含暴力、血腥画面', 5, 2, ...), ('infringing', '侵权盗版', '侵犯他人著作权/商标权', 4, 3, ...), ('false_info', '虚假信息', '虚假、欺骗性内容', 3, 4, ...), ('political', '政治敏感', '涉及政治敏感话题', 5, 5, ...), ('ad_spam', '广告骚扰', '垃圾广告、骚扰信息', 2, 6, ...), ('other', '其他', '其他违规情况', 1, 99, ...); SELECT setval('report_categories_id_seq', (SELECT MAX(id) FROM report_categories)); ``` ### 3.3 `feedback_categories` 反馈分类 ```sql CREATE TABLE feedback_categories ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, description VARCHAR(200), enabled BOOLEAN NOT NULL DEFAULT TRUE, sort_order INT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); INSERT INTO feedback_categories (code, name, description, sort_order, created_at, updated_at) VALUES ('bug', 'BUG 报告', '使用中遇到的问题', 1, ...), ('consult', '使用咨询', '不知道怎么用', 2, ...), ('business', '内容合作', '合作/商务联系', 3, ...), ('suggestion', '功能建议', '希望增加什么功能', 4, ...); SELECT setval('feedback_categories_id_seq', (SELECT MAX(id) FROM feedback_categories)); ``` ### 3.4 `reports` 举报工单 ```sql 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_id BIGINT NOT NULL, target_snapshot JSONB NOT NULL, -- 提交时的对象快照 category_code VARCHAR(50) NOT NULL, description VARCHAR(500), is_anonymous BOOLEAN NOT NULL DEFAULT FALSE, status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / reviewing / auto_hidden / resolved / dismissed is_auto_hidden BOOLEAN NOT NULL DEFAULT FALSE, resolved_action VARCHAR(20), -- takedown | ban | warn | dismissed resolved_by BIGINT, resolved_at BIGINT, resolution_note VARCHAR(500), created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, CONSTRAINT fk_reports_category FOREIGN KEY (category_code) REFERENCES report_categories(code) ON DELETE RESTRICT ); CREATE INDEX idx_reports_status_created ON reports(status, created_at DESC); CREATE INDEX idx_reports_target ON reports(target_type, target_id, status); CREATE INDEX idx_reports_reporter ON reports(reporter_id, created_at DESC); -- 防重复:同一举报人对同一对象在未结案前只能有一条 CREATE UNIQUE INDEX uk_reports_reporter_target_pending ON reports(reporter_id, target_type, target_id) WHERE status IN ('pending', 'reviewing', 'auto_hidden'); ``` ### 3.5 `report_evidence` 举报证据图 ```sql CREATE TABLE report_evidence ( id BIGSERIAL PRIMARY KEY, report_id BIGINT NOT NULL, oss_key VARCHAR(255) NOT NULL, oss_url VARCHAR(500), sort_order INT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, CONSTRAINT fk_report_evidence_report FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE ); CREATE INDEX idx_report_evidence_report ON report_evidence(report_id, sort_order); ``` ### 3.6 `feedbacks` 反馈工单 ```sql CREATE TABLE feedbacks ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, star_id BIGINT, category_code VARCHAR(50) NOT NULL, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, contact VARCHAR(100), is_anonymous BOOLEAN NOT NULL DEFAULT FALSE, status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / reviewing / replied / closed / archived replied_by BIGINT, replied_at BIGINT, reply_content TEXT, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, CONSTRAINT fk_feedbacks_category FOREIGN KEY (category_code) REFERENCES feedback_categories(code) ON DELETE RESTRICT ); CREATE INDEX idx_feedbacks_status_created ON feedbacks(status, created_at DESC); CREATE INDEX idx_feedbacks_user ON feedbacks(user_id, created_at DESC); CREATE INDEX idx_feedbacks_category ON feedbacks(category_code, status); ``` ### 3.7 `feedback_evidence` 反馈截图 ```sql CREATE TABLE feedback_evidence ( id BIGSERIAL PRIMARY KEY, feedback_id BIGINT NOT NULL, oss_key VARCHAR(255) NOT NULL, oss_url VARCHAR(500), sort_order INT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, CONSTRAINT fk_feedback_evidence_feedback FOREIGN KEY (feedback_id) REFERENCES feedbacks(id) ON DELETE CASCADE ); CREATE INDEX idx_feedback_evidence_feedback ON feedback_evidence(feedback_id, sort_order); ``` ### 3.8 `moderation_actions` 审核动作流水 ```sql CREATE TABLE moderation_actions ( id BIGSERIAL PRIMARY KEY, report_id BIGINT, feedback_id BIGINT, admin_id BIGINT NOT NULL, action_type VARCHAR(30) NOT NULL, -- takedown | unhide | ban | warn | dismiss | reply | close | archive target_type VARCHAR(30), target_id BIGINT, note VARCHAR(500), success BOOLEAN NOT NULL, error_message VARCHAR(500), created_at BIGINT NOT NULL, CONSTRAINT chk_moderation_actions_xor CHECK ((report_id IS NOT NULL) OR (feedback_id IS NOT NULL)) ); CREATE INDEX idx_moderation_actions_report ON moderation_actions(report_id, created_at DESC); CREATE INDEX idx_moderation_actions_feedback ON moderation_actions(feedback_id, created_at DESC); CREATE INDEX idx_moderation_actions_admin ON moderation_actions(admin_id, created_at DESC); ``` ### 3.9 `moderation_target_status` 共享"对象受管控状态" ```sql CREATE TABLE moderation_target_status ( id BIGSERIAL PRIMARY KEY, target_type VARCHAR(30) NOT NULL, -- asset | user | description_word target_id BIGINT NOT NULL, is_hidden BOOLEAN NOT NULL DEFAULT FALSE, -- 隐藏/下架 is_banned BOOLEAN NOT NULL DEFAULT FALSE, -- 封禁(仅 user) is_warned BOOLEAN NOT NULL DEFAULT FALSE, -- 已警告(仅 user) reason VARCHAR(200), source VARCHAR(30) NOT NULL, -- auto | admin source_report_id BIGINT, operator_admin_id BIGINT, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, CONSTRAINT uk_moderation_target UNIQUE (target_type, target_id) ); CREATE INDEX idx_mts_hidden ON moderation_target_status(is_hidden) WHERE is_hidden = TRUE; CREATE INDEX idx_mts_banned ON moderation_target_status(is_banned) WHERE is_banned = TRUE; ``` **写入方**: - `moderationService` (Go) — 自动隐藏时 UPSERT - `TopFans-Activity` 后台 (FastAPI) — 审核员手动下架/封禁/警告时 UPSERT **读取方**: - `assetService` 藏品查询/详情/列表:`LEFT JOIN moderation_target_status ON target_type='asset' AND target_id=assets.id WHERE COALESCE(is_hidden, false) = false` - `userService` 登录/操作校验:`LEFT JOIN ... WHERE COALESCE(is_banned, false) = false` - AI 描述词服务查询时同样 JOIN ### 3.10 `admin_audit_logs` 管理员操作日志 ```sql CREATE TABLE admin_audit_logs ( id BIGSERIAL PRIMARY KEY, admin_id BIGINT NOT NULL, action VARCHAR(50) NOT NULL, resource_type VARCHAR(30), resource_id BIGINT, ip VARCHAR(50), user_agent VARCHAR(500), extra JSONB, created_at BIGINT NOT NULL ); CREATE INDEX idx_admin_audit_logs_admin ON admin_audit_logs(admin_id, created_at DESC); CREATE INDEX idx_admin_audit_logs_resource ON admin_audit_logs(resource_type, resource_id); ``` ### 3.11 Redis 数据结构 | Key 模式 | 类型 | 用途 | TTL | |----------|------|------|-----| | `mod:report:counter:{target_type}:{target_id}` | String (INCR) | 自动隐藏阈值计数(独立用户数)| 7 天 | | `mod:report:user:{target_type}:{target_id}:{user_id}` | String | 防重复(仅独立用户才计数)| 24h | | `mod:report:lock:{target_type}:{target_id}` | String | 防并发触发自动隐藏 | 5s | | `mod:auto_hide_threshold` | String (缓存) | 自动隐藏阈值 N(默认 5) | 永久 | ### 3.12 序列同步规范 遵循 CLAUDE.md 规范:所有手动指定 ID 的 INSERT 末尾必须 `SELECT setval('table_id_seq', (SELECT MAX(id) FROM table))`。 新表起始值:`CREATE SEQUENCE table_id_seq START WITH 10000;` 预留测试数据空间。 --- ## 4. 状态机 ### 4.1 举报状态机 ``` ┌──────────────────────────────────┐ │ pending │ │ (待处理) │ └─────┬─────────────┬──────────────┘ 管理员认领 │ │ 阈值 N 触发 ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ reviewing │ │ auto_hidden │ │ (审核中) │ │ (已自动隐藏) │ └────┬────┬────┬───┘ └────┬─────────────┘ 下架/封禁 │ │驳回│ 解除隐藏 │ 管理员认领 /警告 │ │ │ ▼ ▼ │ ▼ ┌──────────────────┐ ┌──────┐ │ ┌──────────────┐ │ │resolved│ │ │ dismissed │ │ │(已处理)│ │ │ (已驳回) │ │ └──────┘ │ └──────────────┘ │ │ │ └────→ resolved / dismissed ``` | 状态 | 含义 | 进入 | 退出 | |------|------|------|------| | `pending` | 新提交待处理 | 创建 | 认领 OR 自动隐藏 | | `reviewing` | 管理员已认领 | 认领 | 做出处理动作 | | `auto_hidden` | 触发自动隐藏 | 阈值达成 | 管理员认领并做最终裁决 | | `resolved` | 已处理 | takedown/ban/warn | 终态 | | `dismissed` | 已驳回 | dismiss | 终态 | **并发认领防护**: ```sql UPDATE reports SET status = 'reviewing', updated_at = ? WHERE id = ? AND status IN ('pending', 'auto_hidden') ``` 用 affected rows = 1 判断是否抢锁成功。 ### 4.2 反馈状态机 ``` ┌────────────┐ │ pending │ └─────┬──────┘ │ 认领 ▼ ┌────────────────┐ │ reviewing │ └─┬──────┬────┬──┘ │ │ │ 已回复 │ │关闭│ 归档 ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │replied │ │closed │ │archived│ └────────┘ └────────┘ └────────┘ ``` | 状态 | 进入 | 备注 | |------|------|------| | `pending` | 创建 | 初始 | | `reviewing` | 认领 | | | `replied` | 填写 `reply_content` | 终态,触发用户通知 | | `closed` | 点击关闭 | 终态,重复/无效 | | `archived` | 归档 | 终态,已处理保留记录 | --- ## 5. API 设计 ### 5.1 客户端 API(uni-app 端调用,路径前缀 `/api/v1/moderation`) | Method & Path | 说明 | |---------------|------| | `GET /api/v1/moderation/report-categories` | 获取启用中的举报分类 | | `POST /api/v1/moderation/reports` | 提交举报 | | `GET /api/v1/moderation/reports?status=&page=&page_size=` | 我的举报记录 | | `GET /api/v1/moderation/reports/{id}` | 查看举报进度与结果 | | `GET /api/v1/moderation/feedback-categories` | 获取反馈分类 | | `POST /api/v1/moderation/feedbacks` | 提交反馈 | | `GET /api/v1/moderation/feedbacks?status=&page=&page_size=` | 我的反馈记录 | | `GET /api/v1/moderation/feedbacks/{id}` | 查看反馈详情(含回复)| **提交举报请求体**: ```json { "target_type": "asset", "target_id": 12345, "category_code": "pornographic", "description": "图片含有裸露内容", "is_anonymous": true, "evidence_keys": ["report/2026/06/11/uuid1.png"] } ``` **响应**: ```json { "code": 200, "data": { "report_id": 67890, "status": "pending", "auto_hidden": true, "target_hidden": true, "created_at": 1718123456789 } } ``` **错误响应**: ```json { "code": 50004, "message": "您已举报过该对象,请等待处理" } ``` > **证据上传**:客户端先调用 `GET /api/v1/assets/oss/signature?type=asset` 直传 OSS,再把返回的 `key` 提交给本接口。 ### 5.2 后台 API(Vue3 后台调用,路径前缀 `/api/admin/moderation`) | Method & Path | 说明 | |---------------|------| | `GET /api/admin/moderation/reports?status=&category=&target_type=&keyword=&page=&page_size=` | 举报工单列表 | | `GET /api/admin/moderation/reports/{id}` | 举报详情 | | `POST /api/admin/moderation/reports/{id}/claim` | 认领 | | `POST /api/admin/moderation/reports/{id}/release` | 释放认领 | | `POST /api/admin/moderation/reports/{id}/actions` | 执行审核动作 | | `GET /api/admin/moderation/feedbacks?status=&category=&keyword=&page=&page_size=` | 反馈列表 | | `GET /api/admin/moderation/feedbacks/{id}` | 反馈详情 | | `POST /api/admin/moderation/feedbacks/{id}/claim` | 认领反馈 | | `POST /api/admin/moderation/feedbacks/{id}/reply` | 回复(→ replied)| | `POST /api/admin/moderation/feedbacks/{id}/close` | 关闭 | | `POST /api/admin/moderation/feedbacks/{id}/archive` | 归档 | | `GET/POST/PUT/DELETE /api/admin/moderation/report-categories` | 举报分类 CRUD | | `GET/POST/PUT/DELETE /api/admin/moderation/feedback-categories` | 反馈分类 CRUD | | `GET /api/admin/moderation/stats` | 看板:今日待处理/平均处理时长/分类分布 | **执行审核动作请求**: ```json { "action": "takedown", "note": "图片含裸露,已下架", "send_notification": true } ``` --- ## 6. 关键流程 ### 6.1 用户提交举报(含自动隐藏) ``` 用户点击"举报" │ ▼ uni-app → POST /api/v1/moderation/reports │ ▼ gateway → moderationService.SubmitReport() │ ├─ 1. 验证:target_type 合法、分类启用、description ≤ 500 字、evidence ≤ 5 张 ├─ 2. 验证:目标对象存在(assetService.GetAssetRPC / userService.GetUserRPC) ├─ 3. 防重复检查:同 (reporter, target, type) 在 pending/reviewing/auto_hidden 已有? │ 有 → 返回 50004 错误 ├─ 4. 写入 reports 表(status='pending',snapshot 拿当前对象信息) ├─ 5. 写 report_evidence(OSS keys) ├─ 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=...) │ ③ 发通知给被举报方:"您的内容被多人举报已自动隐藏" ├─ 7. 返回 {report_id, status, auto_hidden, target_hidden, created_at} ``` ### 6.2 后台审核流程 ``` 审核员登录后台 → 列表页 (筛选 status=pending OR auto_hidden) │ ▼ 详情 → GET /api/admin/moderation/reports/{id} │ ▼ 认领 → POST /api/admin/moderation/reports/{id}/claim │ UPDATE reports SET status='reviewing', updated_at=now │ WHERE id=? AND status IN ('pending','auto_hidden') │ affected=1 才返回成功 │ ▼ 查看证据、目标快照、举报人、流水 │ ▼ 执行动作 → POST /api/admin/moderation/reports/{id}/actions │ ├─ takedown(藏品/描述词/用户内容): │ UPSERT moderation_target_status (is_hidden=true, source='admin', ...) │ UPDATE reports SET status='resolved', resolved_action='takedown', ... │ 发通知给被举报方 │ ├─ ban(用户): │ UPSERT moderation_target_status (is_banned=true, source='admin', ...) │ UPDATE reports SET status='resolved', resolved_action='ban', ... │ 发通知给被封禁用户 │ ├─ warn(用户): │ UPSERT moderation_target_status (is_warned=true, source='admin', ...) │ UPDATE reports SET status='resolved', resolved_action='warn', ... │ 发站内信给用户 │ ├─ dismiss(驳回): │ 如果当前 auto_hidden:UPSERT is_hidden=false(解除隐藏) │ UPDATE reports SET status='dismissed', resolved_action='dismissed', ... │ 发通知给举报人:"您的举报已驳回" │ └─ 写 moderation_actions 流水 + admin_audit_logs ``` ### 6.3 反馈流程 ``` 用户提交反馈 → POST /api/v1/moderation/feedbacks │ moderationService 写 feedbacks (status='pending') ▼ 后台列表 → GET /api/admin/moderation/feedbacks ▼ 认领 → POST /api/admin/moderation/feedbacks/{id}/claim ▼ 回复 → POST /api/admin/moderation/feedbacks/{id}/reply │ body: { content: "..." } │ UPDATE feedbacks SET status='replied', reply_content=?, replied_by=?, replied_at=now │ 发通知给反馈人 ``` ### 6.4 自动隐藏 Lua 脚本 ```lua -- KEYS[1]=counter, KEYS[2]=user_marker, KEYS[3]=lock -- ARGV[1]=threshold, ARGV[2]=ttl_counter, ARGV[3]=ttl_marker 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')} end local n = redis.call('INCR', KEYS[1]) if n == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end if n >= tonumber(ARGV[1]) then return {1, n} end return {0, n} ``` 返回 `{triggered, count}`:triggered=1 时上层触发自动隐藏。 --- ## 7. 错误码 | 码 | 含义 | HTTP | |----|------|------| | 50001 | 举报分类不存在 | 400 | | 50002 | 反馈分类不存在 | 400 | | 50003 | 目标对象不存在 | 404 | | 50004 | 同一对象已举报过(未结案)| 429 | | 50005 | 描述过长(>500 字)| 400 | | 50006 | 证据图超限(>5 张)| 400 | | 50007 | 工单不存在 | 404 | | 50008 | 工单已被他人认领 | 409 | | 50009 | 工单已结案 | 400 | | 50010 | 管理员权限不足 | 403 | --- ## 8. 通知机制 复用 `notificationService` 模块,注册 5 个模板: | 事件 | 接收方 | 模板名 | 渠道 | |------|--------|--------|------| | 举报状态变更为 resolved/dismissed | 举报人 | `report_resolved` | 站内信 + 通知中心 | | 举报成立 + takedown | 被举报方 | `report_takedown_notice` | 站内信 | | 举报成立 + ban | 被举报用户 | `report_ban_notice` | 站内信 + 短信 | | 举报成立 + warn | 被举报用户 | `report_warn_notice` | 站内信 | | 反馈被回复 | 反馈人 | `feedback_replied` | 站内信 + 通知中心 | > 模板字段:`{reporter_nickname, target_type, target_id, action, reason, related_url}` --- ## 9. 安全与防护 ### 9.1 防护机制 | 机制 | 实现 | |------|------| | 防重复举报 | DB 局部唯一索引 `uk_reports_reporter_target_pending`(结案后允许再次举报)| | 防 Redis 计数重复 | Lua 脚本用 `user_marker` SETNX 保证独立用户才 INCR | | 防自动隐藏并发 | Redis 短锁 `mod:report:lock:*` 5s | | 证据图校验 | OSS 签名接口限定目录 `report/` 和 `feedback/` 前缀 | | 描述长度 | 服务端校验 ≤ 500 字(DB 字段 VARCHAR(500))| | 证据图数量 | 服务端校验 ≤ 5 张 | | 匿名保护 | `is_anonymous=true` 时:`reporter_id` 仍记录但 API 响应中不返回给被举报方;后台仅对超级管理员可见 | ### 9.2 数据隔离 - 客户端 JWT 仅可看自己的举报/反馈(`reporter_id = current_user_id`) - 后台 JWT 走 `TopFans-activity` 现有 `verify_token`,所有接口都需 `Depends(verify_token)` - 后续如要分级权限(仅超管可恢复隐藏等),在 JWT payload 加 `role` 字段 ### 9.3 数据保留 - `reports` / `feedbacks` 永久保留(合规需要) - `moderation_actions` 永久保留(审计) - `moderation_target_status` UPSERT 保留最新状态,历史可查 `moderation_actions` --- ## 10. 实施步骤 ### 阶段 1:数据库(topfans 库) - 1.1 写迁移 `D:\shanghai\topfans\backend\migrations\2026_06_11_010_moderation_tables.sql` - 1.2 包含 8 张表 + 索引 + 默认分类种子数据 - 1.3 末尾必须 `SELECT setval('xxx_id_seq', (SELECT MAX(id) FROM xxx))`(CLAUDE.md 强制) - 1.4 在 `backend/scripts/` 加 Go 脚本生成种子数据(带序列重置) ### 阶段 2:Go moderationService(topfans) - 2.1 `D:\shanghai\topfans\backend\services\moderationService\` 新建目录 - `main.go` / `config/` / `repository/` / `service/` / `provider/` / `client/` / `go.mod` / `start.sh` - 复用 socialService 类似的分层 - 2.2 `D:\shanghai\topfans\backend\proto\moderation\moderation.proto` - `SubmitReportRequest/Response` - `ListMyReportsRequest/Response` - `GetReportRequest/Response` - `GetReportCategoriesRequest/Response` - `SubmitFeedbackRequest/Response` - `ListMyFeedbacksRequest/Response` - `GetFeedbackRequest/Response` - `GetFeedbackCategoriesRequest/Response` - 2.3 `D:\shanghai\topfans\backend\pkg\proto\moderation\` 编译生成 `.pb.go` - 2.4 `D:\shanghai\topfans\backend\pkg\models\moderation.go` ORM 模型 - 2.5 `D:\shanghai\topfans\backend\services\moderationService\repository\moderation_repository.go` 数据访问 - 2.6 `D:\shanghai\topfans\backend\services\moderationService\service\`: - `report_service.go` - `feedback_service.go` - `category_service.go` - `auto_hide_service.go`(含 Redis Lua 脚本) - 2.7 `D:\shanghai\topfans\backend\services\moderationService\client\`: - `asset_client.go`(验证目标存在) - `user_client.go`(验证用户/封禁查询) - 2.8 `D:\shanghai\topfans\backend\services\moderationService\provider\social_provider.go` Dubbo 注册 - 2.9 `D:\shanghai\topfans\backend\go.work` 加入新服务 - 2.10 Gateway 层: - `D:\shanghai\topfans\backend\gateway\controller\moderation_controller.go` - `D:\shanghai\topfans\backend\gateway\dto\moderation_dto.go` - `D:\shanghai\topfans\backend\gateway\dto\moderation_converter.go` - `D:\shanghai\topfans\backend\gateway\router\router.go` 注册 `/api/v1/moderation/*` ### 阶段 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 模型 - 3.3 `D:\shanghai\TopFans-activity\backend\crud\moderation_crud.py` 数据库操作 - `list_reports(filters, page) / get_report_detail(id) / claim_report(id) / execute_action(id, action, ...) / list_categories() / upsert_category() / ...` - 3.4 `D:\shanghai\TopFans-activity\backend\handlers\moderation_admin.py` 路由层 - 复用现有 `verify_token` 中间件 - 3.5 `D:\shanghai\TopFans-activity\backend\router\__init__.py` 注册路由 - 3.6 数据库迁移(如 ORM 与 PG 表不一致时): - `D:\shanghai\TopFans-activity\backend\migrations\010_moderation_init.sql`(如果用 SQLAlchemy create_all 可省略) ### 阶段 4:后台前端 (Vue3) - 4.1 `D:\shanghai\TopFans-activity\frontend\src\api\moderation.js` axios 封装 - `getReports()` / `getReportDetail()` / `claimReport()` / `executeAction()` / ... - `getReportCategories()` / `createCategory()` / `updateCategory()` / `deleteCategory()` / ... - 4.2 `D:\shanghai\TopFans-activity\frontend\src\views\moderation\` - `ReportList.vue` 举报工单列表(筛选:状态/分类/目标类型/关键词) - `ReportDetail.vue` 举报详情(证据图、目标快照、举报人、流水)+ 审核动作按钮 - `FeedbackList.vue` 反馈列表 - `FeedbackDetail.vue` 反馈详情 + 回复表单 - `ReportCategoryConfig.vue` 举报分类 CRUD - `FeedbackCategoryConfig.vue` 反馈分类 CRUD - `Dashboard.vue` 审核看板(可选) - 4.3 `D:\shanghai\TopFans-activity\frontend\src\router\index.js` 加路由 - `/moderation/reports` - `/moderation/reports/:id` - `/moderation/feedbacks` - `/moderation/feedbacks/:id` - `/moderation/categories/report` - `/moderation/categories/feedback` - 4.4 菜单注册(按 `Layout.vue` 现成结构) - 4.5 Element Plus 组件复用:el-table / el-form / el-dialog / el-image / el-tag / el-pagination ### 阶段 5:客户端 uni-app - 5.1 `D:\shanghai\topfans\frontend\utils\api.js` 加: - `getReportCategoriesApi()` / `submitReportApi(payload)` / `getMyReportsApi()` / `getMyReportDetailApi(id)` - `getFeedbackCategoriesApi()` / `submitFeedbackApi(payload)` / `getMyFeedbacksApi()` / `getMyFeedbackDetailApi(id)` - 5.2 `D:\shanghai\topfans\frontend\components\ReportModal.vue` 通用举报弹窗 - props: `targetType, targetId, targetName` - 表单:分类单选 + 描述 textarea + 证据图上传(最多 5 张)+ 匿名开关 - 调 `submitReportApi`,成功后提示"已提交"+ 自动隐藏提示 - 5.3 在以下位置接入 `ReportModal`: - 藏品详情页 (`NftDetailModal.vue` 等) 长按或"..."菜单 - 用户主页(粉丝身份下)"..."菜单 - 描述词展示组件(如果有)的"..."菜单 - 5.4 `D:\shanghai\topfans\frontend\pages\profile\feedback.vue` 意见反馈页 - 复用 `ReportModal` 的 evidence 上传组件 - 表单:分类下拉 + 标题 + 内容 + 联系方式(可选) + 截图 - 5.5 `D:\shanghai\topfans\frontend\pages\profile\myReports.vue` 我的举报列表 - 5.6 `D:\shanghai\topfans\frontend\pages\profile\myFeedbacks.vue` 我的反馈列表 - 5.7 页面间跳转:从"我的" → 各项入口 ### 阶段 6:业务服务读路径加 JOIN(按需展开) - 6.1 `D:\shanghai\topfans\backend\services\assetService\repository\` 藏品查询 - 详情/列表 SQL:`LEFT JOIN moderation_target_status ON target_type='asset' AND target_id=assets.id WHERE COALESCE(is_hidden, false) = false` - 6.2 `D:\shanghai\topfans\backend\services\userService\repository\` 用户校验 - 登录/操作校验:`LEFT JOIN ... ON target_type='user' AND target_id=users.id WHERE COALESCE(is_banned, false) = false` - 6.3 描述词相关服务(`aiChatService` 或新)查询时同样 JOIN - 6.4 性能优化(如果热路径压力变大): - Redis 缓存 `mod:status:cache:{target_type}:{target_id}` TTL 60s - 命中即跳过 JOIN ### 阶段 7:通知与测试 - 7.1 通知模板注册到 `notificationService`(5 个模板) - 7.2 Go 单元测试 - `report_service_test.go` 防重复、自动隐藏触发 - `feedback_service_test.go` 状态机 - `auto_hide_service_test.go` Lua 脚本逻辑 - 7.3 FastAPI 单元测试 - `test_moderation_admin.py` 列表/详情/认领/动作 - 鉴权测试(无 token 401) - 7.4 集成测试 - 端到端:提交举报 → 后台审核 → 状态变更 → 用户收到通知 - 自动隐藏:5 个独立用户举报 → 自动隐藏 + 状态表写入 - 7.5 前端联调 - uni-app → gateway → Go service - Vue3 后台 → FastAPI → PG --- ## 11. 附录 ### 11.1 配置文件 在 `D:\shanghai\topfans\backend\services\moderationService\config\moderation_config.go`: ```go type ModerationConfig struct { AutoHideThreshold int // 默认 5 CounterTTL time.Duration // 7 天 UserMarkerTTL time.Duration // 24h LockTTL time.Duration // 5s MaxDescriptionLen int // 500 MaxEvidenceCount int // 5 } ``` ### 11.2 跨服务调用清单 | 调用方 | 被调服务 | 触发时机 | 失败处理 | |--------|----------|----------|----------| | moderationService | assetService.GetAssetRPC | 提交举报验证目标 | 返回 50003 | | moderationService | userService.GetUserRPC | 提交举报验证用户 | 返回 50003 | | moderationService | notificationService | 触发自动隐藏后通知 | 仅日志,不影响主流程 | | TopFans-Activity | (无 — 仅读写 DB) | - | - | | assetService | (无 — 仅读 DB) | - | - | | userService | (无 — 仅读 DB) | - | - | ### 11.3 监控指标(建议) - `moderation.report.submitted.count` (counter, tags: target_type, category_code) - `moderation.report.auto_hidden.count` (counter) - `moderation.report.resolve.duration` (histogram, 从 created_at 到 resolved_at) - `moderation.feedback.reply.duration` (histogram) - `moderation.actions.failed.count` (counter, tags: action_type, error) ### 11.4 变更历史 | 版本 | 日期 | 作者 | 变更内容 | |------|------|------|----------| | v1.0 | 2026-06-11 | Claude | 初始设计 | --- **📝 设计方案已确认,可以开始实现!** ✅