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

223 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 构建 / 发布本地模块
```bash
spacetime start
spacetime publish genarrative-local --clear-database -y --module-path ./spacetimedb
```
### 2. 初始化单例配置
初始化 SQL 已放到:
- `scripts/spacetime/init_local_app_config.sql`
执行方式:
```bash
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 代理层