35 KiB
bark-battle 后端 DDD 技术方案(2026-05-11)
1. 背景、范围与非目标
1.1 背景
bark-battle / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效叫声次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。
现有前端方案 docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md 已覆盖 Phaser / TypeScript / Vite / Web Audio / DOM HUD 的 runtime 落地方式,并明确不覆盖后端表结构、成绩持久化、作品发布、广场接入与实时多人协议。因此需要单独补充后端 DDD 技术方案,避免前端 runtime 在接入平台作品、正式游玩埋点、成绩、排行榜和发布闭环时承接不属于表现层的业务真相。
本方案遵循当前 Genarrative 后端路线:server-rs + Axum + SpacetimeDB。DDD 边界保持为:
module-*:纯领域规则与 use case 约束。spacetime-module:SpacetimeDB 表、reducer、migration。spacetime-client:SpacetimeDB 绑定调用 facade。api-server:HTTP / SSE / BFF 门面与权限校验入口。shared-contracts:前后端共享 DTO、请求、响应、错误码与 schema 约束。platform-*:作品、发布、广场、埋点等平台能力的领域边界。
1.2 本文范围
本文覆盖后端方案,不实现代码:
- bark-battle 玩法接入级别建议。
- 后端 DDD 分层与职责边界。
- shared contracts 草案。
- SpacetimeDB 数据模型草案。
- HTTP API / facade 草案。
- SpacetimeDB migration 与绑定生成策略。
- 安全、隐私与反作弊约束。
- TDD / 验收顺序与可执行命令。
- 与现有前端方案和 BDD 文档的关系。
1.3 非目标
MVP 明确不做:
- 不保存原始麦克风音频、音频片段、可还原语音内容的 waveform 或频谱明细。
- 不做实时多人在线对战;首版只支持本地 runtime + 后端记录派生结果。
- 不做复杂 AI 声纹识别、狗叫语义识别、身份声纹比对或真人/动物声纹分类。
- 不由前端直接写正式成绩、排行榜或作品发布状态。
- 不把 Phaser / Web Audio / DOM HUD 逻辑迁入后端。
- 不在本方案中实现代码、建表或生成绑定。
2. 玩法接入级别建议
2.1 推荐首版闭环
建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 / 可选排行榜”的闭环:
- 创作者创建 bark-battle 草稿,配置标题、描述、狗狗主题、背景、难度、单局时长、音量阈值、AI 对手参数和排行榜开关。
- 发布为稳定作品 ID,
playTypeId = "bark-battle"。 - 玩家从作品页或广场进入 runtime,前端获取发布态 runtime config。
- 玩家授权麦克风后在本地完成 30 秒声控对战。
- 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效叫声次数、节奏命中、最终能量、客户端结果摘要等。
- 后端校验 work、config version、run token、时长、分数范围和权限后,生成服务端认可的 run result / score summary。
- 若作品开启排行榜,则写入可投影的 leaderboard 记录。
- 正式作品级游玩埋点统一写
work_play_start,其中scope_kind=work,scope_id=稳定作品 ID,metadata 包含playType、workId、sourceRoute、userId。
2.2 后续增强路径
后续再考虑多人实时:
- Phase 2:排行榜、挑战分享、个人历史成绩、作品统计面板。
- Phase 3:异步影子对手 / ghost replay,但仍不保存原始音频,只保存低维派生曲线或聚合指标。
- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入 MVP。
3. DDD 分层设计
3.1 总体分层
frontend/runtime
-> api-server HTTP BFF
-> shared-contracts DTO
-> spacetime-client facade
-> generated SpacetimeDB bindings
-> spacetime-module reducers / tables / migration.rs
-> module-bark-battle pure domain
-> platform-work / platform-publish / platform-tracking / platform-leaderboard
3.2 module-bark-battle 职责
建议新增 server-rs/crates/module-bark-battle/,只放纯领域规则,不依赖 Axum、SpacetimeDB SDK、HTTP、数据库绑定或前端类型。
职责:
- 校验 bark-battle 配置合法性。
- 定义配置版本兼容规则。
- 计算提交结果的派生分数区间与胜负判定是否自洽。
- 计算
ScoreSummary、排行榜排序分数、统计指标。 - 定义反作弊基础规则:时长范围、有效叫声次数上限、峰值范围、能量范围、提交窗口、run 状态机。
不职责:
- 不访问数据库。
- 不处理 HTTP 请求。
- 不生成 SpacetimeDB 表。
- 不处理麦克风音频采样。
3.3 shared-contracts 职责
建议新增 server-rs/crates/shared-contracts/src/bark_battle.rs,并在前端共享契约生成流程中对齐同名 DTO。
职责:
- 定义草稿、发布态 runtime config、work summary、run start、run finish、run result、score summary、leaderboard DTO。
- 定义 request / response / error code。
- 保持字段命名、枚举值、时间单位和数值范围稳定。
约束:
- DTO 可以表达业务数据,但不承载领域算法。
- 前端不得手写与后端不一致的正式契约;如果存在 TypeScript mirror,需要由契约生成或测试保证一致。
3.4 spacetime-module 职责
建议在现有 SpacetimeDB 模块边界内新增 bark-battle 表与 reducer,或按当前仓库约定新增独立模块文件。职责:
- 定义表:作品配置、发布版本、runtime run、score/stat、leaderboard entry。
- 定义 reducer / procedure:保存草稿、发布版本、开始 run、结束 run、写排行榜、查询投影所需索引。
- 维护 migration。
必须明确:
- 所有表结构变更进入
migration.rs。 - SpacetimeDB 绑定通过既有生成命令生成。
- 不手改生成物,不手改 generated bindings。
3.5 spacetime-client 职责
职责:
- 封装 generated bindings,向
api-server提供稳定 facade。 - 隐藏 reducer 名称、表订阅细节和绑定类型差异。
- 将 SpacetimeDB 错误转换为后端内部错误模型。
不职责:
- 不放业务规则主逻辑。
- 不绕过
module-bark-battle做胜负和分数裁决。
3.6 api-server 职责
Axum HTTP / BFF 门面职责:
- 鉴权、用户上下文、work 权限、发布态读取权限。
- 解析请求、调用 domain 校验、调用 spacetime-client facade。
- 将内部错误映射成 HTTP status + shared error code。
- 负责正式作品级游玩埋点入口,统一写
work_play_start。 - 为前端 runtime 提供一次性 start token / run id,避免匿名 finish 直接刷榜。
3.7 platform-* 职责
platform-work:稳定作品 ID、作品所有权、作品摘要、作品状态。platform-publish:草稿到发布态版本、config version、发布可见性。platform-tracking:统一埋点,尤其work_play_start。platform-leaderboard:若已有通用排行榜能力,bark-battle 只提供 score projection,不重复建设平台级排名系统。
3.8 frontend/runtime 职责边界
前端只做:
- Phaser / DOM HUD 表现。
- Web Audio 采样、环境噪声校准和本地即时反馈。
- 本地临时 UI 状态:权限、倒计时、动画、结算展示。
- 调用 start / finish / leaderboard API。
前端不得承接正式业务真相:
- 不直接决定正式排行榜结果。
- 不直接写作品发布状态。
- 不绕过后端写成绩。
- 不上传原始麦克风音频。
4. shared contracts 设计草案
以下为字段草案,具体 Rust / TypeScript 命名按仓库契约规范落地。
4.1 BarkBattleDraft
BarkBattleDraft {
draftId: string
ownerUserId: string
playTypeId: "bark-battle"
title: string
description?: string
theme: BarkBattleTheme
runtimeConfig: BarkBattleRuntimeConfigDraft
leaderboardEnabled: boolean
visibility: "private" | "unlisted" | "public"
createdAt: string
updatedAt: string
}
4.2 BarkBattleRuntimeConfig
BarkBattleRuntimeConfig {
workId: string
configVersion: number
playTypeId: "bark-battle"
durationMs: number // MVP 默认 30000
energyMin: number // 默认 -100
energyMax: number // 默认 100
winEnergyThreshold: number // 可选:低于阈值可平局
barkThreshold: number // 归一化 0..1
peakWeight: number
barkCountWeight: number
rhythmWeight: number
opponent: {
difficulty: "easy" | "normal" | "hard"
basePower: number
variance: number
}
theme: {
playerDogSkin: string
opponentDogSkin: string
stageId: string
soundPackId?: string
}
leaderboardEnabled: boolean
publishedAt: string
}
4.3 WorkSummary
BarkBattleWorkSummary {
workId: string
playTypeId: "bark-battle"
title: string
description?: string
coverAssetId?: string
authorUserId: string
authorDisplayName?: string
configVersion: number
leaderboardEnabled: boolean
totalPlayCount: number
publishedAt: string
updatedAt: string
}
4.4 RunStart
BarkBattleRunStartRequest {
workId: string
configVersion: number
sourceRoute?: string
clientRuntimeVersion?: string
}
BarkBattleRunStartResponse {
runId: string
runToken: string
workId: string
configVersion: number
serverStartedAt: string
expiresAt: string
runtimeConfig: BarkBattleRuntimeConfig
}
4.5 RunFinish
BarkBattleRunFinishRequest {
runId: string
runToken: string
workId: string
configVersion: number
clientStartedAt?: string
clientFinishedAt?: string
elapsedMs: number
finalEnergy: number
clientWinner: "player" | "opponent" | "draw"
metrics: BarkBattleDerivedMetrics
clientRuntimeVersion?: string
}
BarkBattleDerivedMetrics {
peakVolumeMax: number // 0..1
peakVolumeAvg: number // 0..1
validBarkCount: number
rhythmHitCount: number
longestCombo: number
sampleWindowCount?: number
calibrationNoiseFloor?: number // 0..1
}
4.6 RunResult
BarkBattleRunResult {
runId: string
workId: string
userId?: string
configVersion: number
accepted: boolean
serverWinner: "player" | "opponent" | "draw"
finalEnergy: number
score: number
scoreSummary: BarkBattleScoreSummary
leaderboardEntry?: BarkBattleLeaderboardEntry
antiCheatFlags: string[]
finishedAt: string
}
4.7 ScoreSummary
BarkBattleScoreSummary {
score: number
grade: "S" | "A" | "B" | "C" | "D"
finalEnergy: number
winMargin: number
validBarkCount: number
peakVolumeMax: number
rhythmHitCount: number
longestCombo: number
elapsedMs: number
}
4.8 Leaderboard 可选类型
BarkBattleLeaderboardQuery {
workId: string
configVersion?: number
period: "all" | "daily" | "weekly"
limit: number
cursor?: string
}
BarkBattleLeaderboardEntry {
rank?: number
runId: string
workId: string
userId?: string
displayName?: string
score: number
scoreSummary: BarkBattleScoreSummary
createdAt: string
}
BarkBattleLeaderboardResponse {
workId: string
entries: BarkBattleLeaderboardEntry[]
viewerBest?: BarkBattleLeaderboardEntry
nextCursor?: string
}
5. 数据模型草案
5.1 作品配置表
建议表:bark_battle_work_config。
字段草案:
work_id:稳定作品 ID,关联平台作品。draft_id:草稿 ID,可选。owner_user_id:创作者。play_type_id:固定bark-battle。config_version:发布配置版本。title、description、cover_asset_id。runtime_config_json:发布态 runtime 配置 JSON;字段需由 shared contracts 校验。leaderboard_enabled。status:draft/published/archived。created_at、updated_at、published_at。
约束:
- 同一
work_id + config_version不可变;新发布生成新版本。 - runtime 请求只读发布态配置。
5.2 runtime run 表
建议表:bark_battle_runtime_run。
字段草案:
run_id。run_token_hash:保存 token hash,不保存明文 token。work_id。config_version。user_id:匿名时可空或使用匿名会话 ID。source_route。status:started/finished/rejected/expired。server_started_at、server_finished_at、expires_at。client_elapsed_ms。final_energy。client_winner、server_winner。anti_cheat_flags_json。client_runtime_version。
5.3 score / stat 表
建议表:bark_battle_score_record。
字段草案:
score_id。run_id。work_id。config_version。user_id。score。grade。final_energy。valid_bark_count。peak_volume_max。peak_volume_avg。rhythm_hit_count。longest_combo。elapsed_ms。metrics_json:只保存派生聚合指标。created_at。
明确禁止:
- 不保存原始麦克风音频。
- 不保存可还原语音的 PCM、Opus、MP3、WAV、base64 音频、逐帧 waveform。
- 不保存高精度声纹向量。
5.4 leaderboard 表
若平台已有通用排行榜,优先复用平台 leaderboard 投影;否则可新增 bark_battle_leaderboard_entry:
entry_id。work_id。config_version。run_id。user_id。score。tie_breaker_energy。tie_breaker_elapsed_ms。created_at。
排序建议:
score降序。final_energy/winMargin降序。elapsed_ms更接近配置时长者优先,避免异常短局刷分。created_at升序或按平台既有规则。
6. API 草案
路径仅为草案,落地时按 api-server 当前路由命名规范调整。
6.1 创建 / 保存草稿
POST /api/bark-battle/drafts
PUT /api/bark-battle/drafts/{draftId}
GET /api/bark-battle/drafts/{draftId}
职责:
- 仅创作者可创建和保存。
- 校验
playTypeId = bark-battle。 - 调用
module-bark-battle校验 runtime config 范围。
6.2 发布 / 获取作品
POST /api/bark-battle/drafts/{draftId}/publish
GET /api/works/{workId}/bark-battle
GET /api/bark-battle/works/{workId}/runtime-config
职责:
- 发布生成稳定
workId和递增configVersion。 - 获取作品只返回发布态配置与展示摘要。
- 未发布或无权限作品返回明确错误。
6.3 start runtime
POST /api/bark-battle/runs/start
请求:BarkBattleRunStartRequest。响应:BarkBattleRunStartResponse。
职责:
- 校验作品存在、发布态、可游玩。
- 校验 config version,必要时返回最新版本。
- 创建
run_id与一次性run_token。 - 写正式作品级游玩埋点:
work_play_start。
埋点要求:
event_key: work_play_start
scope_kind: work
scope_id: <稳定作品 ID>
metadata: {
playType: "bark-battle",
workId: "<workId>",
sourceRoute: "<sourceRoute>",
userId: "<userId or anonymous>"
}
6.4 finish runtime
POST /api/bark-battle/runs/{runId}/finish
请求:BarkBattleRunFinishRequest。响应:BarkBattleRunResult。
职责:
- 校验 run token。
- 校验 run 仍处于
started且未过期。 - 校验
work_id + config_version与 start 时一致。 - 校验时长、finalEnergy、metrics 范围。
- 使用
module-bark-battle生成服务端认可的serverWinner、score、ScoreSummary、antiCheatFlags。 - 写
bark_battle_runtime_runfinish 状态与bark_battle_score_record。 - 如开启 leaderboard 且结果 accepted,写排行榜。
6.5 作品级游玩埋点
start runtime 内部必须触发统一埋点;不建议前端单独调用一个 bark-battle 专用埋点 API。若平台已有通用 tracking API,则 api-server 内部调用 platform tracking facade:
track_event(
event_key = "work_play_start",
scope_kind = "work",
scope_id = workId,
metadata = { playType, workId, sourceRoute, userId }
)
6.6 可选排行榜
GET /api/bark-battle/works/{workId}/leaderboard?period=all&limit=50&cursor=...
GET /api/bark-battle/works/{workId}/leaderboard/me
职责:
- 只读已接受成绩。
- 支持分页。
- 支持匿名用户时隐藏或弱化身份展示。
- 若作品关闭排行榜,返回空投影或明确 disabled 状态。
7. SpacetimeDB 与 migration 策略
7.1 表 / reducer / procedure 边界
SpacetimeDB 侧只承载持久化、索引、reducer 原子写入和可查询投影,不承载 HTTP 鉴权或前端表现。
建议 reducer / procedure:
bark_battle_save_draft_config:保存草稿配置。bark_battle_publish_work_config:发布配置版本。bark_battle_start_run:创建 runtime run。bark_battle_finish_run:结束 run 并写 score。bark_battle_upsert_leaderboard_entry:写排行榜投影。bark_battle_get_work_runtime_config:读取发布态配置。bark_battle_get_leaderboard:读取排行榜投影。
如当前架构要求 reducer 仅由 spacetime-client 调用,则 api-server 不直接操作 SpacetimeDB SDK。
7.2 migration.rs
所有表结构变更必须进入 migration.rs:
- 新增 bark-battle 表时写显式 migration。
- 新增索引、唯一约束或版本字段时写 migration。
- 从草稿 JSON 拆字段时写数据迁移说明。
- 不允许只改 Rust struct 而不补 migration。
7.3 绑定生成
涉及 SpacetimeDB schema / reducer 变更后:
- 运行仓库既有 SpacetimeDB 绑定生成命令。
- 检查 generated bindings 变化。
spacetime-client只引用生成物,不手改生成物。- shared contracts 与 generated bindings 的差异通过 facade 消化,不让前端直接依赖数据库绑定。
明确要求:不手改生成物,不手改 generated bindings,不用临时复制粘贴类型绕过生成流程。
7.4 api-server facade
api-server 通过 spacetime-client facade 调用 SpacetimeDB:
BarkBattleService
-> BarkBattleDomainPolicy
-> BarkBattleSpacetimeClient
-> generated bindings
这样可以保证:
- HTTP 层易测试。
- domain 纯函数可独立测试。
- SpacetimeDB 绑定变更不会扩散到 route handler。
8. 安全、隐私与反作弊
8.1 隐私
- 不上传原始音频。
- 不保存原始音频。
- 不保存可还原用户声音的高精度采样曲线。
- 只保存派生指标:峰值、均值、有效叫声次数、节奏命中、最终能量、分数、耗时。
- 前端权限文案必须说明麦克风只用于本地玩法输入,MVP 不上传原始声音。
8.2 不信任前端胜负
后端不能直接信任:
clientWinner。score。finalEnergy。elapsedMs。validBarkCount。
后端必须校验并重算服务端认可结果。MVP 因不上传音频,无法完全证明声音真实性,但仍需做边界反作弊。
8.3 校验规则
必须校验:
- run token 是否匹配且未使用。
- run 是否未过期。
work_id + config_version是否与 start 时一致。- 用户是否有权限游玩该 work。
- 提交时长是否接近配置时长,例如 30 秒局允许少量网络 / 页面调度误差。
finalEnergy是否在配置范围。peakVolumeMax、peakVolumeAvg是否在0..1。validBarkCount、rhythmHitCount、longestCombo是否在物理合理上限。clientStartedAt/clientFinishedAt与服务端时间窗口是否合理。- 同一用户 / 匿名会话的频率限制。
8.4 反作弊处理
建议结果状态:
accepted:写 score,可进入排行榜。accepted_with_flags:写 score,但标记异常,默认不入榜或降低可信度。rejected:不入榜,只记录 run 失败原因。
常见 flags:
elapsed_too_shortelapsed_too_longmetric_out_of_rangeconfig_version_mismatchtoken_invalidduplicate_finishrate_limitedimpossible_bark_count
9. BDD 行为场景、TDD 落地顺序与验收命令
本任务只写方案,不执行代码实现。后续落地必须先用 BDD 锁定可观察行为,再按 TDD 做 RED-GREEN-REFACTOR。没有先失败的测试,不进入生产代码实现。
9.1 BDD 场景清单
以下场景用于约束后端行为,场景标题应映射到后续 Rust 测试名、API 测试名或 smoke 用例名。
功能: bark-battle 发布态作品运行配置
功能: 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 与统一游玩埋点
功能: 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 派生成绩提交
功能: 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 排行榜投影
功能: bark-battle 排行榜投影
为了让玩家比较同一作品内的有效成绩
作为玩家
我希望排行榜只展示被后端接受的成绩
场景: 作品开启排行榜且成绩被接受
假如作品 leaderboardEnabled 为 true
而且玩家提交的 finish 结果 accepted 为 true
当后端完成成绩写入
那么后端应写入或更新该作品的排行榜投影
而且排行榜排序应优先按 score 降序
场景: 作品关闭排行榜
假如作品 leaderboardEnabled 为 false
当玩家完成一局合法成绩
那么后端应记录 run 与 score
而且不应写入排行榜 entry
场景: 带反作弊 flag 的成绩不进入默认榜单
假如玩家提交的结果为 accepted_with_flags
当后端处理排行榜投影
那么默认排行榜不应展示该成绩
而且该 run 的反作弊标记应可供后台审计
功能: bark-battle 隐私边界
功能: 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:
cd server-rs
cargo test -p shared-contracts bark_battle_contract_uses_camel_case --no-default-features
先新增测试,断言:
BarkBattleRunStartRequest序列化为workId、configVersion、sourceRoute。BarkBattleRunFinishRequest只包含runId、runToken、workId、configVersion、elapsedMs、finalEnergy、clientWinner、metrics。metrics只包含派生聚合字段,不包含audio、audioBase64、waveform、pcmSamples。
GREEN:新增 shared-contracts/src/bark_battle.rs 与 lib.rs 导出,让测试通过。
REFACTOR:统一枚举命名、字段注释和默认值策略。
Slice 2: domain 校验 runtime config 与 finish metrics
RED:
cd server-rs
cargo test -p module-bark-battle validates_runtime_config_and_finish_metrics --no-default-features
先新增测试,断言:
playTypeId必须为bark-battle。durationMs、energyMin、energyMax、barkThreshold必须在合法范围。peakVolumeMax、peakVolumeAvg必须在0..1。- 过短时长产生
elapsed_too_short。 - 不可能叫声次数产生
impossible_bark_count。
GREEN:新增 module-bark-battle 纯领域类型与校验函数。
REFACTOR:把常量收敛为领域常量,避免 magic number 分散。
Slice 3: domain 重新裁决成绩与排行榜资格
RED:
cd server-rs
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:
cd server-rs
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 缺失失败。
GREEN:新增表、reducer/procedure、migration.rs、生成绑定、spacetime-client facade。
REFACTOR:确保 api-server 不直接依赖 generated bindings,mapper 命名与现有玩法一致。
Slice 5: api-server start / finish BFF
RED:
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:
npm run test -- bark-battle
npm run typecheck
先新增前端 contract / client 测试,预期因 TS 类型或 client 缺失失败。
GREEN:补前端共享 contract mirror、runtime client 调用 start / finish。
REFACTOR:删除重复类型,保持后端 DTO 为事实源。
9.4 后端验收命令
按切片逐步运行,不要等全部实现后一次性补测:
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:
npm run api-server
# 另开终端执行对应 bark-battle start / finish smoke 或项目既有 API 测试脚本
每次表结构变更后必须同步:
- 更新
migration.rs。 - 重新生成 SpacetimeDB bindings。
- 检查 generated bindings 变更只来自生成命令。
- 运行
cargo check -p spacetime-module -p spacetime-client --no-default-features。
9.5 前端 contract 对齐验收
前端只在后端 contract 稳定后接入:
npm run typecheck
npm test -- bark-battle
npm run build
验收:
- runtime start / finish 请求字段与 shared contracts 一致。
- 前端 result panel 展示后端
RunResult。 - 前端本地结算只作为即时反馈,正式结果以后端返回为准。
9.6 手工验收清单
- 可以创建并保存 bark-battle 草稿。
- 可以发布成稳定作品 ID,
playTypeId = bark-battle。 - runtime start 返回 config、runId、runToken。
- start 写入
work_play_start,scope 与 metadata 符合要求。 - finish 不上传音频,只上传派生指标。
- finish 返回服务端认可的 result。
- 异常时长、重复提交、config version mismatch 会被拒绝或打 flag。
- 排行榜关闭时不写榜;开启时只写 accepted 结果。
10. 与现有前端方案和 BDD 文档的关系
10.1 依赖文档
- 前端 runtime 方案:
docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md。 - BDD / DDD / TDD 总计划:
.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md。 - 当前后端实现基线:
docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md。 - SpacetimeDB 表结构变更约束:
docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md。
10.2 与前端方案的对齐点
- 前端方案负责 Phaser / Web Audio / DOM HUD;本文负责作品、成绩、排行榜、发布和埋点。
- 前端
BarkBattleSnapshot可用于本地即时表现,但正式RunResult以后端返回为准。 - 前端不上传原始音频,只上传
BarkBattleDerivedMetrics。 - 前端本地 config 应来自后端发布态
BarkBattleRuntimeConfig,不能在生产游玩中使用未发布临时配置。 - 前端 result panel 应能展示后端返回的 score、grade、antiCheatFlags 与 leaderboard entry。
10.3 与 BDD 的对齐点
后续 BDD 场景应覆盖:
- 玩家从作品页进入 bark-battle runtime。
- 玩家授权麦克风后开始 30 秒对战。
- 玩家完成单局后看到后端确认结果。
- 未授权麦克风时可以看到降级说明,但不写正式成绩。
- 作品关闭排行榜时不展示排名入口。
- 作品开启排行榜时展示当前作品排名。
- 重复 finish / 过期 run / 配置版本不一致时返回可解释错误。
10.4 后端落地顺序建议
- 先只做 contract + domain,固定
playTypeId = bark-battle与配置 schema。 - 再做草稿 / 发布态作品配置读写。
- 再做 start / finish run 与
work_play_start埋点。 - 最后做排行榜投影。
- 实时多人协议另起方案,不与 MVP 混做。