Files
Genarrative/docs/technical/SPACETIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md

8.4 KiB
Raw Blame History

SpacetimeDB Server-Node 迁移实现说明

本次落地范围

本次已经把 server-node 里与运行时存档、用户认证状态、自定义世界资料库相关的数据库层迁移到 Rust SpacetimeDB 模块中,模块代码位于 spacetimedb/src/,按职责拆成以下文件:

  • lib.rs
    • 只保留模块入口和生命周期 reducer。
  • types.rs
    • 统一放表结构、枚举、view/procedure 的输入输出类型。
  • config.rs
    • app_config 单例表默认值、读取与客户端配置 view。
  • auth.rs
    • 游客建档、JWT/Identity 建档、短信验证、验证提示事件、踢下线事件、认证审计。
  • runtime.rs
    • 快照、运行时设置、资料库、自定义世界会话、浏览历史、资料统计与相关 view/procedure。

当前模块设计

1. app_config 单例表

原先散落在 Node AppConfig 中、且与认证/验证流程相关的配置,已经迁移到 app_config 单例表。

当前策略:

  • init reducer 会自动插入一行默认配置,主键固定为 id = 1
  • 本地环境通过 spacetime sql 更新这一行
  • 客户端只通过 client_app_config view 读取必要的公开配置,不直接读整张表

2. 默认游客登录

连接进入模块时:

  • client_connected 会按 ctx.sender 自动建档
  • 无 JWT 时默认为 guest
  • 有 JWT 时使用 SpacetimeDB 自带 sender_auth().jwt() 建档,login_provider 标记为 jwt
  • 用户主键按 user_<identity_hex> 生成,避免再依赖原先 Node 自签 access token / refresh token 流程

2.2 内部账户模型状态

当前 STDB 私有实现已经开始显式转向账户语义:

  • 私有 Rust struct 已经切到 Account / AccountIdentity / AccountSession
  • auth.rs / runtime.rs / lib.rs 内部 helper 和生命周期接线也开始改用 account 语义命名
  • 公开 view / procedure 名称暂时保持不变,但当前 schema 字段已经切到 account_id / owner_account_id
  • 前端 TypeScript bindings 已经重新生成并同步适配了 account_id 语义

2.1 当前统一账户策略

当前已经开始按“统一账户,多设备会话”方向调整:

  • 新建账户时,账户主键已经不再直接使用连接 identity而是生成独立 acct_* 账户 id
  • 当前设备在短信验证时,如果手机号已命中已有用户,不再直接报“手机号已绑定其他账号”
  • 当前连接的 identity / session 会被归并到这个已有手机号用户
  • 当前游客账户下的快照、设置、自定义世界、游玩统计、浏览历史等运行时数据也会一起迁移到目标账户
  • 这样同一个手机号可以在多个设备上同时建立会话,并归到同一个用户主体下

当前限制:

  • 归并的是“当前连接身份”和“当前会话”
  • 当前的账户数据迁移是规则式合并,不是全量业务语义级合并
  • 例如看板/游玩统计用了保守合并策略,自定义世界同名冲突按更新时间取新
  • 也就是说,统一账户主语义已经开始生效,但后续仍值得补一轮更细的并档策略

3. 短信验证门禁

当前行为已经按你的要求落地:

  • 是否需要短信验证由 app_config.sms_verification_required 决定
  • 除短信发送 / 短信校验 procedure 外,其余 runtime procedure 统一走门禁
  • 若用户未完成短信验证:
    • 先发 verification_prompt_event
    • 再发 kick_event
    • procedure 直接返回 kicked = true

4. 登录即弹验证窗

client_connected 中新增了这条行为:

  • 如果命中“已存在用户,且未完成短信验证”
  • 立即发 verification_prompt_event
  • 前端订阅到事件后即可弹出手机号验证窗口

5. 客户端同步方式

遵循“尽量不公开表”的要求,当前数据同步面以 view 为主:

  • client_app_config
  • my_auth_state
  • my_auth_audit_logs
  • my_account_sessions
  • my_auth_risk_blocks
  • my_snapshot
  • my_runtime_settings
  • my_profile_dashboard
  • my_profile_wallet_ledger
  • my_profile_played_worlds
  • my_browse_history
  • my_custom_world_profiles
  • my_custom_world_sessions
  • published_custom_world_gallery

当前保留为 public event table 的只有两类事件:

  • verification_prompt_event
  • kick_event

