36 KiB
基于 SpacetimeDB + Axum + 阿里云 OSS 的后端重写设计文档
日期:2026-04-20
1. 文档定位
这份文档不是继续扩写当前 server-node/ 的实现细节,而是基于当前仓库已经落地的后端能力,为下一版 Rust 后端提供一份可以直接落地编码的重写设计。
目标很明确:
- 保留当前项目已经具备的后端能力面,不做需求缩水。
- 把后端实现从
Express + PostgreSQL + 本地 public/generated-* 文件重写为:Axum:唯一 HTTP 边界层与流式接口层SpacetimeDB:唯一运行时状态与实时订阅真相源阿里云 OSS:唯一大文件与二进制资产存储
- 让前端在第一阶段尽量少改,优先兼容当前
/api/*、/healthz、SSE 与资源路径习惯。
2. 当前工程必须继承的能力基线
以下能力清单来自 2026-04-20 迁移设计时对旧 Node 后端的快照整理,只作为迁移参考,不再作为新功能扩展依据。后续实现方向以 docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md 为准。
- 对外挂载面:
6个 - 已登记路由:
96条 - 内部模块目录:
12个 - 公开接口:
10条 - JWT 接口:
69条 - 环境开关接口:
17条 - 流式接口:
6条
当前 Node 后端的历史基线仍然是这 6 个挂载面,但自 2026-04-21 起,本轮 Rust 后端重写的 active rewrite target 固定为其中 5 个:
healthauthassetsruntime-mainruntime-story-action
补充说明:
editor挂载面在历史系统中真实存在,但已被确认为遗留无用能力。editor仅保留为历史基线对照,不纳入本轮server-rs重写验收。- 当前执行顺序允许在
M3 / M4 / M5前,先前置assets / OSS的基础设施接入,以便后续 runtime、custom world、agent 统一复用同一资产入口。
当前后端内部模块也不能“凭感觉重设计”,而要按现有职责做映射:
aiassetscombatcustom-worldeditorinventorynpcprogressionquestruntimeruntime-itemstory
其中:
- 上述
12个模块是历史基线总量。 - 本轮 active rewrite modules 固定为
11个。 editor仅保留历史事实,不进入server-rs主线 crate 与阶段验收。
3. 技术选型后的硬边界
3.1 SpacetimeDB 的平台约束与本项目边界
根据 SpacetimeDB 官方 Rust crate 文档,reducer 不能直接使用 std::net 或 std::fs 进行外部 IO;而 2.0 官方文档又提供了 procedure + ctx.http 的受控 HTTP 能力。
也就是说,平台层面并不是完全不能做外部调用,但对于本项目这次重写,我们仍然主动把下面这些副作用统一放到 Axum,而不是塞进 SpacetimeDB:
- 阿里云 OSS 上传、下载、签名
- DashScope / Ark / 其他 LLM 请求
- 微信 OAuth
- 短信验证码
- 本地文件系统读写
这样设计不是因为“SpacetimeDB 绝对做不到”,而是因为这类能力都要求:
- 更强的重试、超时、日志和熔断能力
- 更自由的 SDK / multipart / 签名上传实现
- 与 HTTP 头、cookie、回调、对象存储策略深度耦合
- 与游戏状态 schema 解耦,避免把第三方供应商能力直接绑进数据库模块发布周期
结论:
SpacetimeDB负责状态、规则、订阅、命令执行与读模型。Axum负责所有外部副作用与 HTTP 协议。- 在本次重写中,不为 SpacetimeDB module 增加任何外部副作用例外通道。
3.2 Axum 的边界
根据 Axum 官方文档,Router<S> 通过 .with_state(...) 注入共享状态后才成为可真正 serve() 的路由树;因此 Axum 适合作为:
- 统一 HTTP 入口
- Bearer / Cookie 鉴权入口
- SSE 输出入口
- OSS 上传凭证签发入口
- 与 SpacetimeDB 的应用层编排入口
结论:
- Axum 是唯一 BFF / API Gateway。
- 前端第一阶段仍然只认识 Axum,不直接依赖 SpacetimeDB 原生接口。
- 在
M0 ~ M6迁移期内,不新增前端直连 SpacetimeDB 的主链方案。
3.3 OSS 的边界
根据阿里云 OSS 官方文档:
PostObject适合浏览器表单直传,服务端可以下发policy、signature与上传条件。STS临时访问凭证适合把上传权限以有限时、有限范围的方式下发给客户端。PutObject/PostObject都支持x-oss-meta-*元数据与标签。
结论:
- 所有图片、动画、精灵表、场景图、封面图、视频参考素材都存 OSS。
- SpacetimeDB 只存
bucket、对象键、版本、尺寸、状态、逻辑元数据,不存二进制。 - Axum 负责下发直传凭证、校验上传结果、补写元数据。
4. 目标总体架构
Web / Mobile Frontend
├─ 继续访问 /api/*、/healthz、/generated-*
└─ 第一阶段不直接依赖 SpacetimeDB 原生协议
Axum API Server
├─ auth:登录、refresh cookie、JWT/OIDC 签发
├─ runtime facade:兼容当前 REST / SSE contract
├─ asset gateway:OSS 直传签名、对象确认、媒体任务编排
├─ ai gateway:DashScope / Ark / 其他模型调用
├─ background workers:异步作业执行与回写
└─ SpacetimeDB client:调用 reducer、查询 view / public table、订阅任务状态
SpacetimeDB Module
├─ auth tables:用户、身份、session、风控、审计
├─ runtime tables:存档、设置、浏览历史、个人面板
├─ gameplay tables:story / npc / quest / inventory / combat / progression
├─ custom-world tables:问答会话、agent 会话、草稿卡、操作记录
├─ asset metadata tables:生成任务、对象清单、引用关系
├─ reducers:唯一状态写入口
└─ views / public tables:唯一读模型与订阅面
Aliyun OSS
├─ generated-character-drafts
├─ generated-characters
├─ generated-animations
├─ generated-custom-world-scenes
├─ generated-custom-world-covers
├─ generated-qwen-sprites
└─ temp-uploads / workflow-cache
5. 重写后的核心原则
5.1 先兼容当前 API 面,再逐步让前端吃到实时能力
第一阶段不要强推前端直接改成 SpacetimeDB 客户端模式,而是:
- Axum 保持当前
/api/*路由空间。 - Axum 保持当前
x-request-id / x-api-version / x-route-version头和响应 envelope。 - Axum 保持当前 story / custom-world-agent 的 SSE 体验。
- SpacetimeDB 先做后端内部真相源。
补充执行口径:
- 虽然总体里程碑仍保留
M6编号,但OSS的平台适配、浏览器直传票据与旧/generated-*路径兼容能力允许提前于M3 / M4 / M5落地。 - 提前落地的目标是先收口统一资产入口,不是提前把全部资产业务状态迁完。
第二阶段再按模块把只读页改成直接订阅 SpacetimeDB。
5.2 命令与读模型分离
SpacetimeDB 官方文档明确说明:
- reducer 是唯一可修改表的入口。
- reducer 不直接返回业务数据给客户端。
- view 可被查询与订阅,并会随底层表变化自动更新。
因此本项目必须采用:
Reducer = 命令入口View / Public Table = 读模型入口Axum = HTTP 兼容层与聚合层
5.3 大对象不进数据库
当前 Node 后端把生成结果落在 public/generated-*。重写后统一改成:
OSS存二进制SpacetimeDB存引用和状态Axum输出 URL、签名 URL 或 CDN URL
5.4 Schema 必须按 SpacetimeDB 的迁移约束设计
SpacetimeDB 官方文档对自动迁移的限制很强:
- 删表、改列类型、改列名、调整列顺序都不是安全日常操作。
- 新列只能追加到表末尾,且要提供默认值。
- reducer 改名或删除会直接影响客户端调用。
因此本项目的数据模型必须尽量满足:
- 主表稳定、字段追加式演进
- 高频变化数据优先事件表化
- 聚合结果优先投影表 / view,而不是频繁重塑旧表结构
5.5 主工程必须按多 crate 方式组织模块
从当前版本开始,Rust 后端固定采用“主工程 crate + 独立模块 crate”的方式组织。
这里再明确一层:
crates/只是工作区下统一承载 Rust crate 的目录名。- 目录里的每个独立单元在 Rust 语义上都按 workspace crate 对待。
组织规则固定为:
crates/api-server作为 Axum 主工程 crate,只负责协议装配与模块组合。crates/spacetime-module作为 SpacetimeDB 主工程 crate,只负责聚合各模块 crate 的表、reducer、view。- 每个独立业务模块必须优先拥有自己的 workspace crate,再由主工程 crate 引用。
- 只有共享 contract、共享领域内核、平台适配、SpacetimeDB client 这类跨模块能力,才允许使用共享 crate。
这样做的目的,是避免把当前 12 个既有模块边界重新压缩回单个“大 application crate”或“大 domain crate”中,确保后续重写能继续按模块独立演进。
5.6 SpacetimeDB 相关修改的执行约束
从当前版本开始,凡是涉及 SpacetimeDB 的设计、实现、脚本、调试与前端接入,统一要求显式使用以下 skill 作为执行依据:
执行要求:
- 涉及
spacetimeCLI、发布、绑定生成、本地联调时,优先按spacetimedb-cli约束执行。 - 涉及
crates/spacetime-module的表、reducer、view、Rust API 使用时,优先按spacetimedb-rust与spacetimedb-concepts约束执行。 - 涉及前端或 Node 侧的 SpacetimeDB 绑定、订阅、TypeScript SDK 接入时,优先按
spacetimedb-typescript与spacetimedb-concepts约束执行。 - 若 skill 约束与仓库内已有旧实现存在冲突,必须先以 skill 约束校正设计文档与实现方案,再继续编码,避免沿用已过时或幻觉式 API。
6. 推荐工程结构
本次重写固定在仓库根目录新增 Rust 工作区 server-rs/,并与 server-node/ 同级:
server-rs/
├─ Cargo.toml
├─ crates/
│ ├─ api-server/ # Axum 主工程 crate,负责装配路由、中间件、SSE 与模块引用
│ ├─ spacetime-module/ # SpacetimeDB 主工程 crate,负责聚合表、reducer、view 并发布 wasm
│ ├─ module-auth/ # 鉴权与会话模块 crate
│ ├─ module-runtime/ # runtime snapshot / settings / profile 模块 crate
│ ├─ module-story/ # story 主循环模块 crate
│ ├─ module-combat/ # 战斗规则模块 crate
│ ├─ module-inventory/ # 背包与奖励模块 crate
│ ├─ module-npc/ # NPC 状态与对话模块 crate
│ ├─ module-progression/ # 成长与章节推进模块 crate
│ ├─ module-quest/ # 任务运行时模块 crate
│ ├─ module-runtime-item/ # 运行时物品模块 crate
│ ├─ module-custom-world/ # 自定义世界与 agent 模块 crate
│ ├─ module-assets/ # 资产任务与对象绑定模块 crate
│ ├─ module-ai/ # AI 编排模块 crate
│ ├─ shared-contracts/ # HTTP DTO / SSE event / 前后端兼容 contract
│ ├─ shared-kernel/ # 跨模块共享领域类型、ID、枚举、值对象
│ ├─ shared-logging/ # 工作区统一日志初始化与 tracing subscriber 基础设施
│ ├─ platform-auth/ # JWT、cookie、provider adapter
│ ├─ platform-oss/ # OSS 直传、签名、对象管理
│ ├─ platform-llm/ # DashScope / Ark / 其他模型适配
│ ├─ spacetime-client/ # 生成 bindings 后的 DB client adapter
│ └─ tests-support/ # 集成测试、contract 测试、smoke 支撑
└─ scripts/
├─ dev.sh / dev.ps1
├─ check.sh / check.ps1
├─ spacetime-dev.sh / spacetime-dev.ps1
└─ smoke.sh / smoke.ps1
目录职责约束:
crates/api-server/只做协议装配、鉴权、中间件、handler 与模块组合,不把业务模块重新堆回单包。crates/spacetime-module/只负责聚合各模块 crate 的状态模型,不直接承接外部副作用。crates/module-*保持与当前业务模块边界一一对应,已明确退出本轮的editor遗留模块除外;必要时可在 crate 内部再拆application、domain、spacetime子层次。crates/shared-contracts/负责与当前前端兼容的 JSON / SSE 协议。crates/shared-kernel/只放跨模块复用的数据结构和规则,不碰框架。crates/shared-logging/负责统一日志初始化、过滤器解析与 subscriber 基础设施,不承接 HTTP 业务语义。crates/platform-*统一承接三方供应商与平台适配。
命名补充说明:
- 本文后续若出现
auth-service、oss-service、llm-service、application::...等历史逻辑名,统一视为职责标签,而不是强制要求继续存在同名顶层目录。 - 在新的多 crate 版本中,这些职责会落到
crates/module-*内部子层次,或落到crates/platform-*、crates/shared-*等共享 crate 中。
7. 目标模块映射
| 当前模块 | 重写后主归属 | 次归属 | 说明 |
|---|---|---|---|
auth |
Axum auth-service + SpacetimeDB private tables |
无 | 登录入口、refresh cookie、JWT/OIDC、审计与风控拆为“Axum 处理副作用 + SpacetimeDB 落状态”。 |
runtime |
SpacetimeDB module | Axum facade | 存档、设置、浏览历史、profile dashboard 统一进入 SpacetimeDB。 |
story |
SpacetimeDB module | Axum SSE facade | story action、状态推进、可选项构造、恢复态读取以后端 reducer/view 为准。 |
combat |
SpacetimeDB module | 无 | 纯规则计算,天然适合 reducer。 |
inventory |
SpacetimeDB module | 无 | 背包、赠礼、交易、物品副作用全部 reducer 化。 |
npc |
SpacetimeDB module | Axum for LLM dialogue | 关系、招募、状态机在 SpacetimeDB;LLM 台词生成留在 Axum。 |
progression |
SpacetimeDB module | 无 | 关卡、等级、敌对 scaling、章节推进都做领域表和 reducer。 |
quest |
SpacetimeDB module | Axum for AI quest drafting | 任务主状态在 SpacetimeDB;生成型内容由 Axum 生产后回写。 |
runtime-item |
SpacetimeDB module | Axum for AI intent | 物品奖励和解析归 reducer;AI 意图生成由 Axum 负责。 |
custom-world |
SpacetimeDB module + Axum orchestration | OSS | 会话、草稿、agent 状态放 SpacetimeDB;世界编译、资产生成、发布编排在 Axum。 |
ai |
Axum llm-service |
SpacetimeDB task tables | 外部模型调用全部放 Axum。 |
assets |
Axum oss-service |
SpacetimeDB asset metadata | 二进制进 OSS,元数据进 SpacetimeDB。 |
补充说明:
- 历史
editor模块不纳入server-rs本轮重写。 - 相关
/api/editor/*与server-node/src/modules/editor仅保留为旧系统对照事实,后续若要清理再单独立项。
8. 数据建模方案
8.1 表的分层
建议在 SpacetimeDB 中至少拆成 5 组表:
A. 身份与会话表
user_accountauth_identityrefresh_sessionauth_audit_logauth_risk_blocksms_auth_eventwechat_auth_state
说明:
- 这些表默认都应为 private。
- 密码哈希、短信验证码校验、微信 code 换 token 的动作在 Axum 完成。
- Axum 再调用 reducer 写入最终结果。
user_account 的详细字段、状态迁移与旧 users 映射规则,见:
auth_identity 的 provider 约束、唯一键与手机号/微信身份写入规则,见:
refresh_session 的 cookie/hash 边界、轮换与吊销语义,见:
auth_audit_log 的事件范围、追加写规则与 DTO 派生约束,见:
auth_risk_block 的作用域、活跃态与解除规则,见:
sms_auth_event 的事件范围、发送/校验写入规则、统计口径与和风控/审计表的边界,见:
wechat_auth_state 的字段、过期时间、授权场景、callback 单次消费与清理策略,见:
B. 运行时主状态表
runtime_snapshotruntime_settingprofile_dashboard_stateprofile_wallet_ledgerprofile_played_worldprofile_save_archiveuser_browse_history
说明:
runtime_snapshot继续作为“恢复游戏”的主聚合表。profile_*作为只读页投影表,避免每次从大快照现算。browse_history、save_archive单独建表,不要藏进 snapshot blob。
C. Gameplay 领域表
story_sessionstory_eventnpc_statequest_recordinventory_slottreasure_recordbattle_stateplayer_progressionchapter_progression
说明:
story_action不直接改大 JSON,而是 reducer 驱动多个领域表。- 是否继续保留兼容快照,可由投影 reducer 汇总生成。
- 旧前端仍需要
gameState + currentStory时,由 Axum 聚合成兼容 DTO。
D. Custom World 表
custom_world_profilecustom_world_sessioncustom_world_agent_sessioncustom_world_agent_messagecustom_world_agent_operationcustom_world_draft_cardcustom_world_asset_linkcustom_world_gallery_entry
说明:
- 传统问答流与 Agent 流不再混一个 payload。
- 卡片、操作、消息必须拆表,不能再都塞进一个大 JSON 会话体。
- 公开画廊作为独立投影,避免从 profile 运行时拼装。
E. 资产与对象元数据表
asset_jobasset_objectasset_manifestcharacter_visual_assetcharacter_animation_assetscene_image_assetsprite_sheet_asset
说明:
- 任务状态在 SpacetimeDB。
- 二进制对象在 OSS。
asset_object的正式真相字段固定为bucket + object_key。- 所有 URL 都只作为派生读模型,不作为对象主键存储。
asset_object的首版字段、访问级别与索引设计见:
8.2 public / private 原则
依据 SpacetimeDB 官方访问权限文档:
- private table 默认只给 reducer / view / owner 看。
- public table 才适合直接查询和订阅。
本项目建议:
auth_*、refresh_session、风控、验证码全部 privateruntime_snapshotprivatestory_sessionprivatecustom_world_agent_*绝大多数 privategallery、公共角色卡、公共作品索引可 public- 如需“用户自己的读模型”,优先用
ViewContextview 暴露
8.3 view 设计原则
依据 SpacetimeDB 官方 view 文档:
- view 可以被查询和订阅,底层表变化时会自动更新。
AnonymousViewContext可被所有用户共享物化结果,性能明显优于按用户单独计算。- view 不适合全表
.iter()扫描,应该通过索引查找或返回可分析的 query。
所以本项目的 view 设计规则必须是:
- 画廊、排行榜、商店、公共世界列表,优先匿名 view。
- “我的背包 / 我的档案 / 我的当前会话”这类按用户隔离的读模型,才用带身份上下文的 view。
- 首页、资料库、历史记录、存档列表都必须先有索引,再写 view。
9. 鉴权设计
9.1 总体方案
建议保留当前“密码 / 手机验证码 / 微信”三类登录能力,但把实现改成:
- Axum 负责登录副作用:
- 密码校验
- 短信发送与校验
- 微信 OAuth code 交换
- refresh cookie 签发与轮换
- Axum 负责签发OIDC 兼容 JWT
- 同一张 JWT 同时用于:
- Axum 自身 Bearer 鉴权
- SpacetimeDB reducer / view 的身份透传
这是可行的,因为 SpacetimeDB 官方文档说明其可以从 OIDC 兼容 JWT 中提取身份声明,并在模块上下文中使用这些 claims。
9.2 Token 设计
建议:
iss:固定为 Axum 网关身份发行者,例如https://api.genarrative.example/authsub:稳定用户 ID- 扩展 claims:
sid:session idprovider:password / phone / wechatrolesphone_verifieddisplay_name
iss/sub/sid/provider/roles/ver/phone_verified/binding_status 的字段定义、哪些字段禁止进入 JWT、以及 Axum 与 SpacetimeDB 的使用边界,见:
9.3 Refresh Session
建议保留当前模式:
- 浏览器短期 Bearer access token
- HttpOnly refresh cookie
- refresh session 服务端可吊销
但新的会话 ledger 写入 SpacetimeDB private 表即可。
10. Axum 侧设计
10.1 进程职责
Axum 进程建议拆成以下子系统:
http::middleware- request id
- tracing
- envelope / 错误标准化
- auth extractor
http::routes/healthz/api/auth/*/api/runtime/*/api/runtime/story/*/api/assets/*
application::services- story facade
- runtime snapshot facade
- custom world facade
- asset facade
infra- SpacetimeDB client
- OSS adapter
- DashScope / Ark adapter
- SMS / WeChat adapter
10.2 流式接口
当前项目已经有 6 条流式接口,重写时不建议一上来全部改成 WebSocket。
第一阶段建议:
- 继续使用 Axum SSE 输出流式文本与阶段事件。
- Axum 在处理流式过程中,持续把阶段状态写回 SpacetimeDB。
- 前端仍按当前 SSE 协议消费。
适合继续保留为 Axum SSE 的场景:
story/initialstory/continuechat/character/*chat/npc/*custom-world/generate/streamcustom-world/agent/messages/stream
10.3 兼容 contract
Axum 第一阶段需要兼容当前项目的这些约定:
- 路径空间不变
x-request-idx-api-versionx-route-versionx-response-time-ms- 可选 envelope:
x-genarrative-response-envelope
这样前端可以在不大改 src/services/* 的前提下切换后端实现。
11. OSS 设计
11.1 对象键规划
建议统一对象键前缀,保持与当前前端路径习惯接近:
generated-character-drafts/{character_id}/{job_id}/{file}
generated-characters/{character_id}/visual/{asset_id}/{file}
generated-animations/{character_id}/{animation_set_id}/{action}/{file}
generated-custom-world-scenes/{profile_id}/{landmark_id}/{asset_id}/{file}
generated-qwen-sprites/{role_id}/{sheet_id}/{file}
generated-custom-world-covers/{profile_id}/{asset_id}/{file}
workflow-cache/{workflow_type}/{workflow_id}.json
11.2 上传模式
建议:
- 前端直传图片、封面、小文件:
PostObject - 大文件或需要细粒度控制时:
STS + PutObject / Multipart - 生成型资产:Axum worker 直接上传 OSS
其中:
PostObject用于浏览器上传时,Axum 负责生成 policy 与 signature。- policy 中必须明确:
- key 前缀
- content-type 白名单
- content-length-range
- success_action_status
当前已落地的最小实现补充:
server-rs/crates/platform-oss已提供PostObject直传签名能力。server-rs/crates/api-server已暴露POST /api/assets/direct-upload-tickets。server-rs/crates/platform-oss已提供私有对象GET短期签名 URL 能力。server-rs/crates/api-server已暴露GET /api/assets/read-url。- 上传接口当前输出:
bucketobjectKeylegacyPublicPathformFieldsexpiresAt
- 读取接口当前支持:
objectKeylegacyPublicPathexpireSeconds
- 当前 bucket 已明确为私有读写,因此
publicUrl不再作为正式对象真相输出。 - 当前签名链路优先兼容旧公开前缀:
/generated-character-drafts/*/generated-characters/*/generated-animations/*/generated-custom-world-scenes/*/generated-custom-world-covers/*/generated-qwen-sprites/*
- 当前
POST /api/assets/sts-upload-credentials已按“服务器上传、Web 只下载”的需求固定为禁用式 contract,不向浏览器下发 OSS 写权限。 - 当前
platform-oss已提供服务端PutObject上传 helper,供后续 AI worker 上传生成资源后继续走对象确认链路。
11.3 元数据与标签
建议所有业务对象写入统一元数据:
x-oss-meta-owner-user-idx-oss-meta-profile-idx-oss-meta-entity-idx-oss-meta-asset-kindx-oss-meta-source-job-idx-oss-meta-content-hashx-oss-meta-origin
注意:
- OSS 官方文档要求自定义元数据使用
x-oss-meta-*前缀。 - 所有元数据总大小不能超过
8 KB。
11.4 URL 策略
建议:
- 业务表里统一存
bucket + object_key - 对外输出
cdn_url或签名 URL - 私有对象默认输出短期签名 URL,而不是假设匿名公开读
为了兼容当前前端相对路径使用习惯,第一阶段可以让 Axum 或 CDN 兼容以下历史前缀:
/generated-character-drafts/*/generated-characters/*/generated-animations/*/generated-custom-world-scenes/*/generated-custom-world-covers/*/generated-qwen-sprites/*
补充约束:
- 当前
xushi-devbucket 已明确为私有读写,因此这些旧前缀在第一阶段只代表兼容路径习惯,不代表对象可匿名读取。 - Web 端若拿到的是历史
/generated-*路径,必须先调用GET /api/assets/read-url换取signedUrl,不能直接把该路径当成正式可读 URL。 - 前端工程内凡是图片、背景图、封面图、角色图、场景图等展示入口,只要可能接收到
/generated-*,都必须统一走资源解析层:- 列表/卡片/普通
<img>优先复用src/services/assetReadUrlService.ts - 组件内优先复用
src/hooks/useResolvedAssetReadUrl.ts - 通用图片标签优先复用
src/components/ResolvedAssetImage.tsx - 当前已完成的高优先级入口包括:
CharacterAnimator、CharacterPanel、CompanionCampModal、CharacterSelectionFlow、MapModal、GameCanvasSceneLayer、GameCanvasShared、GameCanvasEntityLayer、CustomWorldResultView、CustomWorldEntityEditorModal、CustomWorldRoleAssetStudioModal、CustomWorldAgentDraftDetailPanel、PlatformHomeView、PlatformWorldDetailView、QwenSpriteSheetTool
- 列表/卡片/普通
- 对私有 OSS 资源,前端在签名地址返回前不能先回退渲染原始
/generated-*路径,否则浏览器会先发起一次无签名请求并触发403。 - 具体对象引用设计见:
12. 关键业务流设计
12.1 Story Action
目标:
storyActionService当前承担的跨模块结算必须迁到 SpacetimeDB reducer。- Axum 只做 request parse、auth、调用 reducer、读取 view、拼回当前前端响应。
推荐流程:
- 前端
POST /api/runtime/story/actions/resolve - Axum 校验请求并附带 JWT
- Axum 调用 SpacetimeDB
resolve_story_actionreducer - reducer 内部联动:
storycombatinventorynpcquestruntime-itemprogression
- reducer 写回领域表
- Axum 再读取
current_story_view、runtime_snapshot_view - Axum 返回兼容当前前端的
RuntimeStoryActionResponse
12.2 存档与恢复
目标:
- 仍保留当前“完整恢复游戏”的能力。
- 但底层不再由 PostgreSQL 单大 JSON 承担全部职责。
建议:
runtime_snapshot继续保留兼容聚合快照,满足现有恢复链路。- gameplay 真相由领域表维护。
- 每次重要 reducer 提交后,异步或同步刷新 snapshot projection。
这样可以兼顾:
- 旧前端兼容
- 新后端可审计
- SpacetimeDB 实时订阅能力
12.3 Custom World Agent
当前 Node 后端中,customWorldAgentOrchestrator + SessionStore + Operation 已经是一条清晰主链。
重写后建议进一步正规化:
- SpacetimeDB:
- 存会话
- 存消息
- 存卡片
- 存操作状态
- 存草稿 profile
- Axum:
- 调 LLM
- 调图片生成
- 调 OSS
- 发 SSE
- 把阶段结果回写 reducer
不再允许“一整个 agent 会话对象 JSON 一把写回”作为长期形态。
12.4 资产生成
资产链路拆成四步:
- Axum 创建
asset_job - Axum worker 调外部模型
- 产物上传 OSS,写
asset_object - Axum 调 reducer 将对象绑定到:
- 角色
- 地点
- 世界草稿
- Qwen sprite sheet
所有“对象是否已经被业务引用”的事实,以 SpacetimeDB 绑定表为准,而不是以 OSS 是否存在某个 key 为准。
13. 迁移阶段建议
迁移期仓库边界补充约束:
- 旧
server-node/在M0 ~ M6期间继续保留,作为协议对照、回归基线与回退锚点。 - 只有在
M7切流、回归、回退方案都稳定后,才评估是否清理旧 Node 后端。
Phase 0:冻结能力清单
交付:
- 固定当前
96条接口为重写验收基线 - 固定当前
12个模块为迁移映射基线 - 固定当前前端 contract 与 SSE 协议
Phase 1:先搭 Axum 外壳与鉴权
交付:
/healthz/api/auth/*- response envelope
- request id / tracing
- Axum -> SpacetimeDB 基础 client
- OIDC-compatible JWT 签发
阶段执行补充:
- 微信登录链路在当前阶段暂缓,不进入连续执行顺序。
- 当前优先顺序固定为:JWT / refresh cookie / 密码登录 / 手机验证码登录。
Phase 2:迁移 runtime snapshot / settings / profile
交付:
- 存档
- 设置
- 浏览历史
- profile dashboard
- save archives
这是最容易先跑通的闭环。
Phase 3:迁移 story action 主循环
交付:
- story reducer
- combat / inventory / npc / quest / runtime-item / progression 联动
/api/runtime/story/*
这一阶段完成后,运行时真相就真正从 Node 版切出去了。
Phase 4:迁移 custom world 与 agent
交付:
- legacy custom world session
- custom world library / gallery
- custom world agent 会话、卡片、操作
- scene npc / entity generation
Phase 5:迁移 assets / OSS
交付:
- OSS 直传
- 生成任务
- 对象元数据
- 旧
/generated-*路径兼容
补充说明:
editor已于2026-04-21被确认为遗留无用模块,退出本轮 Rust 后端重写范围。- Phase 5 只覆盖资产与 OSS 主链,不再包含 editor 迁移。
Phase 6:联调、回归、部署与切流收口
交付:
- 联调与回归测试体系
- 灰度环境、切流开关、回退方案
- tracing / request id / 关键链路观测
- 拆分
server-rs/crates/spacetime-module/src/lib.rs,按业务模块与 SpacetimeDB 的table / reducer / procedure / view结构重组为runtime、gameplay::{story/combat/inventory/npc/quest/runtime_item/progression}、custom_world、asset_metadata、ai等聚合子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口
阶段执行补充:
- 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。
- 拆分后的目录与模块边界必须对齐
M0已冻结的模块迁移归属,避免spacetime-module回退成“单大文件 + 单大包”结构。 - 拆分完成后至少要保持
cargo check、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。
14. 验收标准
重写完成至少要满足:
- 当前
96条已登记路由全部有对应实现或明确兼容替代。 - 当前历史
6个挂载面的迁移去向全部明确,且本轮 active rewrite target 的5个挂载面全部落地。 - 浏览器无需直接知道 SpacetimeDB 原生接口即可跑通主流程。
- story action、存档、custom world、agent、assets 都以后端为唯一真相。
- 所有生成图片、动画、精灵表都不再依赖本地
public/generated-*持久化。 - Axum 和 SpacetimeDB 的职责边界稳定,不把外部网络 IO 偷放进 module。
15. 关键风险
15.1 不能把 SpacetimeDB 当 PostgreSQL 替身直接套
SpacetimeDB 更像“带强实时订阅能力的状态机数据库”,不是传统 SQL 仓储替身。
如果仍沿用“单大 JSON + 巨型路由文件 + 过程式 handler”思路,重写后仍然会很快回到旧热点。
15.2 schema 演进成本高于 PostgreSQL
SpacetimeDB 自动迁移更适合“增量追加”,不适合高频改列结构。
所以必须提前定好:
- 稳定主键
- 稳定 reducer 命名
- 事件表 / 投影表边界
15.3 view 滥用会造成性能问题
如果把资料库、画廊、排行榜写成按用户逐个计算、又缺索引的 view,性能会很差。
因此 read model 必须先建索引,再决定用匿名 view 还是按用户 view。
15.4 资产路径兼容是迁移成败关键
当前前端大量相对路径、角色图、场景图、动画图都是围绕 /generated-* 组织的。
如果不先做路径兼容层,存档恢复和世界资料库会大面积失效。
16. 内部实现依据
这份设计稿对当前工程的判断,主要依据以下仓库现状:
server-node/src/server.tsserver-node/src/app.tsserver-node/src/routes/authRoutes.tsserver-node/src/routes/runtimeRoutes.tsserver-node/src/modules/story/storyActionRoutes.tsserver-node/src/repositories/runtimeRepository.tsserver-node/src/services/customWorldAgentOrchestrator.ts- 本文第 2 节保留的旧 Node 能力快照
docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md
17. 外部技术依据
以下外部依据用于确定本次新架构的技术边界,均来自官方文档或官方 crate 文档:
- SpacetimeDB Tables
- SpacetimeDB Table Access Permissions
- SpacetimeDB Reducers Overview
- SpacetimeDB Views
- SpacetimeDB Authorization
- SpacetimeDB Authentication
- SpacetimeDB Using Auth Claims
- SpacetimeDB Rust crate docs
- SpacetimeDB Rust Client SDK
- Axum crate docs
- Axum SSE
- 阿里云 OSS 服务端签名表单上传
- 阿里云 OSS STS 临时授权访问
- 阿里云 OSS PutObject
- 阿里云 OSS PostObject
18. 一句话结论
这次重写最正确的落点不是“把 Express 改成 Axum”,而是:
让 Axum 成为唯一外部副作用和 HTTP 边界,让 SpacetimeDB 成为唯一状态机真相源,让 OSS 成为唯一资产对象仓,从而在不丢当前 96 条能力面的前提下,把项目升级成真正可持续扩展的 Rust 后端。