# M4 Runtime Inventory State Query 设计(2026-04-22) 更新时间:`2026-04-22` ## 0. 文档目标 本文件只冻结当前 `M4` 的一个最小新增切片: **新增 `GET /api/runtime/sessions/:runtimeSessionId/inventory`,让 Axum 能从 `SpacetimeDB` 同步读取当前玩家在指定 `runtime_session` 下的 `inventory_slot` 真相态,不提前承诺旧 `GameState.playerInventory / playerEquipment` 全量兼容。** 本轮目标不是把旧前端背包 view model 一次性全迁到 Rust,也不是把 `inventory_use / craft / dismantle / reforge` 一次性补齐。 --- ## 1. 为什么先做这个切片 当前 inventory 主链已经具备: 1. `module-inventory` 已冻结 `inventory_slot`、`apply_inventory_mutation` 的首版领域 contract。 2. `spacetime-module` 已有 `inventory_slot` 真相表与 `apply_inventory_mutation` reducer。 3. `treasure / quest / battle` 奖励物品已经能同步写入 `inventory_slot`。 真正缺的部分是: 1. 背包真相态还没有稳定查询入口。 2. `api-server` 还不能按 `runtime_session_id + 当前用户` 同步返回当前背包与装备状态。 3. 后续如果要把背包面板、装备面板或 runtime story projection 收口到 `SpacetimeDB`,需要先有一个最小 inventory query 切片可复用。 因此本轮先补“只读查询”能力,不提前跳到更重的旧前端状态兼容。 --- ## 2. 当前冻结范围 本轮只包含以下能力: 1. 新增公开接口:`GET /api/runtime/sessions/:runtimeSessionId/inventory` 2. 认证方式:Bearer JWT 3. 查询 scope:`runtime_session_id + 当前登录用户` 4. 数据来源:`SpacetimeDB procedure get_runtime_inventory_state` 5. 返回体只包含: - `runtimeSessionId` - `actorUserId` - `backpackItems` - `equipmentItems` 本轮明确不做: 1. 不兼容旧 `GameState.playerInventory` 2. 不兼容旧 `GameState.playerEquipment` 3. 不补按 `story_session_id` 或全局用户维度的 inventory 查询 4. 不做 inventory public subscription / view 5. 不在查询链路里拼装 quest / npc / battle / story_event 6. 不提前做 `inventory_use / craft / dismantle / reforge` --- ## 3. 为什么按 `runtime_session_id + 当前用户` 查询 当前 `inventory_slot` 的主作用域字段是: 1. `runtime_session_id` 2. `actor_user_id` 3. `story_session_id` 本轮选择 `runtime_session_id + actor_user_id` 作为最小 query scope,原因如下: 1. `inventory_slot` 当前是运行态背包真相,不是单个 story session 的临时投影。 2. 同一 `runtime_session` 下的宝箱、任务、战斗奖励都已经按这个 scope 汇入同一张表。 3. 旧前端背包面板语义本质上也是“当前运行态玩家的背包”,不是“某个 story session 的局部背包”。 4. 若后续确实需要更细的 `story_session` 投影,应通过上层 façade 或专门 view 提供,而不是先把真相查询 scope 做窄。 --- ## 4. 接口 contract ### 4.1 请求 - 方法:`GET` - 路径:`/api/runtime/sessions/:runtimeSessionId/inventory` - 认证:必须携带 Bearer JWT - 路径参数: - `runtimeSessionId`:目标运行时会话 ID ### 4.2 成功响应 成功响应延续当前 `api-server` 统一 envelope,`data` 字段结构为: ```json { "runtimeSessionId": "runtime_xxx", "actorUserId": "user_xxx", "backpackItems": [ { "slotId": "invslot_xxx", "containerKind": "backpack", "slotKey": "invslot_xxx", "itemId": "consumable_heal_potion", "category": "消耗品", "name": "疗伤药", "description": "用于恢复少量气血。", "quantity": 3, "rarity": "common", "tags": ["healing"], "stackable": true, "stackKey": "heal_potion", "equipmentSlotId": null, "sourceKind": "treasure_reward", "sourceReferenceId": "treasure_xxx", "createdAt": "2026-04-22T00:00:00.000000Z", "updatedAt": "2026-04-22T00:01:00.000000Z" } ], "equipmentItems": [ { "slotId": "invslot_weapon_xxx", "containerKind": "equipment", "slotKey": "weapon", "itemId": "weapon_trial_blade", "category": "武器", "name": "试作短剑", "description": "一柄适合入门者的短剑。", "quantity": 1, "rarity": "rare", "tags": ["weapon"], "stackable": false, "stackKey": "weapon_trial_blade", "equipmentSlotId": "weapon", "sourceKind": "quest_reward", "sourceReferenceId": "quest_xxx", "createdAt": "2026-04-22T00:00:00.000000Z", "updatedAt": "2026-04-22T00:05:00.000000Z" } ] } ``` ### 4.3 错误响应 当前延续 runtime/profile/story 查询已有策略: 1. `SpacetimeClientError::Runtime(_)` 映射为 `400` 2. 其他 `SpacetimeClientError` 映射为 `502` 3. 错误 `details.provider`: - 参数构建或本地语义错误:`runtime-inventory` - 下游 `SpacetimeDB` 失败:`spacetimedb` --- ## 5. 分层职责 ### 5.1 `module-inventory` 职责: 1. 冻结 `RuntimeInventoryStateQueryInput` 2. 冻结 `RuntimeInventoryStateSnapshot` 3. 冻结 `RuntimeInventorySlotRecord` 4. 负责 builder、validator 与 record 映射 不负责: 1. HTTP 路径解析 2. JWT 鉴权 3. 旧前端背包 view model 编译 ### 5.2 `spacetime-module` 职责: 1. 读取指定 `runtime_session_id + actor_user_id` 下的 `inventory_slot` 2. 按 `container_kind` 拆成 `backpackItems / equipmentItems` 3. 复用 `module-inventory` 的 query input / snapshot 结构 4. 通过 `get_runtime_inventory_state` procedure 返回当前真相态 ### 5.3 `spacetime-client` 职责: 1. 构造 `RuntimeInventoryStateQueryInput` 2. 调用 `get_runtime_inventory_state` 3. 把返回结果映射为稳定 Rust record ### 5.4 `api-server` 职责: 1. 暴露 `GET /api/runtime/sessions/:runtimeSessionId/inventory` 2. 做 Bearer JWT 鉴权 3. 从 token 中注入 `actorUserId` 4. 把 inventory record 映射到 JSON payload --- ## 6. 排序规则 为了保证前端和后续 façade 读到稳定顺序,本轮冻结以下排序口径: 1. `backpackItems` - 先按 `slot_key` - 再按 `slot_id` 2. `equipmentItems` - 先按装备位固定顺序:`weapon -> armor -> relic` - 再按 `slot_id` 当前不在 query 层额外做“按稀有度”“按分类”“按来源”的排序投影。 --- ## 7. 验收口径 本轮验收只要求以下几点: 1. `module-inventory` 已补 inventory query contract 与最小测试 2. `spacetime-module` 已新增 `get_runtime_inventory_state` procedure 3. `spacetime-client` 已能同步读取 inventory 真相态 4. `api-server` 已真实挂出 `GET /api/runtime/sessions/:runtimeSessionId/inventory` 5. `cargo check -p module-inventory -p spacetime-module -p spacetime-client -p api-server` 可通过 6. `npm run check:encoding` 已执行,确保新增中文文档与接口文件没有编码损坏 --- ## 8. 后续边界 这条最小 inventory query 落地后,后续再继续拆下一层: 1. 评估是否需要补 `story session` 局部 inventory projection 2. 评估是否需要把 inventory query 接成旧背包面板 view model 3. 再继续补 `inventory_use / craft / dismantle / reforge` 4. 最后再考虑 inventory subscription / public view 在这些 contract 未冻结前,不应把当前接口误称为“旧背包系统已完整迁移完成”。