# M4 module-combat battle state 查询设计(2026-04-22) 更新时间:`2026-04-22` 补充状态:`2026-04-22` 当前 battle query 纵切片已经完成到“真实可编译、可生成 binding、可被 Axum 调用”的状态: 1. `spacetime-module` 中的 `get_battle_state` procedure 已稳定存在。 2. `spacetime-client/src/module_bindings` 已重新执行 `spacetime generate`,当前已真实包含: - `battle_state_query_input_type` - `get_battle_state_procedure` - `battle_state.reward_items` 对应字段 3. `spacetime-client/src/lib.rs` 里原本返回“binding 尚未生成”的占位 `get_battle_state(...)` 已替换为真实 procedure 调用。 4. `cargo check -p spacetime-client` 与 `cargo check -p api-server` 已再次通过。 当前仍未完成的只有长时回归验证: 1. `cargo test -p api-server --bin api-server story_battles --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` 在当前机器上编译耗时较长,尚未在单次时窗内拿到最终断言结果。 2. `npm run check:encoding` 已启动但尚未在单次时窗内跑完。 ## 0. 文档目标 本文件只冻结当前 `M4` 的一个最小新增切片: **新增 `GET /api/story/battles/:battleStateId`,让 Axum 能从 `SpacetimeDB` 同步读取单个 `battle_state` 当前快照,不提前承诺旧 runtime story state 兼容。** 这轮目标不是实现旧 `GET /api/runtime/story/state/:sessionId` 的战斗子视图兼容,也不是把 `battle + story_event + currentStory` 一次性收口进 `resolve_story_action`。 --- ## 1. 为什么先补这个切片 当前 battle 链路已经具备: 1. `module-combat` 已冻结 `battle_state` 领域类型与纯结算规则。 2. `spacetime-module` 已有 `create_battle_state_and_return`、`resolve_combat_action_and_return`。 3. `spacetime-client` 与 `api-server` 已能创建战斗并推进单次动作。 但现在仍缺一个最基本的恢复能力: 1. battle 建立后,Axum 还不能按 `battle_state_id` 重新读取真相态。 2. 页面刷新、重连或后续 story 编排都缺一个稳定的单战斗查询入口。 3. 后续若要把 battle 收口进 `resolve_story_action`,也需要先有独立 battle query 可复用。 因此本轮先补最小 `battle state` 查询切片,不提前跳到更重的 runtime story 兼容。 --- ## 2. 当前冻结范围 本轮只包含以下能力: 1. 新增公开接口:`GET /api/story/battles/:battleStateId` 2. 认证方式:Bearer JWT 3. 数据来源:`SpacetimeDB procedure get_battle_state` 4. 返回体只包含: - `battleState` 本轮明确不做: 1. 不兼容旧 `GET /api/runtime/story/state/:sessionId` 2. 不补 battle 列表查询 3. 不做 `battle_state` 订阅与 cache 读模型 4. 不在查询链路里拼装 `story_event / npc / quest / inventory` 5. 不把 battle query 直接拼回旧 `RuntimeStoryActionResponse` --- ## 3. 接口 contract ### 3.1 请求 - 方法:`GET` - 路径:`/api/story/battles/:battleStateId` - 认证:必须携带 Bearer JWT - 路径参数: - `battleStateId`:目标战斗状态 ID ### 3.2 成功响应 成功响应延续当前 `api-server` 统一 envelope,`data` 字段结构为: ```json { "battleState": { "battleStateId": "battle_xxx", "storySessionId": "storysess_xxx", "runtimeSessionId": "runtime_xxx", "actorUserId": "user_xxx", "chapterId": "chapter_xxx", "targetNpcId": "npc_xxx", "targetName": "黑爪狼", "battleMode": "fight", "status": "ongoing", "playerHp": 42, "playerMaxHp": 60, "playerMana": 12, "playerMaxMana": 20, "targetHp": 18, "targetMaxHp": 30, "experienceReward": 18, "rewardItems": [], "turnIndex": 1, "lastActionFunctionId": "battle_attack_basic", "lastActionText": "普通攻击", "lastResultText": "普通攻击命中了黑爪狼,本次攻击已经完成结算。", "lastDamageDealt": 12, "lastDamageTaken": 4, "lastOutcome": "ongoing", "version": 2, "createdAt": "2026-04-22T00:00:00.000000Z", "updatedAt": "2026-04-22T00:01:00.000000Z" } } ``` ### 3.3 错误响应 当前延续 battle facade 已有策略: 1. `SpacetimeClientError::Runtime(_)` 映射为 `400` 2. 其他 `SpacetimeClientError` 映射为 `502` 3. 错误 `details.provider` 固定为 `spacetimedb` --- ## 4. 分层职责 ### 4.1 `module-combat` 职责: 1. 冻结 `BattleStateQueryInput` 2. 负责 query input builder 与 validator 3. 继续复用 `BattleStateProcedureResult` 作为最小查询返回壳 不负责: 1. HTTP 路径解析 2. JWT 鉴权 3. battle view model 编译 ### 4.2 `spacetime-module` 职责: 1. 读取 `battle_state` 2. 校验 `battle_state_id` 3. 返回单个 `BattleStateSnapshot` ### 4.3 `spacetime-client` 职责: 1. 构造 `BattleStateQueryInput` 2. 调用 `get_battle_state` 3. 把 generated binding 结果映射为 `BattleStateRecord` 当前实现补充: 1. `reward_items` 已按 generated binding 映射回 `BattleStateRecord.reward_items`,不再用空集合占位。 2. battle query 当前不再依赖 façade stub 或手写假返回。 ### 4.4 `api-server` 职责: 1. 暴露 `GET /api/story/battles/:battleStateId` 2. 做 Bearer JWT 鉴权 3. 透传 `battleStateId` 4. 把 `BattleStateRecord` 映射到 battle JSON payload --- ## 5. 验收口径 本轮验收只要求以下几点: 1. `api-server` 路由树已真实挂出该接口 2. 未登录访问返回 `401` 3. 在 `SpacetimeDB` 未发布或未连通时返回 `502` 4. `cargo test -p api-server story_battles` 可通过 5. `cargo check -p module-combat -p spacetime-module -p spacetime-client -p api-server` 可通过 6. `npm run check:encoding` 已执行,确保新增中文文档没有编码损坏 当前验证状态: 1. 第 5 条已达成。 2. 第 4、6 条仍在继续追,不应提前宣称通过。 --- ## 6. 后续边界 这条最小 battle query 落地后,后续再继续拆下一层: 1. 评估 battle 查询是否需要补 actor ownership 校验 2. 设计 battle 结束事件如何接入 `story_event` 3. 再把 battle query 与 `story state / resolve_story_action / currentStory` 汇总到更高层编排 在这些 contract 未冻结前,不应把当前接口误称为“旧 runtime story state 已迁移完成”。