# 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_` 生成,避免再依赖原先 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 ""` 里执行。 ## 当前已验证状态 已完成: - `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 代理层