Files
Genarrative/server-rs/crates/spacetime-client
suzmii c732a3f851
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
实现游戏买断制泥点付费的 api-server HTTP 层、公开投影与播放鉴权
- server-rs/crates/api-server/src/runtime_profile.rs:钱包流水来源类型补 GamePurchase 分支并导入 game_purchase 常量,修复 non-exhaustive match 编译错误。
- server-rs/crates/api-server/src/modules/game_distribution.rs:创建版本接受并校验 priceMudPoints(复用 normalize_game_price_mud_points,越界 400),随版本冻结资料一起落库。
- server-rs/crates/api-server/src/modules/game_distribution.rs:新增 POST /api/game-distribution/games/{gameId}/purchase(require_bearer_auth + 必填 Idempotency-Key,请求体 { expectedPriceMudPoints }),经 facade 原子扣费并返回 GameDistributionPurchaseResponse。
- server-rs/crates/api-server/src/modules/game_distribution.rs:购买错误映射——余额不足 400 INSUFFICIENT_MUD_POINTS、价格已变化 409、免费游戏 400、游戏未公开或不存在 404、缺幂等键 400。
- server-rs/crates/api-server/src/modules/game_distribution.rs:新增 POST /api/game-distribution/games/{gameId}/play-session;免费作品直接回既有公开入口,付费作品接受管理员令牌(沿用现有 admin 鉴权,免购买)与用户令牌,校验已购买 / 作者本人,未登录 401、未购买 403,签发绑定 gameId + userId 的 2 小时会话。
- server-rs/crates/api-server/src/modules/game_distribution.rs:新增 GET /api/game-distribution/play-sessions/{token}[/{assetPath}] 网关,无登录中间件、凭令牌读取,能解析平台刷新会话 Cookie 时 403,令牌过期 / 不存在、游戏下架或封禁、缺少有效公开版本一律 404,复用发行包读取与 CSP/MIME/nosniff/CORP 并全程 no-store。
- server-rs/crates/api-server/src/modules/game_distribution.rs:公开发行网关 releases/{gameId} 在当前公开版本 price_mud_points > 0 时返回 404,付费作品只能走播放会话路径。
- server-rs/crates/api-server/src/modules/game_distribution.rs:game_payload 发出 priceMudPoints 与 purchased 默认值;public_game_payload 按查看者解析 purchased,并对未购买且非作者 / 非管理员的付费作品把 currentVersion.entryUrl 置 null;公开详情接口可选鉴权读取当前用户,坏令牌按匿名、购买态读取失败按未购买。
- server-rs/crates/api-server/src/modules/game_distribution.rs:private_version_payload 发出该版本冻结价(历史缺字段为 0);作者 / 管理员版本详情的 game payload 按「冻结价优先、否则游戏行价」。
- server-rs/crates/api-server/src/state.rs:新增付费游戏播放会话的进程内存储与读写方法,读取时清理过期会话。
- server-rs/crates/api-server/src/admin.rs:抽出 resolve_admin_session,新增 try_authenticate_admin_from_headers 供同时接受管理员 / 用户令牌的端点复用,require_admin_auth 的校验与鉴权行为不变。
- server-rs/crates/spacetime-client/src/active.rs:补出 GameDistributionPurchaseRecordInput 的 re-export。
- docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md:登记购买、播放会话、公开详情入口隐藏与公开发行网关的 HTTP 契约。
- 补齐 api-server game_distribution 定向测试:价格校验 400、公开投影入口门禁(未购买 / 已购买 / 作者 / 管理员 / 免费)、冻结价回读、购买错误映射、购买路由鉴权与幂等键、播放会话网关 Cookie / 未知与过期令牌 / 路由挂载。
2026-10-05 17:32:40 +08:00
..
2026-07-17 22:07:52 +08:00

spacetime-client 共享 package 说明

日期:2026-05-01

1. package 职责

spacetime-client 是 SpacetimeDB 客户端适配 package,后续负责:

  1. 生成 bindings 后的客户端访问封装
  2. Axum 与各模块对 reducer、view、订阅的调用适配
  3. 身份透传、连接配置与基础错误处理适配

在 DDD 重构中,本 package 只承接 WP-SC Spacetime Client:

  1. 把 SpacetimeDB 生成绑定转换成 api-server 可消费的 typed facade。
  2. 把 row snapshot / procedure result 转换成 BFF record。
  3. 统一 SDK 调用错误、业务 procedure 错误、缺失快照错误和超时错误。
  4. 不承载领域规则,不直接定义 table / reducer / procedure,不替代 spacetime-module。

当前约束见 ../../../docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md。

2. 当前完成口径

