Files
Genarrative/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

36 KiB
Raw Blame History

基于 SpacetimeDB + Axum + 阿里云 OSS 的后端重写设计文档

日期:2026-04-20

1. 文档定位

这份文档不是继续扩写当前 server-node/ 的实现细节,而是基于当前仓库已经落地的后端能力,为下一版 Rust 后端提供一份可以直接落地编码的重写设计。

目标很明确:

  1. 保留当前项目已经具备的后端能力面,不做需求缩水。
  2. 把后端实现从 Express + PostgreSQL + 本地 public/generated-* 文件 重写为:
    • Axum:唯一 HTTP 边界层与流式接口层
    • SpacetimeDB:唯一运行时状态与实时订阅真相源
    • 阿里云 OSS:唯一大文件与二进制资产存储
  3. 让前端在第一阶段尽量少改,优先兼容当前 /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 个:

  1. health
  2. auth
  3. assets
  4. runtime-main
  5. runtime-story-action

补充说明:

  1. editor 挂载面在历史系统中真实存在,但已被确认为遗留无用能力。
  2. editor 仅保留为历史基线对照,不纳入本轮 server-rs 重写验收。
  3. 当前执行顺序允许在 M3 / M4 / M5 前,先前置 assets / OSS 的基础设施接入,以便后续 runtime、custom world、agent 统一复用同一资产入口。

当前后端内部模块也不能“凭感觉重设计”,而要按现有职责做映射:

  1. ai
  2. assets
  3. combat
  4. custom-world
  5. editor
  6. inventory
  7. npc
  8. progression
  9. quest
  10. runtime
  11. runtime-item
  12. story

其中:

  1. 上述 12 个模块是历史基线总量。
  2. 本轮 active rewrite modules 固定为 11 个。
  3. editor 仅保留历史事实,不进入 server-rs 主线 crate 与阶段验收。

3. 技术选型后的硬边界

3.1 SpacetimeDB 的平台约束与本项目边界

根据 SpacetimeDB 官方 Rust crate 文档,reducer 不能直接使用 std::netstd::fs 进行外部 IO而 2.0 官方文档又提供了 procedure + ctx.http 的受控 HTTP 能力。
也就是说,平台层面并不是完全不能做外部调用,但对于本项目这次重写,我们仍然主动把下面这些副作用统一放到 Axum而不是塞进 SpacetimeDB

  1. 阿里云 OSS 上传、下载、签名
  2. DashScope / Ark / 其他 LLM 请求
  3. 微信 OAuth
  4. 短信验证码
  5. 本地文件系统读写

这样设计不是因为“SpacetimeDB 绝对做不到”,而是因为这类能力都要求:

  1. 更强的重试、超时、日志和熔断能力
  2. 更自由的 SDK / multipart / 签名上传实现
  3. 与 HTTP 头、cookie、回调、对象存储策略深度耦合
  4. 与游戏状态 schema 解耦,避免把第三方供应商能力直接绑进数据库模块发布周期

结论:

  • SpacetimeDB 负责状态、规则、订阅、命令执行与读模型。
  • Axum 负责所有外部副作用与 HTTP 协议。
  • 在本次重写中,不为 SpacetimeDB module 增加任何外部副作用例外通道。

3.2 Axum 的边界

根据 Axum 官方文档,Router<S> 通过 .with_state(...) 注入共享状态后才成为可真正 serve() 的路由树;因此 Axum 适合作为:

  1. 统一 HTTP 入口
  2. Bearer / Cookie 鉴权入口
  3. SSE 输出入口
  4. OSS 上传凭证签发入口
  5. 与 SpacetimeDB 的应用层编排入口

结论:

  • Axum 是唯一 BFF / API Gateway。
  • 前端第一阶段仍然只认识 Axum不直接依赖 SpacetimeDB 原生接口。
  • M0 ~ M6 迁移期内,不新增前端直连 SpacetimeDB 的主链方案。

3.3 OSS 的边界

根据阿里云 OSS 官方文档:

  1. PostObject 适合浏览器表单直传,服务端可以下发 policysignature 与上传条件。
  2. STS 临时访问凭证适合把上传权限以有限时、有限范围的方式下发给客户端。
  3. 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 gatewayOSS 直传签名、对象确认、媒体任务编排
