Files
Genarrative/docs/technical/M4_RUNTIME_INVENTORY_STATE_QUERY_DESIGN_2026-04-22.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

7.2 KiB
Raw Blame History

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_slotapply_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. 查询 scoperuntime_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 统一 envelopedata 字段结构为:

{
  "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 未冻结前,不应把当前接口误称为“旧背包系统已完整迁移完成”。