42 KiB
举报与反馈系统实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在 TopFans 平台新增内容举报与用户反馈两大工单系统(客户端提交 + 后台审核处理 + 自动隐藏阈值闭环)
Architecture:
- 独立 Go Dubbo 微服务
moderationService(端口 20010)承载客户端 API + 自动隐藏阈值触发 - 共享 PostgreSQL
top-fans库 9 张表;Redis 计数;OSS 证据图上传 TopFans-activity-admin后台(FastAPI + Vue3)独立读写共享库做审核动作- 业务表软删除(
is_active+deleted_at)承担下架/封禁,moderation_target_status仅做审计 + 警告状态
Tech Stack: Go 1.21+ / Dubbo-go / GORM / PostgreSQL 16 / Redis 7 / FastAPI / SQLAlchemy / Vue3 + Element Plus / uni-app + uView 2.x
参考 spec: docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md (v2.5)
子计划拆分建议(scope check):本计划跨 2 个仓库、5 种技术栈、11 阶段。建议在执行时按仓库拆分为 4 个独立执行计划:
| 子计划 | 阶段 | 仓库 | 工作目录 |
|---|---|---|---|
| A: 主仓数据库 + Go 微服务 | 1, 2, 6 | topfans 主仓 | TopFansByGithub |
| B: 后台 API + 前端 | 3, 4 | TopFans-activity-admin | TopFans-activity-admin |
| C: 客户端集成 | 5 | topfans 主仓 frontend/ |
TopFansByGithub |
| D: 通知 + 测试 | 7 | 两仓并行 | - |
约束(来自 CLAUDE.md + 设计文档):
- ✅ 任何手动指定 ID 的 INSERT 必须
SELECT setval('xxx_id_seq', (SELECT MAX(id) FROM xxx)) - ✅ 不主动 commit,等用户明确指示
- ✅ 错误处理统一错误码(50001-50019)+ 友好提示 + 日志
- ✅ 自审修复后做整体回归检查
文件结构总览
A. topfans 主仓 — 新建文件
| 路径 | 职责 |
|---|---|
backend/migrations/2026_06_11_011_moderation_tables.sql |
9 张表 + 索引 + 种子数据 + 9 个序列同步 |
backend/proto/moderation.proto |
RPC 接口定义 |
backend/pkg/proto/moderation/moderation.pb.go |
proto 生成(编译产物) |
backend/pkg/proto/moderation/moderation.triple.go |
proto 生成(编译产物) |
backend/pkg/models/moderation.go |
GORM 模型(共享 pkg 模式) |
backend/services/moderationService/main.go |
Dubbo 启动入口(端口 20010) |
backend/services/moderationService/start.sh |
启动脚本 |
backend/services/moderationService/configs/dubbo.yaml |
Dubbo 配置 |
backend/services/moderationService/config/moderation_config.go |
业务配置 |
backend/services/moderationService/repository/moderation_repository.go |
仓储层 |
backend/services/moderationService/repository/moderation_repository_test.go |
仓储单测 |
backend/services/moderationService/service/report_service.go |
举报业务(含事务内双重/三重写入) |
backend/services/moderationService/service/feedback_service.go |
反馈业务 |
backend/services/moderationService/service/category_service.go |
分类维护 |
backend/services/moderationService/service/auto_hide_service.go |
Redis Lua 自动隐藏 |
backend/services/moderationService/service/transaction_helper.go |
WithTx(ctx, fn) 封装 |
backend/services/moderationService/service/lua/auto_hide.lua |
Lua 脚本(//go:embed) |
backend/services/moderationService/client/asset_client.go |
验证目标存在 |
backend/services/moderationService/client/user_client.go |
验证用户/封禁查询 |
backend/services/moderationService/client/notification_client.go |
发通知(v1.5 stub → 阶段 7 替换) |
backend/services/moderationService/client/redis_client.go |
Redis 客户端 |
backend/services/moderationService/provider/moderation_provider.go |
Dubbo 服务注册 |
backend/services/moderationService/service/report_service_test.go |
状态机 + claimed_* 一致性测试 |
backend/services/moderationService/service/feedback_service_test.go |
状态机测试 |
backend/services/moderationService/service/auto_hide_service_test.go |
Lua 测试 |
backend/services/moderationService/service/concurrency_test.go |
并发场景 |
backend/gateway/controller/moderation_controller.go |
HTTP 控制器(客户端路由) |
backend/gateway/dto/moderation_dto.go |
DTO |
backend/gateway/dto/moderation_converter.go |
RPC ↔ DTO 映射 |
backend/gateway/client/moderation_client.go |
Gateway 端 Dubbo 客户端 |
frontend/utils/api.js (末尾追加) |
8 个客户端 API 方法 |
frontend/components/ReportModal.vue |
举报弹窗 |
frontend/pages/profile/feedback.vue |
意见反馈页 |
frontend/pages/profile/myReports.vue |
我的举报列表 |
frontend/pages/profile/myFeedbacks.vue |
我的反馈列表 |
B. topfans 主仓 — 修改文件
| 路径 | 改动 |
|---|---|
backend/services/assetService/client/oss/config.go |
GetUploadDir() switch 补 report / feedback 两个 case |
backend/services/assetService/client/oss/asset_controller.go |
白名单扩为 4 个值;InitMintOrder 调用按 type 分支跳过 |
backend/gateway/router/router.go |
注册 /api/v1/moderation/*(仅客户端路由) |
backend/go.work |
加入 moderationService |
docker/Dockerfile.services |
加 moderationservice 构建行 |
docker/docker-compose.local.yml + docker-compose.prod.yml |
加服务定义 |
Makefile |
加 start-moderationservice 目标 |
frontend/pages/profile/profile.vue |
菜单项追加"我的举报"/"我的反馈" |
frontend/components/NftDetailModal.vue 等 |
"..."菜单接入 ReportModal |
frontend/pages/user/userProfile.vue |
"..."菜单接入 ReportModal |
C. TopFans-activity-admin 仓 — 新建文件
| 路径 | 职责 |
|---|---|
backend/models/moderation.py |
SQLAlchemy ORM |
backend/schemas/moderation.py |
Pydantic 模型 |
backend/crud/moderation_crud.py |
数据库操作 |
backend/handlers/moderation_admin.py |
/api/admin/moderation/* 路由 |
backend/handlers/test_moderation_admin.py |
FastAPI 单元测试 |
frontend/src/api/moderation.js |
axios 封装 |
frontend/src/views/moderation/ReportList.vue |
举报工单列表 |
frontend/src/views/moderation/ReportDetail.vue |
举报详情 |
frontend/src/views/moderation/FeedbackList.vue |
反馈列表 |
frontend/src/views/moderation/FeedbackDetail.vue |
反馈详情 |
frontend/src/views/moderation/ReportCategoryConfig.vue |
举报分类 CRUD |
frontend/src/views/moderation/FeedbackCategoryConfig.vue |
反馈分类 CRUD |
frontend/src/views/moderation/Dashboard.vue |
审核看板(可选) |
D. TopFans-activity-admin 仓 — 修改文件
| 路径 | 改动 |
|---|---|
backend/router/__init__.py |
注册 /api/admin/moderation/* |
frontend/src/router/index.js |
注册 /moderation/* 路由 |
frontend/src/layout/Layout.vue |
菜单注册 |
阶段 1:数据库 Schema(topfans 主仓)
Task 1.1: 创建迁移文件
Files:
- Create:
backend/migrations/2026_06_11_011_moderation_tables.sql
前置:
-
已有
2026_06_08_010_statistic_partitions_initial.sql;新文件按三位编号递增到 011 -
注意:
2026_06_16_001_create_notifications.sql已存在,本 plan 用 011 不冲突(不同日期段) -
Step 1: 写完整 SQL 迁移
按 spec §3 写入 9 张表的 CREATE TABLE + 索引 + 种子数据 + 9 个序列同步。
关键 SQL 模板(完整内容按 spec §3.2-3.10 复制,9 张表全部带 COMMENT ON TABLE 文档注释):
-- ============================================================================
-- 举报与反馈系统 (spec: docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md v2.5)
-- 9 张表 + 索引 + 种子数据 + 序列同步
-- 所有 *_at 字段均为 Unix 毫秒 (BIGINT),与 Go time.Now().UnixMilli() 一致
-- ============================================================================
-- 1. report_categories (举报分类)
CREATE SEQUENCE report_categories_id_seq START WITH 10000 OWNED BY report_categories.id;
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,
CONSTRAINT chk_report_categories_severity CHECK (severity BETWEEN 1 AND 5)
);
COMMENT ON TABLE report_categories IS '举报分类字典(动态配置,enabled=FALSE 不下发客户端)';
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, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('infringing', '侵权盗版', '侵犯他人著作权/商标权', 4, 3, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('false_info', '虚假信息', '虚假、欺骗性内容', 3, 4, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('political', '政治敏感', '涉及政治敏感话题', 5, 5, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('ad_spam', '广告骚扰', '垃圾广告、骚扰信息', 2, 6, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
('other', '其他', '其他违规情况', 1, 99, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000);
SELECT setval('report_categories_id_seq', (SELECT MAX(id) FROM report_categories));
按相同模式写 feedback_categories / reports / report_evidence / feedbacks / feedback_evidence / moderation_actions / moderation_target_status / admin_audit_logs 8 张表 + 各自序列同步(每张表末尾必须 SELECT setval — CLAUDE.md 强制)。
关键约束清单(必填):
reports.chk_reports_status含'withdrawn'/'archived'(spec §3.4 v2.4 V2 修复)moderation_actions.chk_moderation_actions_action_type含'autohide_noop'/'withdraw'/'force_release'(v2.4 V3)moderation_actions.chk_moderation_actions_admin_id含(admin_id >= 0)(v2.2 B4 系统自动 sentinel)moderation_target_status.chk_mts_last_action_type含'warn_cleared'(v2.4 V4)chk_mts_warn_fields第 3 子句支持"历史警告已清除"(v2.7 H2)uk_reports_reporter_target_pending唯一索引(防重复)
关键索引:
-
idx_reports_status_created (status, created_at DESC) -
idx_reports_triggered_autohide (target_type, target_id, created_at DESC) WHERE triggered_auto_hide=TRUE(v2.5 W2) -
idx_moderation_actions_target (target_type, target_id, created_at DESC) WHERE target_type IS NOT NULL(v1.7 H1 修复) -
idx_feedbacks_claimed_by (claimed_by, status, created_at DESC) WHERE claimed_by IS NOT NULL -
Step 2: 验证 SQL 语法
# 用 docker 起临时 PG 验证
docker run -d --name pg-verify -e POSTGRES_PASSWORD=test -p 15433:5432 postgres:16
sleep 3
PGPASSWORD=test psql -h localhost -p 15433 -U postgres -d postgres -f backend/migrations/2026_06_11_011_moderation_tables.sql
# 期望:所有 CREATE/INSERT/setval 都成功,无 CHECK 违例
docker rm -f pg-verify
- Step 3: 验证 9 个序列都同步
SELECT sequencename, last_value,
(SELECT MAX(id) FROM report_categories) AS table_max_id
FROM pg_sequences WHERE sequencename LIKE '%moderation%' OR sequencename LIKE '%reports%' OR ...;
-- 所有 last_value >= MAX(id)
- Step 4: 暂不 commit(CLAUDE.md 规定不主动 commit)
Task 1.2: 写 integration test 验证约束
Files:
-
Create:
backend/services/moderationService/repository/moderation_repository_test.go(前置创建空文件) -
Step 1: 写测试用例覆盖关键 CHECK 约束
使用 testcontainers-go 起临时 PG(参考 spec §7.4 隔离策略),加载阶段 1.1 迁移后跑:
func TestReports_WithdrawnStatus_Allowed(t *testing.T) {
// 测试 chk_reports_status 包含 'withdrawn'
db.Exec(`UPDATE reports SET status='withdrawn' WHERE id=?`, id)
// 期望:无 CHECK 违例
}
func TestReports_DuplicatePending_Blocked(t *testing.T) {
// 测试 uk_reports_reporter_target_pending 唯一索引
db.Exec(`INSERT INTO reports (...) VALUES (...)`)
db.Exec(`INSERT INTO reports (...) VALUES (...)`) // 同 reporter+target pending
// 期望:第二次 INSERT 失败 unique violation
}
func TestModerationActions_AdminIDZero_Allowed(t *testing.T) {
// 测试 chk_moderation_actions_admin_id (admin_id >= 0)
db.Exec(`INSERT INTO moderation_actions (..., admin_id=0, ...)`)
// 期望:成功(0 是 sentinel)
}
func TestMTS_WarnClearedPreservesHistory(t *testing.T) {
// 测试 chk_mts_warn_fields 第 3 子句
db.Exec(`UPDATE moderation_target_status SET is_warned=FALSE, warn_count=3, last_warned_at=$now WHERE target_id=?`)
// 期望:成功(保留 warn_count + last_warned_at)
}
- Step 2: 跑测试
cd backend/services/moderationService
go test ./repository/... -run TestReports -v
- Step 3: 暂不 commit
阶段 2:Go moderationService 微服务
Task 2.1: 创建服务目录骨架
Files:
- Create:
backend/services/moderationService/main.go等所有空文件 +go.mod - Modify:
backend/go.work
参考模式: 复制 backend/services/socialService/ 全部结构,改服务名为 moderation-service。
- Step 1: 创建目录与 go.mod
cd backend/services
cp -r socialService moderationService
cd moderationService
# 替换所有 "socialService" / "social" / "socialService" 为 "moderationService" / "moderation"
# 端口 20002 → 20010
# Dubbo 服务名 SocialService → ModerationService
- Step 2: 修改 go.work
在 backend/go.work 中追加:
./services/moderationService
- Step 3: 编译验证
cd backend/services/moderationService
go mod tidy
go build -o moderationService main.go
# 期望:编译通过
Task 2.2: 写 proto 定义
Files:
- Create:
backend/proto/moderation.proto
关键 RPC(按 spec §5.1 客户端 API):
GetReportCategories/GetFeedbackCategoriesSubmitReport(SubmitReportRequest) returns (SubmitReportResponse)ListMyReports(ListMyReportsRequest) returns (ListMyReportsResponse)GetReport(GetReportRequest) returns (GetReportResponse)SubmitFeedback / ListMyFeedbacks / GetFeedback
关键字段:
message SubmitReportRequest {
string target_type = 1; // asset | user_profile
int64 target_id = 2;
string category_code = 3;
string description = 4; // <= 500
bool is_anonymous = 5;
repeated string evidence_keys = 6; // OSS keys, <= 5
}
message SubmitReportResponse {
int64 report_id = 1;
string status = 2; // pending | auto_hidden
bool auto_hidden = 3;
bool target_hidden = 4; // 本次 auto-hide 是否执行了 UPDATE
int64 claimed_by = 5; // 新工单始终 0
int64 claimed_at = 6;
int64 created_at = 7;
}
-
Step 1: 写 proto 文件(按 spec §5.1 完整字段)
-
Step 2: 编译生成 .pb.go 和 .triple.go
cd backend
# 参考现有 proto 编译流程
make proto-gen # 或按 gen-swagger.sh 同等脚本
- Step 3: 验证生成产物
ls backend/pkg/proto/moderation/
# 期望:moderation.pb.go + moderation.triple.go 都在
Task 2.3: 写 ORM 模型
Files:
- Create:
backend/pkg/models/moderation.go
关键模型(按 spec §3 表结构):
type Report struct {
ID int64 `gorm:"primaryKey"`
ReporterID int64
StarID *int64
TargetType string // asset | user_profile
TargetID int64
TargetSnapshot datatypes.JSON
TriggeredAutoHide bool
CategoryCode string
Description string
IsAnonymous bool
Status string // pending | reviewing | auto_hidden | resolved | dismissed | withdrawn
IsAutoHidden bool
ClaimedBy *int64
ClaimedAt *int64
ResolvedAction *string
ResolvedBy *int64
ResolvedAt *int64
ResolutionNote *string
CreatedAt int64
UpdatedAt int64
}
// 同模式:ReportEvidence / Feedback / FeedbackEvidence / ModerationAction / ModerationTargetStatus / AdminAuditLog
注意: *int64 表示可空指针,对应 NULL 列。
Task 2.4: 写仓储层 + 单测
Files:
- Create:
backend/services/moderationService/repository/moderation_repository.go
关键方法:
CreateReport(ctx, tx, report) errorGetReportByID(ctx, id) (*Report, error)ClaimReport(ctx, tx, id, adminID, now) (claimed bool, error)—— 用 affected rows 判断抢锁ReleaseReport(ctx, tx, id, adminID, now) errorDismissReportPathA(ctx, tx, id, adminID, now) error—— pending → dismissed 快速通道DismissReportPathB/C/D(ctx, tx, id, adminID, now, restore bool) errorResolveReport(ctx, tx, id, action, adminID, note, now) errorListReports(ctx, filters) ([]*Report, error)BulkDismissReports(ctx, tx, ids, adminID, reason, now) (successCount int, error)ForceReleaseReport(ctx, tx, id, operatorAdminID, reason, now) error- 同模式:
feedback_repository.go
关键 SQL 模式:
// Claim - 用 affected rows 判定
res := tx.Model(&Report{}).
Where("id = ? AND status IN ?", id, []string{"pending", "auto_hidden"}).
Updates(map[string]interface{}{"status": "reviewing", "claimed_by": adminID, "claimed_at": now})
if res.Error != nil { return false, res.Error }
return res.RowsAffected == 1, nil
-
Step 1: 写仓储层
-
Step 2: 写仓储测试(testcontainers 启临时 PG)
-
Step 3: 跑测试
cd backend/services/moderationService
go test ./repository/... -v
# 期望:全部 PASS
Task 2.5: 写 Service 层
Files:
- Create:
backend/services/moderationService/service/{report,feedback,category,auto_hide,transaction_helper}_service.go - Create:
backend/services/moderationService/service/lua/auto_hide.lua
关键方法:
report_service.go — SubmitReport(spec §6.1 完整 7 步):
func (s *ReportService) SubmitReport(ctx, req) (*SubmitReportResponse, error) {
// 0. 限流:每用户/IP/device 每天 30/200/200 次
// 1. 验证分类、target_type、description、evidence
// 2. 验证目标存在(assetClient.GetAssetRPC / userClient.GetUserRPC)
// 2.5 自举报拦截:reporter_id != owner_uid(target)
// 3. 防重复:uk_reports_reporter_target_pending
// 4. 写 reports (status='pending', claimed_by/claimed_at=NULL)
// 5. 写 report_evidence
// 6. Redis Lua 计数 → 触发自动隐藏
// → 事务内:业务表软删除 + reports UPDATE auto_hidden + mts UPSERT + moderation_actions 流水
// 7. 返回 SubmitReportResponse
}
auto_hide_service.go — Lua 脚本(spec §6.4):
-- KEYS[1]=counter, KEYS[2]=user_marker
-- 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}
使用 //go:embed 加载:
//go:embed lua/auto_hide.lua
var autoHideLuaScript string
auto_hide_service.go — 锁管理(spec §6.4):
func (s *AutoHideService) tryAutoHide(ctx, targetType, targetID, triggerReportID) error {
ok, _ := s.redis.SetNX(ctx, "mod:report:lock:"+targetType+":"+targetID, "1", 5*time.Second)
if !ok { return nil } // 5s 内已有并发请求
defer s.redis.Del(ctx, "mod:report:lock:"+targetType+":"+targetID)
triggered, count := s.redis.Eval(ctx, autoHideLuaScript, ...).Result()
if triggered.(int64) != 1 { return nil }
// 事务内:业务表软删除 + reports UPDATE + mts UPSERT + moderation_actions 流水
return s.txHelper.WithTx(ctx, func(tx *gorm.DB) error {
// ① 业务表软删除(按 target_type 路由)
var affected int64
if targetType == "asset" {
tx.Model(&models.Asset{}).Where("id = ? AND is_active = true", targetID).
Updates(map[string]interface{}{"is_active": false, "deleted_at": now})
affected = tx.RowsAffected
} else { ... }
// ② reports UPDATE pending → auto_hidden
tx.Model(&models.Report{}).
Where("target_type = ? AND target_id = ? AND status = 'pending'", targetType, targetID).
Updates(map[string]interface{}{
"status": "auto_hidden",
"is_auto_hidden": true,
"triggered_auto_hide": gorm.Expr("(id = ?)", triggerReportID),
})
// ③ mts UPSERT
tx.Clauses(clause.OnConflict{...}).Create(&models.ModerationTargetStatus{...})
// ③.5 写 moderation_actions 流水
actionType := "takedown"
if affected == 0 { actionType = "autohide_noop" }
tx.Create(&models.ModerationAction{
ReportID: triggerReportID, AdminID: 0, ActionType: actionType,
TargetType: targetType, TargetID: targetID,
Note: "auto-hide threshold reached", Success: true, CreatedAt: now,
})
return nil
})
}
feedback_service.go — 状态机 + claimed_* 一致性(spec §6.3):
SubmitFeedback/ListMyFeedbacks/GetFeedbackClaimFeedback(认领) /ReleaseFeedback(释放,清 claimed_*)ReplyFeedback(终态,清 claimed_) /CloseFeedback(终态,清 claimed_) /ArchiveFeedback(终态,清 claimed_*)
关键一致性(spec §4.2 chk_feedbacks_claimed_pair):所有终态迁移必须同时清 claimed_by=NULL, claimed_at=NULL。
- Step 1: 写 transaction_helper.go
func (h *TxHelper) WithTx(ctx context.Context, fn func(*gorm.DB) error) error {
return h.db.WithContext(ctx).Transaction(fn)
}
-
Step 2: 写 report_service.go
-
Step 3: 写 feedback_service.go
-
Step 4: 写 auto_hide_service.go + Lua 脚本
-
Step 5: 写 category_service.go
-
Step 6: 跑 service 测试
cd backend/services/moderationService
go test ./service/... -v
# 期望:全部 PASS(claimed_* 一致性测试、Lua 测试、并发测试都覆盖)
Task 2.6: 写 Client 层(RPC 客户端)
Files:
- Create:
backend/services/moderationService/client/{asset,user,notification,redis}_client.go
关键实现:
asset_client.go— Dubbo 调用assetService.GetAssetRPC验证目标存在user_client.go— Dubbo 调用userService.GetUserRPCnotification_client.go— Dubbo 调用notificationService.SendNotification(阶段 7.1 替换 stub 为真实现)redis_client.go— 初始化 Redis 客户端(参考aiChatService/main.go)
参考: backend/services/socialService/client/user_rpc_client.go
Task 2.7: 写 Provider 层(Dubbo 注册)
Files:
- Create:
backend/services/moderationService/provider/moderation_provider.go
关键实现(参考 social_provider.go):
type ModerationProvider struct {
pb.UnimplementedModerationServiceServer
reportSvc *service.ReportService
feedbackSvc *service.FeedbackService
categorySvc *service.CategoryService
}
func (p *ModerationProvider) SubmitReport(ctx, req) (*pb.SubmitReportResponse, error) {
return p.reportSvc.SubmitReport(ctx, req)
}
// ... 其他 RPC 方法映射
-
Step 1: 写 provider
-
Step 2: 在 main.go 注册 provider
Task 2.8: 完善 main.go 与启动脚本
Files:
- Modify:
backend/services/moderationService/main.go - Create:
backend/services/moderationService/start.sh - Create:
backend/services/moderationService/configs/dubbo.yaml
main.go — 参考 socialService/main.go,加:
- 端口 20010
notificationServiceURL=tri://localhost:20008(不指定 moderationService 自指)DUBBO_USER_SERVICE_URL/DUBBO_ASSET_SERVICE_URL/DUBBO_NOTIFICATION_SERVICE_URL- 注册 Prometheus
/metrics - 注册 13+ 业务指标(spec §11.3)
start.sh — spec §10 阶段 2.1 模板(PORT/DB/REDIS/DUBBO 全 env)
-
Step 1: 写 main.go + start.sh + dubbo.yaml
-
Step 2: 编译验证
cd backend/services/moderationService
./start.sh # 期望:build + start 成功,监听 20010
- Step 3: metrics 端点验证
curl http://localhost:20010/metrics | head -20
# 期望:promhttp 输出 + 自定义指标
Task 2.9: Gateway 集成(HTTP 路由 + 客户端)
Files:
- Create:
backend/gateway/{controller,client,dto}/moderation_*.go - Modify:
backend/gateway/router/router.go
关键实现(参考 backend/gateway/controller/social_controller.go):
moderation_controller.go:
type ModerationController struct {
client *moderationClient.Client
}
func NewModerationController(cli *client.Client) (*ModerationController, error) {
moderationCli, err := moderationClient.NewModerationClient(cli)
if err != nil { return nil, err }
return &ModerationController{client: moderationCli}, nil
}
func (c *ModerationController) SubmitReport(ctx *gin.Context) {
var req dto.SubmitReportRequestDTO
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(400, dto.ErrorResponse{Code: 50005, Message: "描述过长(>500 字)"})
return
}
resp, err := c.client.SubmitReport(ctx, converter.ToRPCSubmitReport(req))
if err != nil { /* 错误码映射 */ }
ctx.JSON(200, converter.FromRPCSubmitReport(resp))
}
router.go 新增(spec §阶段 2.10):
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)
-
Step 1: 写 controller + dto + converter + client
-
Step 2: 在 router.go 注册路由
-
Step 3: 启动 gateway,curl 验证
curl -X POST http://localhost:8080/api/v1/moderation/reports \
-H "Authorization: Bearer $TOKEN" \
-d '{"target_type":"asset","target_id":12345,"category_code":"pornographic","description":"test","is_anonymous":false,"evidence_keys":[]}'
# 期望:返回 report_id
Task 2.10: 完善 main.go / docker / Makefile
Files:
- Modify:
docker/Dockerfile.services/docker/docker-compose.local.yml/docker/docker-compose.prod.yml/Makefile
关键改动:
- Dockerfile 加
moderationservice构建阶段 - docker-compose 加服务定义(端口 20010、env)
- Makefile 加
start-moderationservicetarget
阶段 3:TopFans-activity-admin 后台 FastAPI
Task 3.1: 创建模型 / Schema / CRUD
Files:
- Create:
backend/models/moderation.py - Create:
backend/schemas/moderation.py - Create:
backend/crud/moderation_crud.py
关键模型(9 张表对应 SQLAlchemy ORM,按 spec §3 表结构):
class Report(Base):
__tablename__ = "reports"
id = Column(BigInteger, primary_key=True)
reporter_id = Column(BigInteger, nullable=False)
star_id = Column(BigInteger)
target_type = Column(String(30), nullable=False)
target_id = Column(BigInteger, nullable=False)
target_snapshot = Column(JSON, nullable=False)
triggered_auto_hide = Column(Boolean, default=False)
# ... 其他字段
关键 CRUD 函数:
-
list_reports(filters, page) -> (rows, total) -
get_report_detail(id) -> Report -
claim_report(id, admin_id, now) -> bool(用 affected rows 抢锁) -
release_report(id, admin_id, now) -
execute_action(id, action, admin_id, note, now)—— 4 条 dismiss 路径 + takedown/ban/warn/restore -
bulk_dismiss_reports(ids, admin_id, reason, now) -> int -
force_release_report(id, operator_admin_id, reason, now) -
clear_warn(user_id, admin_id, reason, now)—— warn_cleared 流程 -
同模式:
feedback_crud.py -
Step 1: 写 ORM
-
Step 2: 写 Pydantic Schema
-
Step 3: 写 CRUD 函数
Task 3.2: 写 Handler(FastAPI 路由)
Files:
- Create:
backend/handlers/moderation_admin.py
关键端点(spec §5.2):
GET /api/admin/moderation/reports—— 列表(filter: 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—— 审核动作POST /api/admin/moderation/reports/bulk-dismiss—— 批量驳回POST /api/admin/moderation/reports/{id}/force-release—— 强制释放POST /api/admin/moderation/users/{id}/clear-warn—— 清除警告GET/POST/PUT/DELETE /api/admin/moderation/report-categories—— 分类 CRUD- 同模式:feedback endpoints
GET /api/admin/moderation/stats—— 看板
关键依赖:
Depends(verify_token)—— 复用现有 JWT 校验- 解析 token →
(admin_id, role, exp),admin_id写入moderation_actions.admin_id/admin_audit_logs.admin_id
关键错误处理(50008 vs 50009 vs 50014 区分 — spec §7):
# claim 0 行匹配时二次 SELECT 查 status
result = await db.execute(update(Report).where(Report.id == id, Report.status.in_(['pending','auto_hidden'])).values(status='reviewing', ...))
if result.rowcount == 0:
cur = await db.execute(select(Report).where(Report.id == id))
row = cur.scalar_one_or_none()
if row is None:
raise HTTPException(404, detail={"code": 50007, "message": "工单不存在"})
if row.status == 'reviewing' and row.claimed_by != admin_id:
raise HTTPException(409, detail={"code": 50008, "message": "工单已被他人认领", "claimed_by": row.claimed_by, "claimed_at": row.claimed_at})
if row.status in ['resolved','dismissed']:
raise HTTPException(400, detail={"code": 50009, "message": "工单已结案"})
-
Step 1: 写 handlers
-
Step 2: 在 router/init.py 注册
from .moderation_admin import router as moderation_admin_router
admin_router.include_router(moderation_admin_router)
- Step 3: 启动 FastAPI 验证
cd TopFans-activity-admin
uvicorn backend.main:app --reload
curl http://localhost:8000/api/admin/moderation/reports -H "Authorization: Bearer $ADMIN_TOKEN"
# 期望:返回分页列表
Task 3.3: 写单元测试
Files:
- Create:
backend/handlers/test_moderation_admin.py
关键测试(spec §7.3):
-
列表/详情/认领/动作 happy path
-
dismiss 4 路径分支覆盖
-
鉴权测试(无 token → 401)
-
50008 vs 50009 区分
-
Fixture:
httpx.AsyncClient+ pytest;admin token 用现有make_test_token() -
Step 1: 写测试
-
Step 2: 跑测试
cd TopFans-activity-admin
pytest backend/handlers/test_moderation_admin.py -v
阶段 4:后台前端 Vue3
Task 4.1: API 封装
Files:
- Create:
frontend/src/api/moderation.js
关键函数:
import request from '@/utils/request'
export function getReports(params) { return request({ url: '/api/admin/moderation/reports', method: 'get', params }) }
export function getReportDetail(id) { return request({ url: `/api/admin/moderation/reports/${id}`, method: 'get' }) }
export function claimReport(id) { return request({ url: `/api/admin/moderation/reports/${id}/claim`, method: 'post' }) }
export function releaseReport(id) { return request({ url: `/api/admin/moderation/reports/${id}/release`, method: 'post' }) }
export function executeAction(id, payload) { return request({ url: `/api/admin/moderation/reports/${id}/actions`, method: 'post', data: payload }) }
export function bulkDismissReports(ids, reason) { return request({ url: '/api/admin/moderation/reports/bulk-dismiss', method: 'post', data: { report_ids: ids, reason } }) }
export function forceReleaseReport(id, reason) { return request({ url: `/api/admin/moderation/reports/${id}/force-release`, method: 'post', data: { reason } }) }
// 同模式:feedback API + categories API
注意:复用 utils/request.js 的 axios 拦截器(已含 JWT 注入)。
Task 4.2: 举报工单列表页
Files:
- Create:
frontend/src/views/moderation/ReportList.vue
关键 UI(Element Plus):
- 筛选栏:状态 select + 分类 select + 目标类型 select + 关键词 input + 搜索按钮
- el-table 列表:report_id / target_type / target_id / category_code / status (el-tag) / claimed_by / created_at
- 分页:el-pagination
- 行操作:详情按钮(跳转 detail 页)
Task 4.3: 举报详情页 + 审核动作
Files:
- Create:
frontend/src/views/moderation/ReportDetail.vue
关键 UI:
- 顶部信息卡片:report_id / status / target_snapshot (JSON 展示) / 举报人 / 分类 / 描述
- 证据图:el-image preview
- 流水时间线:el-timeline(moderation_actions 列表)
- 动作按钮组(按 status 显隐):
pending/auto_hidden:认领reviewing(本人认领):下架/封禁/警告/驳回/释放- 任何状态:详情查看
关键交互:
- "认领"按钮 →
claimReport→ 成功后切换到 reviewing 模式,按钮组切换 - "执行动作" → 弹 el-dialog 输入 action/note/send_notification →
executeAction - 4 条 dismiss 路径在 UI 上对应不同选项("驳回(不恢复)"/"驳回(恢复隐藏对象)" 等)
Task 4.4: 反馈列表/详情/分类管理页面
Files:
- Create:
frontend/src/views/moderation/{FeedbackList,FeedbackDetail,ReportCategoryConfig,FeedbackCategoryConfig,Dashboard}.vue
按相同模式实现。Dashboard 可选(spec §4 阶段 4.2 标记 optional)。
Task 4.5: 路由注册 + 菜单
Files:
- Modify:
frontend/src/router/index.js - Modify:
frontend/src/layout/Layout.vue
router:
{
path: '/moderation',
component: Layout,
children: [
{ path: 'reports', component: () => import('@/views/moderation/ReportList.vue'), meta: { title: '举报工单' } },
{ path: 'reports/:id', component: () => import('@/views/moderation/ReportDetail.vue'), meta: { title: '举报详情' }, hidden: true },
{ path: 'feedbacks', component: () => import('@/views/moderation/FeedbackList.vue'), meta: { title: '反馈工单' } },
{ path: 'feedbacks/:id', component: () => import('@/views/moderation/FeedbackDetail.vue'), meta: { title: '反馈详情' }, hidden: true },
{ path: 'categories/report', component: () => import('@/views/moderation/ReportCategoryConfig.vue'), meta: { title: '举报分类' } },
{ path: 'categories/feedback', component: () => import('@/views/moderation/FeedbackCategoryConfig.vue'), meta: { title: '反馈分类' } },
]
}
阶段 5:客户端 uni-app
Task 5.1: API 方法追加
Files:
- Modify:
frontend/utils/api.js(末尾追加 8 个方法,不新建文件)
export function getReportCategoriesApi() { return uni.request({ url: '/api/v1/moderation/report-categories', method: 'GET' }) }
export function submitReportApi(payload) { return uni.request({ url: '/api/v1/moderation/reports', method: 'POST', data: payload }) }
export function getMyReportsApi(params) { return uni.request({ url: '/api/v1/moderation/reports', method: 'GET', data: params }) }
export function getMyReportDetailApi(id) { return uni.request({ url: `/api/v1/moderation/reports/${id}`, method: 'GET' }) }
export function getFeedbackCategoriesApi() { return uni.request({ url: '/api/v1/moderation/feedback-categories', method: 'GET' }) }
export function submitFeedbackApi(payload) { return uni.request({ url: '/api/v1/moderation/feedbacks', method: 'POST', data: payload }) }
export function getMyFeedbacksApi(params) { return uni.request({ url: '/api/v1/moderation/feedbacks', method: 'GET', data: params }) }
export function getMyFeedbackDetailApi(id) { return uni.request({ url: `/api/v1/moderation/feedbacks/${id}`, method: 'GET' }) }
Task 5.2: ReportModal 通用举报弹窗
Files:
- Create:
frontend/components/ReportModal.vue
关键实现(uView 2.x):
- props:
targetType: 'asset' | 'user_profile',targetId: Number,targetName: String - 表单:u-form 包含分类 radio + 描述 textarea + u-upload (max 5) + 匿名 switch
- 提交:
- 调用
submitReportApi - 双分支 toast:
res.data.target_hidden === true→uni.showToast({ title: '已自动隐藏,等待审核', icon: 'none' })- 否则 →
uni.showToast({ title: '举报已提交', icon: 'success' })
- 调用
- 错误码处理(50004/50011/50012 等):用 u-toast 显示 message
Task 5.3: 接入到藏品详情 / 用户主页
Files:
-
Modify:
frontend/components/NftDetailModal.vue等藏品详情组件 -
Modify:
frontend/pages/user/userProfile.vue -
在 "..." 菜单加入"举报"选项,点击弹出
ReportModal
Task 5.4: 意见反馈页 + 我的举报/反馈列表
Files:
- Create:
frontend/pages/profile/feedback.vue - Create:
frontend/pages/profile/myReports.vue - Create:
frontend/pages/profile/myFeedbacks.vue - Modify:
frontend/pages/profile/profile.vue(菜单追加)
阶段 7:通知模板 + 单元测试
Task 7.1: 通知模板注册
Files:
- Modify:
backend/services/notificationService/model/notification.go或notification_repository.go(按现有模板管理方式)
6 个模板(spec §8):
report_auto_hidden_notice— 被举报方"内容已被自动隐藏"report_resolved— 举报人"举报已处理"report_takedown_notice— 被举报方"内容已被下架"report_ban_notice— 被举报用户"账号已被封禁"report_warn_notice— 被举报用户"已被警告"feedback_replied— 反馈人"反馈已回复"
模板字段:{reporter_nickname, target_type, target_id, action, reason, related_url}
-
Step 1: 写 6 个模板到 notificationService
-
Step 2: 在 moderationService
client/notification_client.go替换 v1.5 stub 为真实现
Task 7.2: Go 单元测试
Files:
- Create/Modify:
backend/services/moderationService/service/{report,feedback,auto_hide,concurrency}_test.go
关键测试覆盖(spec §7.2):
-
report_service_test.go:防重复、自动隐藏触发、dismiss 4 路径、claimed_* 一致性 -
feedback_service_test.go:状态机 + claimed_* 一致性 -
auto_hide_service_test.go:Lua 脚本逻辑 -
concurrency_test.go:- 100 用户同时举报同对象,counter INCR 但只触发 1 次自动隐藏
- 2 个审核员同时 claim 同 report,1 成功 1 返回 50008
- 自动隐藏 Lua + 5s 短锁交互(errgroup)
-
Step 1: 写所有测试
-
Step 2: 跑测试
cd backend/services/moderationService
go test ./... -v -race
# 期望:全部 PASS
Task 7.3: 集成测试(testcontainers)
Files:
- Create:
backend/services/moderationService/integration/
关键场景(spec §7.4):
- 提交举报 → 后台审核 → 状态变更 → 通知
- 自动隐藏:5 个独立用户举报 → 自动隐藏 + 状态表写入
- dismiss 4 路径覆盖
- 跨服务事务回滚:业务表软删除成功但 mts UPSERT 失败,验证整体回滚
隔离策略:testcontainers-go 启临时 PG 16 + Redis 7,业务表自动跑阶段 1.1 迁移,Redis key 加 test:mod:report:* 前缀。
自审检查清单
完成后逐项验证:
- 所有手动 INSERT 都带
SELECT setval - 9 个序列
last_value >= MAX(id) chk_reports_status含'withdrawn'/'archived'chk_moderation_actions_action_type含'autohide_noop'/'withdraw'/'force_release'chk_mts_last_action_type含'warn_cleared'uk_reports_reporter_target_pending唯一索引存在- 所有终态迁移清
claimed_by=NULL, claimed_at=NULL - OSS 白名单扩为 4 个值(avatar/asset/report/feedback)
asset_controller.goInitMintOrder按 type 分支- 错误码 50008 vs 50009 vs 50014 在 handler 区分
- Lua 脚本并发 + 锁测试通过
- 6 个通知模板已注册
- Gateway 路由仅
/api/v1/moderation/*(后台/api/admin/*由 FastAPI 处理)
执行选项
Plan complete and saved to docs/superpowers/plans/2026-06-17-moderation-report-feedback-system.md.
由于本计划跨 2 个仓库、5 种技术栈、11 阶段,建议按以下两种方式之一执行:
选项 1: 子计划拆分执行(推荐) 按仓库/技术栈拆分为 4 个独立执行计划:
- Plan A (本仓 DB + Go service + Gateway):阶段 1, 2, 2.10
- Plan B (TopFans-activity-admin 后台 + 前端):阶段 3, 4
- Plan C (本仓 uni-app 客户端):阶段 5
- Plan D (通知 + 测试):阶段 7
每个 Plan 独立由 subagent-driven-development 派发执行。
选项 2: 整体 Inline 执行 按本计划 11 阶段顺序执行(in this session with checkpoints)。
请选择执行方式。