当前目录已不再只是占位。WP-SC Spacetime Client 在当前稳定 SpacetimeDB facade 范围内已经完成收尾:

  1. 通过 npm run spacetime:generate -- --rust-only 生成并纳管公开 Rust bindings。
  2. DbConnection 调用连接池、单一缓存读连接、握手等待、超时和断线清理已封装在 SpacetimeClient 内部;调用池不订阅 read model,HTTP 角色额外使用一条共享连接保存订阅行缓存。
  3. 已稳定的 assets、auth、AI task、Big Fish、Custom World、Puzzle、Runtime/Profile/Save、Story session、combat、inventory、NPC facade 均通过 typed 方法对外暴露。
  4. 生成绑定到 BFF record / module record 的 row snapshot mapper 已集中在 mapper.rs。
  5. SDK 调用错误、reducer 业务错误、procedure 业务错误、缺快照错误和本地输入校验错误已统一收口到 SpacetimeClientError helper。
  6. Story runtime projection source 已复用 runtime inventory typed facade,读取投影不再只依赖 runtime snapshot 中的历史背包 JSON 副本。
  7. Story runtime 投影读取会对历史 currentStory.options 做兼容推断:若旧快照缺少 scope,仍会按 functionId 通过 module-runtime-story 的 option helper 还原为 story / combat / npc 作用域,避免旧存档把读取链路卡死。

confirm_asset_object_and_return 与 bind_asset_object_to_entity_and_return 的调用必须等到 SDK on_connect 回调后再发起。DbConnection::build() 只代表 WebSocket 已经初始化,不代表 SpacetimeDB 身份握手完成;如果过早调用 procedure,本地联调会表现为连接建立但请求长期没有回调,最终等到 idle timeout。

后续新增工作只随 WP-ST 新 table / reducer / procedure 或 row shape 稳定后按领域增量接入,不再把整个 WP-SC 包保持为进行中状态。新增 facade 时必须继续满足:

  1. 不手写 module_bindings 生成物。
  2. 不在 spacetime-client 内新增领域规则。
  3. procedure / reducer shape 稳定后再接 typed facade。
  4. 错误映射继续使用 SpacetimeClientError helper。
  5. mapper 测试或 facade 定向测试随新增场景补齐。

2.1 module_bindings 生成物约束

src/module_bindings 目录下的 Rust 文件统一视为 SpacetimeDB CLI 生成产物,后续维护必须遵守:

  1. 只允许通过仓库根目录 npm run spacetime:generate -- --rust-only 刷新,不允许手工修改。
  2. 不生成私有表绑定,不追加 --include-private;如后端需要读取私有表,应先在 api-server 或模块层补明确 contract,而不是让客户端 crate 直接依赖私有表结构。
  3. 不允许手工对该目录执行散装 rustfmt;若 SpacetimeDB CLI 已生成文件但自身 formatter 在 Windows 下失败,只能由 scripts/generate-spacetime-bindings.mjs 在短临时目录中分批 rustfmt 后同步。
  4. src/lib.rs 已通过 #[rustfmt::skip] pub mod module_bindings; 显式阻止 workspace 级 cargo fmt 继续递归格式化该目录。
  5. Windows 下 SpacetimeDB CLI 2.1.0 的生成后 formatter 可能因为一次性传入过多 Rust 文件路径而失败;仓库脚本会先输出到短临时目录,必要时接管分批格式化,再同步回本目录。

2.1.1 绑定缺文件恢复流程

若 mod.rs 已声明 *_table 模块,但目录内缺少对应 *_table.rs 文件,说明 Rust bindings 刷新不完整。不要手工补 generated code,统一在仓库根目录执行:

npm.cmd run spacetime:generate -- --rust-only

脚本内部会使用 --no-config:仓库根目录的 spacetime.json 同时配置了 TypeScript 与 Rust 两个生成目标,直接追加 --lang / --out-dir 会触发 SpacetimeDB CLI 的多目标参数冲突。

生成后用以下命令确认 mod.rs 声明的模块都有落盘文件:

$modFile = 'server-rs\crates\spacetime-client\src\module_bindings\mod.rs'
$dir = 'server-rs\crates\spacetime-client\src\module_bindings'
$mods = Select-String -Path $modFile -Pattern '^pub mod ([a-zA-Z0-9_]+);' |
  ForEach-Object { $_.Matches[0].Groups[1].Value }
$missing = @()
foreach ($m in $mods) {
  if (-not (Test-Path (Join-Path $dir ($m + '.rs')))) {
    $missing += $m
  }
}
if ($missing.Count -eq 0) { 'missing module files: 0' } else { $missing }

最后至少执行:

cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml

3. 边界约束

  1. spacetime-client 只承接 SpacetimeDB 客户端访问适配,不承接具体业务模块的规则实现。
  2. 业务状态真相仍由 apps/spacetime-module 管理,业务编排由各模块 package 与 apps/api-server 承担。
  3. 不允许把 reducer、view、订阅调用细节重新散落到多个业务模块里各自实现。
  4. 新增 facade 必须等待对应 spacetime-module facade 稳定后再接,不提前假设 row shape。
  5. src/module_bindings/** 是生成产物,只能通过 SpacetimeDB CLI 生成流程刷新。