Files
Genarrative/docs/technical/M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.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

8.3 KiB
Raw Permalink Blame History

M3browse history Axum + SpacetimeDB 落地设计

日期:2026-04-21

关联任务:

关联现状:

  • server-node/src/routes/rpg-profile/rpgProfileRoutes.ts
  • server-node/src/repositories/runtimeRepository.ts
  • packages/shared/src/contracts/runtime.ts

1. 文档目的

02_M3_RUNTIME_PROFILE.md 已经把 user_browse_historybrowse history facade 列入 M3但还没有冻结到可直接编码的字段、去重规则、路由兼容方式和错误语义。

本文只解决 browse history 这一个最小闭环切片:

  1. user_browse_history 真相表
  2. GET /api/runtime/profile/browse-history
  3. POST /api/runtime/profile/browse-history
  4. DELETE /api/runtime/profile/browse-history
  5. /api/profile/browse-history 兼容路径

本文不新建 checklist不替代 ../../backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md,只补足本轮编码所需的冻结口径。

2. 旧实现冻结口径

当前 Node 侧行为来自:

  • server-node/src/routes/rpg-profile/rpgProfileRoutes.ts
  • server-node/src/repositories/runtimeRepository.ts

冻结行为如下。

2.1 路由

主路径与兼容路径都必须保留:

  1. GET /api/runtime/profile/browse-history
  2. POST /api/runtime/profile/browse-history
  3. DELETE /api/runtime/profile/browse-history
  4. GET /api/profile/browse-history
  5. POST /api/profile/browse-history
  6. DELETE /api/profile/browse-history

所有路径都要求 Bearer JWT。

2.2 数据字段

单条浏览记录字段与 packages/shared/src/contracts/runtime.ts 保持一致:

  1. ownerUserId
  2. profileId
  3. worldName
  4. subtitle
  5. summaryText
  6. coverImageSrc
  7. themeMode
  8. authorDisplayName
  9. visitedAt

2.3 POST 请求体兼容

POST 同时支持两种形态:

  1. 单条对象
  2. { entries: [...] } 批量对象

批量最多 100 条。

2.4 归一化规则

旧 Node 仓储不是严格校验,而是宽松归一化:

  1. ownerUserIdprofileIdworldName 去首尾空白后必须非空,否则该条忽略。
  2. subtitlesummaryTextcoverImageSrc 去首尾空白,空串按空值处理。
  3. themeMode 不做严格枚举校验,未知值统一回落到 mythic
  4. authorDisplayName 空值回落到 玩家
  5. visitedAt 缺失时回落到当前时间。

2.5 去重与排序规则

旧 Node 仓储的关键行为必须保持:

  1. 去重键:ownerUserId + profileId
  2. 同一批写入时,先按 visitedAt DESC 排序,再去重,只保留最新一条
  3. 表内最终查询结果按 visitedAt DESC 返回

2.6 清空行为

DELETE 清空当前用户的全部浏览历史,并返回:

{
  "entries": []
}

3. Rust 落位决议

3.1 crate 分工

本切片固定按以下边界落位:

  1. crates/module-runtime
    • 定义 browse history DTO、字段校验、去重排序与宽松归一化规则。
  2. crates/spacetime-module
    • 定义 user_browse_history 表。
    • 提供 list / upsert / clear 三个 procedure。
  3. crates/spacetime-client
    • 提供 list_platform_browse_history
    • 提供 upsert_platform_browse_history_entries
    • 提供 clear_platform_browse_history
  4. crates/shared-contracts
    • 冻结 Axum facade 的请求/响应 DTO。
  5. crates/api-server
    • 提供双路径兼容 facade。
    • 保持 envelope / 错误格式与当前 runtime settings 一致。

3.2 身份边界

本轮仍沿用 Axum Bearer JWT 作为唯一鉴权边界:

  1. require_bearer_auth 校验 token。
  2. 从 claims 中提取 user_id
  3. user_id 作为 procedure 入参传入 SpacetimeDB。

当前阶段不把浏览历史直接暴露给前端直连订阅。

4. SpacetimeDB 表设计

4.1 表名

user_browse_history

4.2 字段