这是因为客户端要订阅并即时响应事件,而 event table 不能被 view 读取。

6. 账户弹窗同步面

为了承接客户端账户弹窗,本轮补了:

  • my_account_sessions
    • 用于读取当前账号关联的会话列表
  • my_auth_risk_blocks
    • 用于读取当前账号手机号/IP 对应的保护记录
  • lift_my_risk_block
    • 用于当前账号自助解除手机号或当前连接 IP 的保护
  • revoke_user_session
    • 用于撤销指定会话,并通过事件通知目标连接断开
  • logout_all_user_sessions
    • 用于撤销当前账号全部会话,并广播撤销事件
  • session_revocation_event
    • 用于通知目标连接当前 session 已失效

说明:

  • 当前 session 已按连接维度追踪,不再只是 identity 级别的审计记录
  • 被撤销的连接在再次调用受保护 procedure 时会被踢下线
  • 前端也会订阅 session_revocation_event,在目标 session 被撤销时主动断开当前连接

本地初始化

1. 构建 / 发布本地模块

spacetime start
spacetime publish genarrative-local --clear-database -y --module-path ./spacetimedb

2. 初始化单例配置

初始化 SQL 已放到:

  • scripts/spacetime/init_local_app_config.sql

执行方式:

spacetime sql genarrative-local "$(tr '\n' ' ' < scripts/spacetime/init_local_app_config.sql)"

如果你不想走命令替换,也可以直接把 SQL 内容整段复制到 spacetime sql genarrative-local "<SQL>" 里执行。

当前已验证状态

已完成:

  • cargo check
  • spacetime build

说明:

  • 当前模块可以通过 Rust 编译和 Spacetime 模块构建
  • spacetime.json 已切到 TypeScript 绑定输出目录 src/spacetime/generated
  • 前端已开始接入 Spacetime 连接与绑定生成代码

当前客户端改造

本轮已经把前端里最核心的账号与运行时存储链路切到 Spacetime

  • src/spacetime/client.ts
    • 统一维护浏览器端 Spacetime 连接、订阅和事件桥。
  • src/spacetime/mappers.ts
    • 负责把生成绑定里的 view/event 数据映射回现有前端契约类型。
  • src/services/authService.ts
    • 已从 /api/auth/* 切到 Spacetime 连接、view、procedure。
  • src/services/storageService.ts
    • 已从 /api/runtime/* 的存档/设置/资料库接口切到 Spacetime。
  • src/services/authService.ts
    • 现在也会读取 my_account_sessions / my_auth_risk_blocks,并调用 lift_my_risk_block
  • src/components/auth/AuthGate.tsx
    • 已改成默认游客建连,并监听 verification_prompt_event / kick_event
  • src/components/auth/PhoneVerificationModal.tsx
    • 新增短信验证弹窗,收到验证提示事件后直接弹出。

当前客户端行为

  • 页面启动后会直接连到 SpacetimeDB并复用/写回 Spacetime token
  • 若账号未完成短信验证:
    • 连接阶段收到 verification_prompt_event 会弹出验证窗
    • 调用受保护 procedure 后收到 kick_event 也会重新弹出验证窗
  • 存档、设置、个人看板、浏览历史、自定义世界资料库与作品广场已经改走 Spacetime
  • 账户弹窗里的“当前安全状态 / 会话列表”已经开始读取真实 STDB view
  • 账户弹窗里的“移除设备 / 全部退出”已经开始调用真实 STDB procedure
  • 作品广场详情为支持客户端恢复完整 profile新补了 published_custom_world_profiles view

当前仍保留的旧链路

  • /api/runtime/story/* 相关故事运行时接口
  • AI / 资源生成相关 Express 路由
  • 账号弹窗中的“解除保护 / 移除设备 / 全部退出”都已经走真实 STDB procedure
  • runtimeStoryService.ts 这条故事运行时链路仍未迁移,还是当前最大的遗留

前端环境变量

请在本地配置:

  • VITE_SPACETIME_URI
  • VITE_SPACETIME_DATABASE_NAME

示例已补到 .env.example

后续建议

下一步如果继续推进,建议按这个顺序:

  1. 前端接入 verification_prompt_event / kick_event
  2. 补齐账号弹窗需要的 session / risk block view 与对应操作 procedure
  3. 把原 /api/runtime/story/* 运行时动作接口继续向 Spacetime 迁移
  4. 再决定是否保留 server-node 作为纯 HTTP/AI 代理层