├─ ai gatewayDashScope / Ark / 其他模型调用
├─ background workers异步作业执行与回写
└─ SpacetimeDB client调用 reducer、查询 view / public table、订阅任务状态

SpacetimeDB Module
├─ auth tables用户、身份、session、风控、审计
├─ runtime tables存档、设置、浏览历史、个人面板
├─ gameplay tablesstory / 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 客户端模式,而是:

  1. Axum 保持当前 /api/* 路由空间。
  2. Axum 保持当前 x-request-id / x-api-version / x-route-version 头和响应 envelope。
  3. Axum 保持当前 story / custom-world-agent 的 SSE 体验。
  4. SpacetimeDB 先做后端内部真相源。

补充执行口径:

  1. 虽然总体里程碑仍保留 M6 编号,但 OSS 的平台适配、浏览器直传票据与旧 /generated-* 路径兼容能力允许提前于 M3 / M4 / M5 落地。
  2. 提前落地的目标是先收口统一资产入口,不是提前把全部资产业务状态迁完。

第二阶段再按模块把只读页改成直接订阅 SpacetimeDB。

5.2 命令与读模型分离

SpacetimeDB 官方文档明确说明:

  1. reducer 是唯一可修改表的入口。
  2. reducer 不直接返回业务数据给客户端。
  3. view 可被查询与订阅,并会随底层表变化自动更新。

因此本项目必须采用:

  • Reducer = 命令入口
  • View / Public Table = 读模型入口
  • Axum = HTTP 兼容层与聚合层

5.3 大对象不进数据库

当前 Node 后端把生成结果落在 public/generated-*。重写后统一改成:

  • OSS 存二进制
  • SpacetimeDB 存引用和状态
  • Axum 输出 URL、签名 URL 或 CDN URL

5.4 Schema 必须按 SpacetimeDB 的迁移约束设计

SpacetimeDB 官方文档对自动迁移的限制很强:

  1. 删表、改列类型、改列名、调整列顺序都不是安全日常操作。
  2. 新列只能追加到表末尾,且要提供默认值。
  3. reducer 改名或删除会直接影响客户端调用。

因此本项目的数据模型必须尽量满足:

  1. 主表稳定、字段追加式演进
  2. 高频变化数据优先事件表化
  3. 聚合结果优先投影表 / view而不是频繁重塑旧表结构

5.5 主工程必须按多 crate 方式组织模块

从当前版本开始Rust 后端固定采用“主工程 crate + 独立模块 crate”的方式组织。

这里再明确一层:

  1. crates/ 只是工作区下统一承载 Rust crate 的目录名。
  2. 目录里的每个独立单元在 Rust 语义上都按 workspace crate 对待。

组织规则固定为:

  1. crates/api-server 作为 Axum 主工程 crate只负责协议装配与模块组合。
  2. crates/spacetime-module 作为 SpacetimeDB 主工程 crate只负责聚合各模块 crate 的表、reducer、view。
  3. 每个独立业务模块必须优先拥有自己的 workspace crate再由主工程 crate 引用。
  4. 只有共享 contract、共享领域内核、平台适配、SpacetimeDB client 这类跨模块能力,才允许使用共享 crate。

这样做的目的,是避免把当前 12 个既有模块边界重新压缩回单个“大 application crate”或“大 domain crate”中确保后续重写能继续按模块独立演进。

5.6 SpacetimeDB 相关修改的执行约束

从当前版本开始,凡是涉及 SpacetimeDB 的设计、实现、脚本、调试与前端接入,统一要求显式使用以下 skill 作为执行依据:

  1. $spacetimedb-cli
  2. $spacetimedb-rust
  3. $spacetimedb-concepts
  4. $spacetimedb-typescript

执行要求:

  1. 涉及 spacetime CLI、发布、绑定生成、本地联调时优先按 spacetimedb-cli 约束执行。
  2. 涉及 crates/spacetime-module 的表、reducer、view、Rust API 使用时,优先按 spacetimedb-rustspacetimedb-concepts 约束执行。
  3. 涉及前端或 Node 侧的 SpacetimeDB 绑定、订阅、TypeScript SDK 接入时,优先按 spacetimedb-typescriptspacetimedb-concepts 约束执行。
  4. 若 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

目录职责约束:

  1. crates/api-server/ 只做协议装配、鉴权、中间件、handler 与模块组合,不把业务模块重新堆回单包。
  2. crates/spacetime-module/ 只负责聚合各模块 crate 的状态模型,不直接承接外部副作用。
  3. crates/module-* 保持与当前业务模块边界一一对应,已明确退出本轮的 editor 遗留模块除外;必要时可在 crate 内部再拆 applicationdomainspacetime 子层次。
  4. crates/shared-contracts/ 负责与当前前端兼容的 JSON / SSE 协议。
  5. crates/shared-kernel/ 只放跨模块复用的数据结构和规则,不碰框架。
  6. crates/shared-logging/ 负责统一日志初始化、过滤器解析与 subscriber 基础设施,不承接 HTTP 业务语义。
  7. crates/platform-* 统一承接三方供应商与平台适配。

命名补充说明:

  1. 本文后续若出现 auth-serviceoss-servicellm-serviceapplication::... 等历史逻辑名,统一视为职责标签,而不是强制要求继续存在同名顶层目录。
  2. 在新的多 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 关系、招募、状态机在 SpacetimeDBLLM 台词生成留在 Axum。
progression SpacetimeDB module 关卡、等级、敌对 scaling、章节推进都做领域表和 reducer。
quest SpacetimeDB module Axum for AI quest drafting 任务主状态在 SpacetimeDB生成型内容由 Axum 生产后回写。
runtime-item SpacetimeDB module Axum for AI intent 物品奖励和解析归 reducerAI 意图生成由 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。

补充说明:

  1. 历史 editor 模块不纳入 server-rs 本轮重写。
  2. 相关 /api/editor/*server-node/src/modules/editor 仅保留为旧系统对照事实,后续若要清理再单独立项。

8. 数据建模方案

8.1 表的分层

建议在 SpacetimeDB 中至少拆成 5 组表:

A. 身份与会话表

  • user_account
  • auth_identity
  • refresh_session
  • auth_audit_log
  • auth_risk_block
  • sms_auth_event
  • wechat_auth_state

说明:

  1. 这些表默认都应为 private。
  2. 密码哈希、短信验证码校验、微信 code 换 token 的动作在 Axum 完成。
  3. 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_snapshot
  • runtime_setting
  • profile_dashboard_state
  • profile_wallet_ledger
  • profile_played_world
  • profile_save_archive
  • user_browse_history

说明:

  1. runtime_snapshot 继续作为“恢复游戏”的主聚合表。
  2. profile_* 作为只读页投影表,避免每次从大快照现算。
  3. browse_historysave_archive 单独建表,不要藏进 snapshot blob。

C. Gameplay 领域表

  • story_session
  • story_event
  • npc_state
  • quest_record
  • inventory_slot
  • treasure_record
  • battle_state
  • player_progression
  • chapter_progression

说明:

  1. story_action 不直接改大 JSON而是 reducer 驱动多个领域表。
  2. 是否继续保留兼容快照,可由投影 reducer 汇总生成。
  3. 旧前端仍需要 gameState + currentStory 时,由 Axum 聚合成兼容 DTO。

D. Custom World 表

  • custom_world_profile
  • custom_world_session
  • custom_world_agent_session
  • custom_world_agent_message
  • custom_world_agent_operation
  • custom_world_draft_card
  • custom_world_asset_link
  • custom_world_gallery_entry

说明:

  1. 传统问答流与 Agent 流不再混一个 payload。
  2. 卡片、操作、消息必须拆表,不能再都塞进一个大 JSON 会话体。
  3. 公开画廊作为独立投影,避免从 profile 运行时拼装。

E. 资产与对象元数据表

  • asset_job
  • asset_object
  • asset_manifest
  • character_visual_asset
  • character_animation_asset
  • scene_image_asset
  • sprite_sheet_asset

说明:

  1. 任务状态在 SpacetimeDB。
  2. 二进制对象在 OSS。
  3. asset_object 的正式真相字段固定为 bucket + object_key
  4. 所有 URL 都只作为派生读模型,不作为对象主键存储。
  5. asset_object 的首版字段、访问级别与索引设计见:

8.2 public / private 原则

依据 SpacetimeDB 官方访问权限文档:

  1. private table 默认只给 reducer / view / owner 看。
  2. public table 才适合直接查询和订阅。

本项目建议:

  • auth_*refresh_session、风控、验证码全部 private
  • runtime_snapshot private
  • story_session private
  • custom_world_agent_* 绝大多数 private
  • gallery、公共角色卡、公共作品索引可 public
  • 如需“用户自己的读模型”,优先用 ViewContext view 暴露

8.3 view 设计原则

依据 SpacetimeDB 官方 view 文档:

  1. view 可以被查询和订阅,底层表变化时会自动更新。
  2. AnonymousViewContext 可被所有用户共享物化结果,性能明显优于按用户单独计算。
  3. view 不适合全表 .iter() 扫描,应该通过索引查找或返回可分析的 query。

所以本项目的 view 设计规则必须是:

  1. 画廊、排行榜、商店、公共世界列表,优先匿名 view。
  2. “我的背包 / 我的档案 / 我的当前会话”这类按用户隔离的读模型,才用带身份上下文的 view。
  3. 首页、资料库、历史记录、存档列表都必须先有索引,再写 view。

9. 鉴权设计

9.1 总体方案

建议保留当前“密码 / 手机验证码 / 微信”三类登录能力,但把实现改成:

  1. Axum 负责登录副作用:
    • 密码校验
    • 短信发送与校验
    • 微信 OAuth code 交换
    • refresh cookie 签发与轮换
  2. Axum 负责签发OIDC 兼容 JWT
  3. 同一张 JWT 同时用于:
    • Axum 自身 Bearer 鉴权
    • SpacetimeDB reducer / view 的身份透传

这是可行的,因为 SpacetimeDB 官方文档说明其可以从 OIDC 兼容 JWT 中提取身份声明,并在模块上下文中使用这些 claims。

9.2 Token 设计

建议:

  • iss:固定为 Axum 网关身份发行者,例如 https://api.genarrative.example/auth
  • sub:稳定用户 ID
  • 扩展 claims
    • sidsession id
    • providerpassword / phone / wechat
    • roles
    • phone_verified
    • display_name

iss/sub/sid/provider/roles/ver/phone_verified/binding_status 的字段定义、哪些字段禁止进入 JWT、以及 Axum 与 SpacetimeDB 的使用边界,见:

9.3 Refresh Session

建议保留当前模式:

  1. 浏览器短期 Bearer access token
  2. HttpOnly refresh cookie
  3. refresh session 服务端可吊销

但新的会话 ledger 写入 SpacetimeDB private 表即可。

10. Axum 侧设计

10.1 进程职责

Axum 进程建议拆成以下子系统:

  1. http::middleware
    • request id
    • tracing
    • envelope / 错误标准化
    • auth extractor
  2. http::routes
    • /healthz
    • /api/auth/*
    • /api/runtime/*
    • /api/runtime/story/*
    • /api/assets/*
  3. application::services
    • story facade
    • runtime snapshot facade
    • custom world facade
    • asset facade
  4. infra
    • SpacetimeDB client
    • OSS adapter
    • DashScope / Ark adapter
    • SMS / WeChat adapter

10.2 流式接口

当前项目已经有 6 条流式接口,重写时不建议一上来全部改成 WebSocket。

第一阶段建议:

  1. 继续使用 Axum SSE 输出流式文本与阶段事件。
  2. Axum 在处理流式过程中,持续把阶段状态写回 SpacetimeDB。
  3. 前端仍按当前 SSE 协议消费。

适合继续保留为 Axum SSE 的场景:

  1. story/initial
  2. story/continue
  3. chat/character/*
  4. chat/npc/*
  5. custom-world/generate/stream
  6. custom-world/agent/messages/stream

10.3 兼容 contract

Axum 第一阶段需要兼容当前项目的这些约定:

  1. 路径空间不变
  2. x-request-id
  3. x-api-version
  4. x-route-version
  5. x-response-time-ms
  6. 可选 envelopex-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 上传模式

建议:

  1. 前端直传图片、封面、小文件:PostObject
  2. 大文件或需要细粒度控制时:STS + PutObject / Multipart
  3. 生成型资产Axum worker 直接上传 OSS

其中:

  • PostObject 用于浏览器上传时Axum 负责生成 policy 与 signature。
  • policy 中必须明确:
    • key 前缀
    • content-type 白名单
    • content-length-range
    • success_action_status

当前已落地的最小实现补充:

  1. server-rs/crates/platform-oss 已提供 PostObject 直传签名能力。
  2. server-rs/crates/api-server 已暴露 POST /api/assets/direct-upload-tickets
  3. server-rs/crates/platform-oss 已提供私有对象 GET 短期签名 URL 能力。
  4. server-rs/crates/api-server 已暴露 GET /api/assets/read-url
  5. 上传接口当前输出:
    • bucket
    • objectKey
    • legacyPublicPath
    • formFields
    • expiresAt
  6. 读取接口当前支持:
    • objectKey
    • legacyPublicPath
    • expireSeconds
  7. 当前 bucket 已明确为私有读写,因此 publicUrl 不再作为正式对象真相输出。
  8. 当前签名链路优先兼容旧公开前缀:
    • /generated-character-drafts/*
    • /generated-characters/*
    • /generated-animations/*
    • /generated-custom-world-scenes/*
    • /generated-custom-world-covers/*
    • /generated-qwen-sprites/*
  9. 当前 POST /api/assets/sts-upload-credentials 已按“服务器上传、Web 只下载”的需求固定为禁用式 contract不向浏览器下发 OSS 写权限。
  10. 当前 platform-oss 已提供服务端 PutObject 上传 helper供后续 AI worker 上传生成资源后继续走对象确认链路。

11.3 元数据与标签

建议所有业务对象写入统一元数据:

  • x-oss-meta-owner-user-id
  • x-oss-meta-profile-id
  • x-oss-meta-entity-id
  • x-oss-meta-asset-kind
  • x-oss-meta-source-job-id
  • x-oss-meta-content-hash
  • x-oss-meta-origin

注意:

  1. OSS 官方文档要求自定义元数据使用 x-oss-meta-* 前缀。
  2. 所有元数据总大小不能超过 8 KB

11.4 URL 策略

建议:

  1. 业务表里统一存 bucket + object_key
  2. 对外输出 cdn_url 或签名 URL
  3. 私有对象默认输出短期签名 URL而不是假设匿名公开读

为了兼容当前前端相对路径使用习惯,第一阶段可以让 Axum 或 CDN 兼容以下历史前缀:

  1. /generated-character-drafts/*
  2. /generated-characters/*
  3. /generated-animations/*
  4. /generated-custom-world-scenes/*
  5. /generated-custom-world-covers/*
  6. /generated-qwen-sprites/*

补充约束:

  1. 当前 xushi-dev bucket 已明确为私有读写,因此这些旧前缀在第一阶段只代表兼容路径习惯,不代表对象可匿名读取。
  2. Web 端若拿到的是历史 /generated-* 路径,必须先调用 GET /api/assets/read-url 换取 signedUrl,不能直接把该路径当成正式可读 URL。
  3. 前端工程内凡是图片、背景图、封面图、角色图、场景图等展示入口,只要可能接收到 /generated-*,都必须统一走资源解析层:
    • 列表/卡片/普通 <img> 优先复用 src/services/assetReadUrlService.ts
    • 组件内优先复用 src/hooks/useResolvedAssetReadUrl.ts
    • 通用图片标签优先复用 src/components/ResolvedAssetImage.tsx
    • 当前已完成的高优先级入口包括:CharacterAnimatorCharacterPanelCompanionCampModalCharacterSelectionFlowMapModalGameCanvasSceneLayerGameCanvasSharedGameCanvasEntityLayerCustomWorldResultViewCustomWorldEntityEditorModalCustomWorldRoleAssetStudioModalCustomWorldAgentDraftDetailPanelPlatformHomeViewPlatformWorldDetailViewQwenSpriteSheetTool
  4. 对私有 OSS 资源,前端在签名地址返回前不能先回退渲染原始 /generated-* 路径,否则浏览器会先发起一次无签名请求并触发 403
  5. 具体对象引用设计见:

12. 关键业务流设计

12.1 Story Action

目标:

  1. storyActionService 当前承担的跨模块结算必须迁到 SpacetimeDB reducer。
  2. Axum 只做 request parse、auth、调用 reducer、读取 view、拼回当前前端响应。

推荐流程:

  1. 前端 POST /api/runtime/story/actions/resolve
  2. Axum 校验请求并附带 JWT
  3. Axum 调用 SpacetimeDB resolve_story_action reducer
  4. reducer 内部联动:
    • story
    • combat
    • inventory
    • npc
    • quest
    • runtime-item
    • progression
  5. reducer 写回领域表
  6. Axum 再读取 current_story_viewruntime_snapshot_view
  7. Axum 返回兼容当前前端的 RuntimeStoryActionResponse

12.2 存档与恢复

目标:

  1. 仍保留当前“完整恢复游戏”的能力。
  2. 但底层不再由 PostgreSQL 单大 JSON 承担全部职责。

建议:

  1. runtime_snapshot 继续保留兼容聚合快照,满足现有恢复链路。
  2. gameplay 真相由领域表维护。
  3. 每次重要 reducer 提交后,异步或同步刷新 snapshot projection。

这样可以兼顾:

  1. 旧前端兼容
  2. 新后端可审计
  3. SpacetimeDB 实时订阅能力

12.3 Custom World Agent

当前 Node 后端中,customWorldAgentOrchestrator + SessionStore + Operation 已经是一条清晰主链。
重写后建议进一步正规化:

  1. SpacetimeDB
    • 存会话
    • 存消息
    • 存卡片
    • 存操作状态
    • 存草稿 profile
  2. Axum
    • 调 LLM
    • 调图片生成
    • 调 OSS
    • 发 SSE
    • 把阶段结果回写 reducer

不再允许“一整个 agent 会话对象 JSON 一把写回”作为长期形态。

12.4 资产生成

资产链路拆成四步:

  1. Axum 创建 asset_job
  2. Axum worker 调外部模型
  3. 产物上传 OSSasset_object
  4. Axum 调 reducer 将对象绑定到:
    • 角色
    • 地点
    • 世界草稿
    • Qwen sprite sheet

所有“对象是否已经被业务引用”的事实,以 SpacetimeDB 绑定表为准,而不是以 OSS 是否存在某个 key 为准。

13. 迁移阶段建议

迁移期仓库边界补充约束:

  1. server-node/M0 ~ M6 期间继续保留,作为协议对照、回归基线与回退锚点。
  2. 只有在 M7 切流、回归、回退方案都稳定后,才评估是否清理旧 Node 后端。

Phase 0冻结能力清单

交付:

  1. 固定当前 96 条接口为重写验收基线
  2. 固定当前 12 个模块为迁移映射基线
  3. 固定当前前端 contract 与 SSE 协议

Phase 1先搭 Axum 外壳与鉴权

交付:

  1. /healthz
  2. /api/auth/*
  3. response envelope
  4. request id / tracing
  5. Axum -> SpacetimeDB 基础 client
  6. OIDC-compatible JWT 签发

阶段执行补充:

  1. 微信登录链路在当前阶段暂缓,不进入连续执行顺序。
  2. 当前优先顺序固定为JWT / refresh cookie / 密码登录 / 手机验证码登录。

Phase 2迁移 runtime snapshot / settings / profile

交付:

  1. 存档
  2. 设置
  3. 浏览历史
  4. profile dashboard
  5. save archives

这是最容易先跑通的闭环。

Phase 3迁移 story action 主循环

交付:

  1. story reducer
  2. combat / inventory / npc / quest / runtime-item / progression 联动
  3. /api/runtime/story/*

这一阶段完成后,运行时真相就真正从 Node 版切出去了。

Phase 4迁移 custom world 与 agent

交付:

  1. legacy custom world session
  2. custom world library / gallery
  3. custom world agent 会话、卡片、操作
  4. scene npc / entity generation

Phase 5迁移 assets / OSS

交付:

  1. OSS 直传
  2. 生成任务
  3. 对象元数据
  4. /generated-* 路径兼容

补充说明:

  1. editor 已于 2026-04-21 被确认为遗留无用模块,退出本轮 Rust 后端重写范围。
  2. Phase 5 只覆盖资产与 OSS 主链,不再包含 editor 迁移。

Phase 6联调、回归、部署与切流收口

交付:

  1. 联调与回归测试体系
  2. 灰度环境、切流开关、回退方案
  3. tracing / request id / 关键链路观测
  4. 拆分 server-rs/crates/spacetime-module/src/lib.rs,按业务模块与 SpacetimeDB 的 table / reducer / procedure / view 结构重组为 runtimegameplay::{story/combat/inventory/npc/quest/runtime_item/progression}custom_worldasset_metadataai 等聚合子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口

阶段执行补充:

  1. 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。
  2. 拆分后的目录与模块边界必须对齐 M0 已冻结的模块迁移归属,避免 spacetime-module 回退成“单大文件 + 单大包”结构。
  3. 拆分完成后至少要保持 cargo check、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。

14. 验收标准

重写完成至少要满足:

  1. 当前 96 条已登记路由全部有对应实现或明确兼容替代。
  2. 当前历史 6 个挂载面的迁移去向全部明确,且本轮 active rewrite target 的 5 个挂载面全部落地。
  3. 浏览器无需直接知道 SpacetimeDB 原生接口即可跑通主流程。
  4. story action、存档、custom world、agent、assets 都以后端为唯一真相。
  5. 所有生成图片、动画、精灵表都不再依赖本地 public/generated-* 持久化。
  6. Axum 和 SpacetimeDB 的职责边界稳定,不把外部网络 IO 偷放进 module。

15. 关键风险

15.1 不能把 SpacetimeDB 当 PostgreSQL 替身直接套

SpacetimeDB 更像“带强实时订阅能力的状态机数据库”,不是传统 SQL 仓储替身。
如果仍沿用“单大 JSON + 巨型路由文件 + 过程式 handler”思路重写后仍然会很快回到旧热点。

15.2 schema 演进成本高于 PostgreSQL

SpacetimeDB 自动迁移更适合“增量追加”,不适合高频改列结构。
所以必须提前定好:

  1. 稳定主键
  2. 稳定 reducer 命名
  3. 事件表 / 投影表边界

15.3 view 滥用会造成性能问题

如果把资料库、画廊、排行榜写成按用户逐个计算、又缺索引的 view性能会很差。
因此 read model 必须先建索引,再决定用匿名 view 还是按用户 view。

15.4 资产路径兼容是迁移成败关键

当前前端大量相对路径、角色图、场景图、动画图都是围绕 /generated-* 组织的。
如果不先做路径兼容层,存档恢复和世界资料库会大面积失效。

16. 内部实现依据

这份设计稿对当前工程的判断,主要依据以下仓库现状:

  1. server-node/src/server.ts
  2. server-node/src/app.ts
  3. server-node/src/routes/authRoutes.ts
  4. server-node/src/routes/runtimeRoutes.ts
  5. server-node/src/modules/story/storyActionRoutes.ts
  6. server-node/src/repositories/runtimeRepository.ts
  7. server-node/src/services/customWorldAgentOrchestrator.ts
  8. 本文第 2 节保留的旧 Node 能力快照
  9. docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md

17. 外部技术依据

以下外部依据用于确定本次新架构的技术边界,均来自官方文档或官方 crate 文档:

  1. SpacetimeDB Tables
  2. SpacetimeDB Table Access Permissions
  3. SpacetimeDB Reducers Overview
  4. SpacetimeDB Views
  5. SpacetimeDB Authorization
  6. SpacetimeDB Authentication
  7. SpacetimeDB Using Auth Claims
  8. SpacetimeDB Rust crate docs
  9. SpacetimeDB Rust Client SDK
  10. Axum crate docs
  11. Axum SSE
  12. 阿里云 OSS 服务端签名表单上传
  13. 阿里云 OSS STS 临时授权访问
  14. 阿里云 OSS PutObject
  15. 阿里云 OSS PostObject

18. 一句话结论

这次重写最正确的落点不是“把 Express 改成 Axum”而是

让 Axum 成为唯一外部副作用和 HTTP 边界,让 SpacetimeDB 成为唯一状态机真相源,让 OSS 成为唯一资产对象仓,从而在不丢当前 96 条能力面的前提下,把项目升级成真正可持续扩展的 Rust 后端。