字段 类型 说明
browse_history_id String 主键,格式为 user_id:owner_user_id:profile_id
user_id String 当前登录用户
owner_user_id String 被浏览世界所属用户
profile_id String 被浏览世界 profile
world_name String 世界名
subtitle String 副标题
summary_text String 摘要
cover_image_src Option<String> 封面图
theme_mode RuntimeBrowseHistoryThemeMode 主题枚举
author_display_name String 作者显示名
visited_at Timestamp 最近访问时间
created_at Timestamp 首次写入时间
updated_at Timestamp 最近更新时间

4.3 索引

至少保留以下访问路径:

  1. 主键 browse_history_id
  2. (user_id, visited_at) 用于当前用户按时间倒序列出
  3. (user_id, owner_user_id, profile_id) 用于幂等 upsert

4.4 设计决议

  1. 不额外引入自增 ID直接把幂等键收口成主键。
  2. visited_at 单独持久化成 Timestamp,避免把时间排序退回字符串比较。
  3. theme_mode 在表内固定为枚举,但 Axum 输入仍宽松接受字符串。

5. Procedure 设计

5.1 procedure 列表

  1. list_platform_browse_history
  2. upsert_platform_browse_history_and_return
  3. clear_platform_browse_history_and_return

统一返回:

RuntimeBrowseHistoryProcedureResult {
  ok
  entries
  error_message
}

5.2 行为约束

list_platform_browse_history

  1. 校验 user_id
  2. 读取当前用户所有记录
  3. visited_at DESC 返回

upsert_platform_browse_history_and_return

  1. 校验 user_id
  2. 接受单批最多 100
  3. 先按旧 Node 规则宽松归一化
  4. 先按 visitedAt DESC 排序,再按 ownerUserId + profileId 去重
  5. browse_history_id 幂等 upsert
  6. 返回当前用户完整倒序列表

clear_platform_browse_history_and_return

  1. 校验 user_id
  2. 删除当前用户全部记录
  3. 返回空列表

6. Axum facade 设计

6.1 双路径兼容

两组路径必须共用同一组 handler

  1. /api/runtime/profile/browse-history
  2. /api/profile/browse-history

只允许路由名不同,不允许行为分叉。

6.2 GET

行为:

  1. Bearer JWT 校验
  2. 读取 claims 中的 user_id
  3. spacetime_client.list_platform_browse_history
  4. 返回 PlatformBrowseHistoryResponse

6.3 POST

行为:

  1. Bearer JWT 校验
  2. 通过 serde(untagged) 同时接单条和批量 shape
  3. 不对 themeMode 做严格 400 拒绝
  4. ownerUserIdprofileIdworldName 的缺失或空串按旧 Node 路由规则直接返回 400
  5. 写入成功后返回最新完整列表

6.4 DELETE

行为:

  1. Bearer JWT 校验
  2. 清空当前用户全部记录
  3. 返回 entries: []

6.5 错误映射

  1. JSON 解析失败:400 BAD_REQUEST
  2. DTO 构建失败:400 BAD_REQUEST
  3. SpacetimeDB 调用失败:502 BAD_GATEWAY
  4. JWT 缺失或失效:沿用当前 401 UNAUTHORIZED

错误 details.provider 固定为:

  1. browse-history
  2. spacetimedb

7. 测试策略

7.1 必跑测试

  1. module-runtime
    • 宽松 theme 归一化
    • visitedAt 默认值
    • 去重与倒序逻辑
  2. api-server
    • 未登录返回 401
    • 兼容路径与主路径一致
    • POST 同时支持单条和批量
    • envelope 打开时错误结构稳定

7.2 可选联调测试

保留 #[ignore] 的本地 SpacetimeDB 集成测试:

  1. POST -> GET
  2. DELETE -> GET

8. 本文完成定义

当以下条件成立时,本设计视为完成:

  1. user_browse_history 表字段、主键和排序规则已冻结。
  2. 双路径 facade、请求 shape 和错误契约已冻结。
  3. 后续编码不再需要猜测:
    • themeMode 是否严格校验
    • POST 是否支持单条/批量双 shape
    • 去重时机与排序依据

9. 2026-04-22 实际落地进度

  1. module-runtime 已切换为“API 入口严格校验 + 领域层静默过滤”的旧 Node 对齐模式。
  2. api-server 已补齐双路径 browse history handler并补 401400、批量 shape、兼容路径一致性测试。
  3. 剩余阻塞主要在工作树内其他并行任务带来的 Rust 编译占用与跨模块联调,不属于 browse history 方案本身。