24 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. TDD / 验收顺序与命令
本任务只写方案,不执行代码实现。后续落地建议按以下顺序:
9.1 domain 纯函数测试
先实现 module-bark-battle:
cd server-rs
cargo test -p module-bark-battle
测试覆盖:
- runtime config 校验。
- finish metrics 范围校验。
- 胜负判定。
- score / grade / leaderboard score 计算。
- 反作弊 flags。
9.2 contracts 测试
再实现 shared contracts:
cd server-rs
cargo test -p shared-contracts bark_battle
cargo check -p shared-contracts
验收:
- DTO 可序列化 / 反序列化。
- 枚举值稳定。
- 可选字段向后兼容。
- TypeScript mirror 或契约生成产物与 Rust contract 对齐。
9.3 SpacetimeDB / api-server check
实现表、reducer、migration 与 facade 后:
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
如仓库有统一命令,以统一命令为准,例如:
cd server-rs
cargo test --workspace
验收:
- migration.rs 包含新增表与变更。
- 绑定由生成命令产出,未手改生成物。
- api-server route handler 只做编排,不内嵌复杂计分规则。
9.4 前端 contract 对齐
前端只在后端 contract 稳定后接入:
npm run typecheck
npm test -- bark-battle
npm run build
验收:
- runtime start / finish 请求字段与 shared contracts 一致。
- 前端 result panel 展示后端
RunResult。 - 前端本地结算只作为即时反馈,正式结果以后端返回为准。
9.5 手工验收清单
- 可以创建并保存 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 混做。