diff --git a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md index b1933036..69ed4771 100644 --- a/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md +++ b/docs/technical/BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md @@ -671,70 +671,326 @@ BarkBattleService - `rate_limited` - `impossible_bark_count` -## 9. TDD / 验收顺序与命令 +## 9. BDD 行为场景、TDD 落地顺序与验收命令 -本任务只写方案,不执行代码实现。后续落地建议按以下顺序: +本任务只写方案,不执行代码实现。后续落地必须先用 BDD 锁定可观察行为,再按 TDD 做 RED-GREEN-REFACTOR。没有先失败的测试,不进入生产代码实现。 -### 9.1 domain 纯函数测试 +### 9.1 BDD 场景清单 -先实现 `module-bark-battle`: +以下场景用于约束后端行为,场景标题应映射到后续 Rust 测试名、API 测试名或 smoke 用例名。 + +#### 功能: bark-battle 发布态作品运行配置 + +```gherkin +功能: bark-battle 发布态作品运行配置 + 为了让玩家进入稳定的声控狗叫对战作品 + 作为已发布作品的玩家 + 我希望后端只返回发布态且版本一致的运行配置 + + 背景: + 假如平台中存在 playTypeId 为 "bark-battle" 的作品配置 + + 场景: 玩家请求已发布作品的 runtime config + 假如作品处于 published 状态 + 而且请求携带的 configVersion 与当前发布版本一致 + 当玩家请求 bark-battle runtime config + 那么后端应返回 BarkBattleRuntimeConfig + 而且响应中的 workId 应为稳定作品 ID + 而且响应中的 playTypeId 应为 "bark-battle" + 而且响应不应包含草稿态配置或创作者私有字段 + + 场景: 玩家请求未发布作品的 runtime config + 假如作品处于 draft 状态 + 当玩家请求 bark-battle runtime config + 那么后端应返回不可游玩的错误 + 而且不应创建 runtime run + + 场景: 玩家携带过期配置版本进入 runtime + 假如作品已发布 configVersion 为 3 的版本 + 当玩家使用 configVersion 为 2 的请求开始 runtime + 那么后端应拒绝开始本局或返回最新版本提示 + 而且不应写入正式成绩 +``` + +#### 功能: bark-battle runtime start 与统一游玩埋点 + +```gherkin +功能: bark-battle runtime start 与统一游玩埋点 + 为了统计正式作品游玩行为 + 作为数据分析人员 + 我希望玩家开始 bark-battle 正式游玩时写入统一 work_play_start 事件 + + 场景: 玩家成功开始已发布作品运行态 + 假如存在一个已发布的 bark-battle 作品 + 而且玩家有权限游玩该作品 + 当玩家调用 start runtime + 那么后端应创建状态为 started 的 run + 而且应返回 runId、一次性 runToken 与 runtimeConfig + 而且应写入 work_play_start 事件 + 而且事件 scope_kind 应为 work + 而且事件 scope_id 应为稳定作品 ID + 而且 metadata 应包含 playType、workId、sourceRoute 和 userId + + 场景: 无权限玩家尝试开始运行态 + 假如作品不可被当前玩家访问 + 当玩家调用 start runtime + 那么后端应返回权限错误 + 而且不应创建 run + 而且不应写入 work_play_start 事件 +``` + +#### 功能: bark-battle runtime finish 派生成绩提交 + +```gherkin +功能: bark-battle runtime finish 派生成绩提交 + 为了保护用户隐私并保证成绩可信 + 作为后端服务 + 我只接受派生指标并重新裁决正式结果 + + 场景: 玩家完成一局并提交合法派生指标 + 假如玩家已经成功 start runtime + 而且 run 处于 started 状态 + 而且提交的 runToken 与 start 返回一致 + 当玩家提交 elapsedMs、finalEnergy 和 BarkBattleDerivedMetrics + 那么后端应校验派生指标范围 + 而且应重新计算 serverWinner、score 和 ScoreSummary + 而且应把 run 状态改为 finished + 而且响应中的 accepted 应为 true + 而且请求与持久化记录中不应包含原始麦克风音频 + + 场景: 玩家重复提交同一个 run 的 finish + 假如某个 run 已经处于 finished 状态 + 当玩家再次提交 finish + 那么后端应拒绝重复提交 + 而且不应重复写入成绩 + 而且不应重复写入排行榜 + + 场景: 玩家提交不合理的时长 + 假如作品配置 durationMs 为 30000 + 当玩家提交 elapsedMs 明显短于配置时长 + 那么后端应返回 rejected 或 accepted_with_flags + 而且 antiCheatFlags 应包含 elapsed_too_short + 而且该结果默认不应进入排行榜 + + 场景: 玩家提交超出范围的声音指标 + 假如玩家已经成功 start runtime + 当玩家提交 peakVolumeMax 大于 1 或 validBarkCount 超过物理合理上限 + 那么后端应拒绝或打 flag + 而且 antiCheatFlags 应包含 metric_out_of_range 或 impossible_bark_count +``` + +#### 功能: bark-battle 排行榜投影 + +```gherkin +功能: bark-battle 排行榜投影 + 为了让玩家比较同一作品内的有效成绩 + 作为玩家 + 我希望排行榜只展示被后端接受的成绩 + + 场景: 作品开启排行榜且成绩被接受 + 假如作品 leaderboardEnabled 为 true + 而且玩家提交的 finish 结果 accepted 为 true + 当后端完成成绩写入 + 那么后端应写入或更新该作品的排行榜投影 + 而且排行榜排序应优先按 score 降序 + + 场景: 作品关闭排行榜 + 假如作品 leaderboardEnabled 为 false + 当玩家完成一局合法成绩 + 那么后端应记录 run 与 score + 而且不应写入排行榜 entry + + 场景: 带反作弊 flag 的成绩不进入默认榜单 + 假如玩家提交的结果为 accepted_with_flags + 当后端处理排行榜投影 + 那么默认排行榜不应展示该成绩 + 而且该 run 的反作弊标记应可供后台审计 +``` + +#### 功能: bark-battle 隐私边界 + +```gherkin +功能: bark-battle 隐私边界 + 为了保护玩家声音隐私 + 作为平台 + 我不能上传或保存原始麦克风音频 + + 场景: finish 请求包含原始音频字段 + 假如前端误传 audio、audioBase64、waveform 或 pcmSamples 字段 + 当后端解析 finish 请求 + 那么后端应拒绝请求或忽略并记录非法字段 + 而且持久化记录中不应出现可还原声音的数据 + + 场景: 正常 finish 只提交派生聚合指标 + 假如玩家完成一局 bark-battle + 当玩家提交 finish 请求 + 那么请求体应只包含峰值、均值、有效叫声次数、节奏命中、最终能量和耗时等派生指标 + 而且后端只保存这些派生指标与裁决结果 +``` + +### 9.2 BDD 到测试映射 + +| BDD 场景 | 测试层级 | 建议目标文件 | RED 期望 | +| --- | --- | --- | --- | +| 玩家请求已发布作品的 runtime config | contract + API | `server-rs/crates/shared-contracts/src/bark_battle.rs`、`server-rs/crates/api-server/src/bark_battle.rs` | 新 DTO / route 不存在导致编译或断言失败 | +| 玩家请求未发布作品的 runtime config | domain + API | `server-rs/crates/module-bark-battle/src/domain.rs`、`server-rs/crates/api-server/src/bark_battle.rs` | 未发布状态未被拒绝 | +| 玩家携带过期配置版本进入 runtime | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | configVersion mismatch 未返回错误 | +| 玩家成功开始已发布作品运行态 | API + integration | `server-rs/crates/api-server/src/bark_battle.rs` | start 未返回 runToken 或未写 tracking draft | +| 无权限玩家尝试开始运行态 | API | `server-rs/crates/api-server/src/bark_battle.rs` | 无权限仍创建 run 或写埋点 | +| 玩家完成一局并提交合法派生指标 | domain + facade | `server-rs/crates/module-bark-battle/src/domain.rs`、`server-rs/crates/spacetime-client/src/bark_battle.rs` | score / serverWinner 未由后端计算 | +| 玩家重复提交同一个 run 的 finish | SpacetimeDB reducer / facade | `server-rs/crates/spacetime-module/src/...`、`server-rs/crates/spacetime-client/src/bark_battle.rs` | duplicate finish 被接受 | +| 玩家提交不合理的时长 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | 未产生 elapsed_too_short flag | +| 玩家提交超出范围的声音指标 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | 未产生 metric_out_of_range / impossible_bark_count | +| 作品开启排行榜且成绩被接受 | domain + projection | `server-rs/crates/module-bark-battle/src/domain.rs`、SpacetimeDB reducer 测试 | accepted 成绩未生成 leaderboard projection | +| 作品关闭排行榜 | domain + projection | `server-rs/crates/module-bark-battle/src/domain.rs`、SpacetimeDB reducer 测试 | leaderboardEnabled=false 仍写榜 | +| 带反作弊 flag 的成绩不进入默认榜单 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | flagged 成绩仍进入默认榜 | +| finish 请求包含原始音频字段 | contract + API | `server-rs/crates/shared-contracts/src/bark_battle.rs`、API 反序列化测试 | DTO 接受或持久化原始音频字段 | +| 正常 finish 只提交派生聚合指标 | contract | `server-rs/crates/shared-contracts/src/bark_battle.rs` | DTO 缺字段或包含隐私风险字段 | + +### 9.3 TDD RED-GREEN-REFACTOR 切片 + +每个切片必须先写失败测试并运行到预期失败,再写最小实现。 + +#### Slice 1: shared contracts 固定请求/响应形状 + +RED: ```bash cd server-rs -cargo test -p module-bark-battle +cargo test -p shared-contracts bark_battle_contract_uses_camel_case --no-default-features ``` -测试覆盖: +先新增测试,断言: -- runtime config 校验。 -- finish metrics 范围校验。 -- 胜负判定。 -- score / grade / leaderboard score 计算。 -- 反作弊 flags。 +- `BarkBattleRunStartRequest` 序列化为 `workId`、`configVersion`、`sourceRoute`。 +- `BarkBattleRunFinishRequest` 只包含 `runId`、`runToken`、`workId`、`configVersion`、`elapsedMs`、`finalEnergy`、`clientWinner`、`metrics`。 +- `metrics` 只包含派生聚合字段,不包含 `audio`、`audioBase64`、`waveform`、`pcmSamples`。 -### 9.2 contracts 测试 +GREEN:新增 `shared-contracts/src/bark_battle.rs` 与 `lib.rs` 导出,让测试通过。 -再实现 shared contracts: +REFACTOR:统一枚举命名、字段注释和默认值策略。 + +#### Slice 2: domain 校验 runtime config 与 finish metrics + +RED: ```bash cd server-rs -cargo test -p shared-contracts bark_battle -cargo check -p shared-contracts +cargo test -p module-bark-battle validates_runtime_config_and_finish_metrics --no-default-features ``` -验收: +先新增测试,断言: -- DTO 可序列化 / 反序列化。 -- 枚举值稳定。 -- 可选字段向后兼容。 -- TypeScript mirror 或契约生成产物与 Rust contract 对齐。 +- `playTypeId` 必须为 `bark-battle`。 +- `durationMs`、`energyMin`、`energyMax`、`barkThreshold` 必须在合法范围。 +- `peakVolumeMax`、`peakVolumeAvg` 必须在 `0..1`。 +- 过短时长产生 `elapsed_too_short`。 +- 不可能叫声次数产生 `impossible_bark_count`。 -### 9.3 SpacetimeDB / api-server check +GREEN:新增 `module-bark-battle` 纯领域类型与校验函数。 -实现表、reducer、migration 与 facade 后: +REFACTOR:把常量收敛为领域常量,避免 magic number 分散。 + +#### Slice 3: domain 重新裁决成绩与排行榜资格 + +RED: ```bash cd server-rs -cargo check -p spacetime-module -cargo check -p spacetime-client -cargo check -p api-server -cargo test -p api-server bark_battle +cargo test -p module-bark-battle scores_finish_result_and_filters_leaderboard --no-default-features ``` -如仓库有统一命令,以统一命令为准,例如: +先新增测试,断言: + +- 后端根据 `finalEnergy` 重算 `serverWinner`,不直接信任 `clientWinner`。 +- 合法胜利生成 `accepted=true` 与稳定 `ScoreSummary`。 +- 带 flag 的结果默认 `leaderboardEligible=false`。 +- `leaderboardEnabled=false` 时不生成榜单投影。 + +GREEN:补 `score_finish_result(...)`、`is_leaderboard_eligible(...)` 等纯函数。 + +REFACTOR:拆分 score、winner、anti-cheat、leaderboard 四组小函数。 + +#### Slice 4: SpacetimeDB 表、reducer 与 migration + +RED: ```bash cd server-rs -cargo test --workspace +cargo check -p spacetime-module --no-default-features +cargo test -p spacetime-client bark_battle_facade_maps_run_records --no-default-features ``` -验收: +先新增 facade / mapper 测试或编译断言,预期因绑定、表或 reducer 缺失失败。 -- migration.rs 包含新增表与变更。 -- 绑定由生成命令产出,未手改生成物。 -- api-server route handler 只做编排,不内嵌复杂计分规则。 +GREEN:新增表、reducer/procedure、`migration.rs`、生成绑定、`spacetime-client` facade。 -### 9.4 前端 contract 对齐 +REFACTOR:确保 api-server 不直接依赖 generated bindings,mapper 命名与现有玩法一致。 + +#### Slice 5: api-server start / finish BFF + +RED: + +```bash +cd server-rs +cargo test -p api-server bark_battle_start_records_work_play_start --no-default-features +cargo test -p api-server bark_battle_finish_rejects_duplicate_or_invalid_metrics --no-default-features +``` + +先新增 API 测试,断言: + +- start 成功返回 `runId`、`runToken`、`runtimeConfig`。 +- start 成功后主动补写 `work_play_start`,不只依赖 route-level 兜底。 +- finish 重复提交被拒绝。 +- finish 不接受非法 metrics。 + +GREEN:新增 Axum route、handler、错误映射、tracking 调用。 + +REFACTOR:handler 只做编排,把规则留在 domain,把持久化留在 facade。 + +#### Slice 6: 前端 contract 对齐 + +RED: + +```bash +npm run test -- bark-battle +npm run typecheck +``` + +先新增前端 contract / client 测试,预期因 TS 类型或 client 缺失失败。 + +GREEN:补前端共享 contract mirror、runtime client 调用 start / finish。 + +REFACTOR:删除重复类型,保持后端 DTO 为事实源。 + +### 9.4 后端验收命令 + +按切片逐步运行,不要等全部实现后一次性补测: + +```bash +cd server-rs +cargo test -p shared-contracts bark_battle --no-default-features +cargo test -p module-bark-battle --no-default-features +cargo check -p spacetime-module --no-default-features +cargo check -p spacetime-client --no-default-features +cargo test -p api-server bark_battle --no-default-features +``` + +如涉及 API smoke: + +```bash +npm run api-server +# 另开终端执行对应 bark-battle start / finish smoke 或项目既有 API 测试脚本 +``` + +每次表结构变更后必须同步: + +1. 更新 `migration.rs`。 +2. 重新生成 SpacetimeDB bindings。 +3. 检查 generated bindings 变更只来自生成命令。 +4. 运行 `cargo check -p spacetime-module -p spacetime-client --no-default-features`。 + +### 9.5 前端 contract 对齐验收 前端只在后端 contract 稳定后接入: @@ -750,7 +1006,7 @@ npm run build - 前端 result panel 展示后端 `RunResult`。 - 前端本地结算只作为即时反馈,正式结果以后端返回为准。 -### 9.5 手工验收清单 +### 9.6 手工验收清单 - 可以创建并保存 bark-battle 草稿。 - 可以发布成稳定作品 ID,`playTypeId = bark-battle`。