238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# Plan D: 通知模板 + 单元测试 + 集成测试
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**父计划:** `docs/superpowers/plans/2026-06-17-moderation-report-feedback-system.md`
|
||
|
||
**Goal:** 注册 6 个通知模板 + 单元测试 + 集成测试 + 监控指标
|
||
|
||
**Architecture:**
|
||
- 6 个模板注册到 notificationService(替换 v1.5 stub 为真实现)
|
||
- Go 单元测试覆盖状态机 + claimed_* 一致性 + Lua + 并发
|
||
- FastAPI 单元测试覆盖 50008/50009/50014 区分
|
||
- 集成测试(testcontainers):端到端场景
|
||
- Prometheus 13+ 业务指标
|
||
|
||
**Tech Stack:** Go test / testify / testcontainers-go / pytest / pytest-asyncio / prometheus/client_golang
|
||
|
||
**仓库:** 双仓并行
|
||
- `/Users/liulujian/Documents/code/TopFansByGithub`(Go service + Gateway + 集成测试)
|
||
- `/Users/liulujian/Documents/code/TopFans-activity-admin`(FastAPI 测试)
|
||
|
||
---
|
||
|
||
## 阶段 D.1:通知模板注册
|
||
|
||
### Task D.1.1: 在 notificationService 注册 6 个模板
|
||
|
||
**Files:**
|
||
- Modify: `backend/services/notificationService/model/notification.go` 或 `repository/notification_repository.go`(按现有模板管理方式)
|
||
|
||
**6 个模板**(spec §8):
|
||
|
||
| 事件 | 模板名 | 接收方 | 字段 |
|
||
|------|--------|--------|------|
|
||
| 举报达到阈值触发自动隐藏 | `report_auto_hidden_notice` | 被举报方 | reporter_nickname, target_type, target_id, action, reason, related_url |
|
||
| 举报状态变更为 resolved/dismissed | `report_resolved` | 举报人 | 同上 |
|
||
| 举报成立 + takedown | `report_takedown_notice` | 被举报方 | 同上 |
|
||
| 举报成立 + ban | `report_ban_notice` | 被举报用户 | 同上 + 短信 |
|
||
| 举报成立 + warn | `report_warn_notice` | 被举报用户 | 同上 |
|
||
| 反馈被回复 | `feedback_replied` | 反馈人 | 同上 |
|
||
|
||
```sql
|
||
INSERT INTO notification_templates (code, name, channel, subject, body_template, enabled) VALUES
|
||
('report_auto_hidden_notice', '举报自动隐藏通知', 'site_msg',
|
||
'您的内容被多人举报已自动隐藏',
|
||
'您发布的{target_type}(ID: {target_id})已被系统自动隐藏。如有异议请联系客服。',
|
||
TRUE),
|
||
('report_resolved', '举报处理结果', 'site_msg+push',
|
||
'您的举报已处理',
|
||
'您的举报已处理:{action}。{reason}',
|
||
TRUE),
|
||
('report_takedown_notice', '内容下架通知', 'site_msg',
|
||
'您的内容已被下架',
|
||
'您发布的{target_type}(ID: {target_id})因{reason}已被下架。',
|
||
TRUE),
|
||
('report_ban_notice', '账号封禁通知', 'site_msg+sms',
|
||
'您的账号已被封禁',
|
||
'您的账号因{reason}已被封禁,如有异议请联系客服。',
|
||
TRUE),
|
||
('report_warn_notice', '账号警告通知', 'site_msg',
|
||
'您的账号已被警告',
|
||
'您的账号因{reason}已被警告,请遵守社区规范。',
|
||
TRUE),
|
||
('feedback_replied', '反馈已回复', 'site_msg+push',
|
||
'您的反馈已回复',
|
||
'您的反馈"{title}"已被回复:{reply_content}',
|
||
TRUE);
|
||
```
|
||
|
||
### Task D.1.2: 替换 moderationService notification_client.go stub
|
||
|
||
**Files:**
|
||
- Modify: `backend/services/moderationService/client/notification_client.go`
|
||
|
||
将 v1.5 stub 替换为真实 Dubbo 调用 `notificationService.SendNotification`。
|
||
|
||
```go
|
||
func (c *NotificationClient) SendReportAutoHidden(ctx context.Context, userID int64, targetType string, targetID int64) error {
|
||
_, err := c.cli.SendNotification(ctx, &pbNotification.SendNotificationRequest{
|
||
UserID: userID,
|
||
Template: "report_auto_hidden_notice",
|
||
Vars: map[string]string{
|
||
"target_type": targetType,
|
||
"target_id": strconv.FormatInt(targetID, 10),
|
||
},
|
||
})
|
||
if err != nil {
|
||
logger.Sugar.Warnw("send notification failed", "template", "report_auto_hidden_notice", "err", err)
|
||
// 通知失败不影响主流程(spec §11.2 "仅日志,不影响主流程")
|
||
}
|
||
return err
|
||
}
|
||
// 同模式:SendReportResolved / SendReportTakedownNotice / SendReportBanNotice / SendReportWarnNotice / SendFeedbackReplied
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段 D.2:Go 单元测试
|
||
|
||
### Task D.2.1: report_service_test.go
|
||
|
||
**Files:**
|
||
- Create: `backend/services/moderationService/service/report_service_test.go`
|
||
|
||
**关键测试用例**(spec §7.2):
|
||
- `TestSubmitReport_Duplicate` —— 同 reporter+target+type 第二次提交返回 50004
|
||
- `TestSubmitReport_SelfReport` —— reporter_id == owner_uid 返回 50011
|
||
- `TestSubmitReport_AutoHideTrigger` —— 5 个独立用户举报 → status='auto_hidden'
|
||
- `TestClaimReport_Concurrent` —— 2 admin 同时 claim → 1 成功 1 返回 50008
|
||
- `TestReleaseReport_OnlyOwner` —— 非 claimed_by admin 释放返回 0 行
|
||
- `TestDismissPathA_ClearsClaimedBy` —— pending 状态必 claimed_by=NULL(chk_reports_claimed_pair)
|
||
- `TestDismissPathB_DismissesFromReviewing` —— reviewing → dismissed
|
||
- `TestDismissPathC_RestoresAsset` —— auto_hidden → reviewing → dismissed(restore=true) 恢复业务表
|
||
- `TestDismissPathD_PreservesHidden` —— auto_hidden → reviewing → dismissed(restore=false) 不恢复
|
||
- `TestResolveReport_Takedown` —— takedown 业务表软删除 + mts UPSERT
|
||
- `TestWithdrawReport_OnlyPending` —— 非 pending 撤回返回 0 行 → 50019
|
||
- `TestBulkDismiss_MultipleIds` —— 批量驳回返回 affected 数
|
||
- `TestForceRelease_AnyAdmin` —— 任何 admin 可强制释放他人认领
|
||
- `TestClearWarn_PreservesHistory` —— warn_cleared 保留 warn_count + last_warned_at(v1.7 H2)
|
||
- `TestAutoHideNoop_WhenAssetAlreadyInactive` —— 业务表已 inactive 时记 autohide_noop
|
||
|
||
### Task D.2.2: feedback_service_test.go
|
||
|
||
- `TestClaimFeedback_Concurrent`
|
||
- `TestReplyFeedback_ClearsClaimedBy` —— replied 终态清 claimed_by/claimed_at(chk_feedbacks_claimed_pair)
|
||
- `TestCloseFeedback_OnlyFromReviewing` —— 仅从 reviewing 入口
|
||
- `TestArchiveFeedback_NotFromReplied` —— replied 状态 archive 返回 0 行(v2.0 CRITICAL)
|
||
|
||
### Task D.2.3: auto_hide_service_test.go
|
||
|
||
- `TestLuaScript_FirstIncrement` —— 首次 INCR 返回 {0, 1}
|
||
- `TestLuaScript_DuplicateUser` —— 同用户第二次 SETNX 失败返回 {0, 1} 不再 INCR
|
||
- `TestLuaScript_ThresholdReached` —— 第 N 次 INCR 返回 {1, N}
|
||
- `TestLuaScript_ExpireOnFirst` —— n=1 时设置 EXPIRE
|
||
|
||
### Task D.2.4: concurrency_test.go
|
||
|
||
- `TestConcurrentReports_AutoHide` —— 100 用户同时举报同对象,counter INCR 100 次但只触发 1 次自动隐藏
|
||
- `TestConcurrentClaim_OnlyOneWins` —— 2 admin 同时 claim,1 成功 1 返回 50008
|
||
- `TestLockTTL_BypassAfterExpiry` —— 5s 锁过期后新请求可重入
|
||
- `TestLuaWithLock_Interaction` —— errgroup 模拟锁 + Lua 交互
|
||
|
||
### Task D.2.5: 跑测试
|
||
|
||
```bash
|
||
cd backend/services/moderationService
|
||
go test ./service/... -v -race
|
||
# 期望:全部 PASS
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段 D.3:FastAPI 单元测试
|
||
|
||
### Task D.3.1: test_moderation_admin.py
|
||
|
||
**Files:**
|
||
- Modify: `TopFans-activity-admin/backend/handlers/test_moderation_admin.py`
|
||
|
||
**关键测试**:
|
||
- `test_list_reports` —— happy path + 筛选
|
||
- `test_claim_report_already_claimed` —— 返回 50008(响应体含 claimed_by/claimed_at)
|
||
- `test_claim_report_resolved` —— 返回 50009
|
||
- `test_claim_report_not_found` —— 返回 50007
|
||
- `test_execute_action_takedown` —— 业务表 + mts 双写
|
||
- `test_execute_action_dismiss_path_a` —— pending → dismissed 快速通道
|
||
- `test_execute_action_dismiss_path_c_restore` —— 业务表恢复 + Redis 计数清零
|
||
- `test_execute_action_warn_no_soft_delete` —— warn 不软删除 user
|
||
- `test_execute_action_restore_cte` —— restore 用 CTE 条件写 mts(v2.5 W6)
|
||
- `test_bulk_dismiss` —— 批量
|
||
- `test_force_release` —— 强制释放他人认领
|
||
- `test_clear_warn_preserves_history` —— 保留 warn_count + last_warned_at
|
||
- `test_unauthorized` —— 无 token → 401
|
||
- Fixture:`httpx.AsyncClient` + pytest;admin token 用现有 `make_test_token()`
|
||
|
||
---
|
||
|
||
## 阶段 D.4:集成测试
|
||
|
||
### Task D.4.1: testcontainers 集成测试
|
||
|
||
**Files:**
|
||
- Create: `backend/services/moderationService/integration/`
|
||
|
||
**隔离策略**(spec §7.4):
|
||
- testcontainers-go 启临时 PG 16 + Redis 7
|
||
- 业务表自动跑阶段 A.1 迁移
|
||
- Redis key 加 `test:mod:report:*` 前缀
|
||
|
||
**端到端场景**:
|
||
- 提交举报 → 后台审核 → 状态变更 → 通知
|
||
- 自动隐藏:5 个独立用户举报 → 自动隐藏 + 状态表写入
|
||
- dismiss 4 路径覆盖
|
||
- 跨服务事务回滚:业务表软删除成功但 mts UPSERT 失败 → 整体回滚
|
||
- Lua 脚本测试:SETNX/INCR/EXPIRE 边界
|
||
|
||
---
|
||
|
||
## 阶段 D.5:监控指标
|
||
|
||
### Task D.5.1: 在 moderationService 注册 13+ Prometheus 指标
|
||
|
||
**Files:**
|
||
- Modify: `backend/services/moderationService/main.go`
|
||
|
||
按 spec §11.3 注册指标:
|
||
- `moderation.report.submitted.count (counter, tags: target_type, category_code)`
|
||
- `moderation.report.auto_hidden.count (counter)`
|
||
- `moderation.report.auto_hidden.noop.count (counter)` (v2.3)
|
||
- `moderation.report.resolve.duration (histogram)`
|
||
- `moderation.feedback.reply.duration (histogram)`
|
||
- `moderation.feedback.terminal_state.claim_attempt.count (counter, tag: status)` (v2.3)
|
||
- `moderation.actions.failed.count (counter, tags: action_type, error)`
|
||
- `moderation.actions.auto_hide.count (counter, tag: target_type)` (v2.3)
|
||
- `moderation.actions.warn_cleared.count (counter)` (v2.3)
|
||
- `moderation.report.duplicate.count (counter, tag: target_type)`
|
||
- `moderation.auto_hide.lock.contention.count (counter)`
|
||
- `moderation.notification.send_failed.count (counter, tag: template)`
|
||
- `moderation.transaction.rollback.count (counter, tag: flow)`
|
||
- `moderation.api.latency (histogram, tag: endpoint, status)`
|
||
- `moderation.dashboard.queue.pending (gauge)`
|
||
- `moderation.dashboard.queue.auto_hidden (gauge)`
|
||
- `moderation.redis.counter_reset.count (counter, tag: path=C|D)` (v2.3)
|
||
- `moderation.rate_limit.hit.count (counter, tag: type, scope)` (v2.3)
|
||
- `moderation.api.latency.evidence_upload.p95 (histogram)` (v2.3)
|
||
|
||
---
|
||
|
||
## 自审检查清单(Plan D)
|
||
|
||
- [ ] 6 个通知模板已注册到 notificationService
|
||
- [ ] moderationService notification_client.go stub 替换为真实现
|
||
- [ ] Go 单元测试全部 PASS(含 claimed_* 一致性、Lua、并发)
|
||
- [ ] FastAPI 单元测试全部 PASS(含 50008/50009/50014 区分)
|
||
- [ ] 集成测试(testcontainers)通过
|
||
- [ ] 13+ Prometheus 指标注册
|
||
- [ ] 监控指标覆盖业务成功路径 + 失败/异常 + 延迟/SLO
|