8.3 KiB
M3:browse history Axum + SpacetimeDB 落地设计
日期:2026-04-21
关联任务:
- ../../backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md
- ./M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md
关联现状:
server-node/src/routes/rpg-profile/rpgProfileRoutes.tsserver-node/src/repositories/runtimeRepository.tspackages/shared/src/contracts/runtime.ts
1. 文档目的
02_M3_RUNTIME_PROFILE.md 已经把 user_browse_history 和 browse history facade 列入 M3,但还没有冻结到可直接编码的字段、去重规则、路由兼容方式和错误语义。
本文只解决 browse history 这一个最小闭环切片:
user_browse_history真相表GET /api/runtime/profile/browse-historyPOST /api/runtime/profile/browse-historyDELETE /api/runtime/profile/browse-history/api/profile/browse-history兼容路径
本文不新建 checklist,不替代 ../../backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md,只补足本轮编码所需的冻结口径。
2. 旧实现冻结口径
当前 Node 侧行为来自:
server-node/src/routes/rpg-profile/rpgProfileRoutes.tsserver-node/src/repositories/runtimeRepository.ts
冻结行为如下。
2.1 路由
主路径与兼容路径都必须保留:
GET /api/runtime/profile/browse-historyPOST /api/runtime/profile/browse-historyDELETE /api/runtime/profile/browse-historyGET /api/profile/browse-historyPOST /api/profile/browse-historyDELETE /api/profile/browse-history
所有路径都要求 Bearer JWT。
2.2 数据字段
单条浏览记录字段与 packages/shared/src/contracts/runtime.ts 保持一致:
ownerUserIdprofileIdworldNamesubtitlesummaryTextcoverImageSrcthemeModeauthorDisplayNamevisitedAt
2.3 POST 请求体兼容
POST 同时支持两种形态:
- 单条对象
{ entries: [...] }批量对象
批量最多 100 条。
2.4 归一化规则
旧 Node 仓储不是严格校验,而是宽松归一化:
ownerUserId、profileId、worldName去首尾空白后必须非空,否则该条忽略。subtitle、summaryText、coverImageSrc去首尾空白,空串按空值处理。themeMode不做严格枚举校验,未知值统一回落到mythic。authorDisplayName空值回落到玩家。visitedAt缺失时回落到当前时间。
2.5 去重与排序规则
旧 Node 仓储的关键行为必须保持:
- 去重键:
ownerUserId + profileId - 同一批写入时,先按
visitedAt DESC排序,再去重,只保留最新一条 - 表内最终查询结果按
visitedAt DESC返回
2.6 清空行为
DELETE 清空当前用户的全部浏览历史,并返回:
{
"entries": []
}
3. Rust 落位决议
3.1 crate 分工
本切片固定按以下边界落位:
crates/module-runtime- 定义
browse historyDTO、字段校验、去重排序与宽松归一化规则。
- 定义
crates/spacetime-module- 定义
user_browse_history表。 - 提供
list / upsert / clear三个 procedure。
- 定义
crates/spacetime-client- 提供
list_platform_browse_history - 提供
upsert_platform_browse_history_entries - 提供
clear_platform_browse_history
- 提供
crates/shared-contracts- 冻结 Axum facade 的请求/响应 DTO。
crates/api-server- 提供双路径兼容 facade。
- 保持 envelope / 错误格式与当前
runtime settings一致。
3.2 身份边界
本轮仍沿用 Axum Bearer JWT 作为唯一鉴权边界:
require_bearer_auth校验 token。- 从 claims 中提取
user_id。 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 索引
至少保留以下访问路径:
- 主键
browse_history_id (user_id, visited_at)用于当前用户按时间倒序列出(user_id, owner_user_id, profile_id)用于幂等 upsert
4.4 设计决议
- 不额外引入自增 ID,直接把幂等键收口成主键。
visited_at单独持久化成Timestamp,避免把时间排序退回字符串比较。theme_mode在表内固定为枚举,但 Axum 输入仍宽松接受字符串。
5. Procedure 设计
5.1 procedure 列表
list_platform_browse_historyupsert_platform_browse_history_and_returnclear_platform_browse_history_and_return
统一返回:
RuntimeBrowseHistoryProcedureResult {
ok
entries
error_message
}
5.2 行为约束
list_platform_browse_history
- 校验
user_id - 读取当前用户所有记录
- 按
visited_at DESC返回
upsert_platform_browse_history_and_return
- 校验
user_id - 接受单批最多
100条 - 先按旧 Node 规则宽松归一化
- 先按
visitedAt DESC排序,再按ownerUserId + profileId去重 - 用
browse_history_id幂等 upsert - 返回当前用户完整倒序列表
clear_platform_browse_history_and_return
- 校验
user_id - 删除当前用户全部记录
- 返回空列表
6. Axum facade 设计
6.1 双路径兼容
两组路径必须共用同一组 handler:
/api/runtime/profile/browse-history/api/profile/browse-history
只允许路由名不同,不允许行为分叉。
6.2 GET
行为:
- Bearer JWT 校验
- 读取 claims 中的
user_id - 调
spacetime_client.list_platform_browse_history - 返回
PlatformBrowseHistoryResponse
6.3 POST
行为:
- Bearer JWT 校验
- 通过
serde(untagged)同时接单条和批量 shape - 不对
themeMode做严格 400 拒绝 - 对
ownerUserId、profileId、worldName的缺失或空串按旧 Node 路由规则直接返回400 - 写入成功后返回最新完整列表
6.4 DELETE
行为:
- Bearer JWT 校验
- 清空当前用户全部记录
- 返回
entries: []
6.5 错误映射
- JSON 解析失败:
400 BAD_REQUEST - DTO 构建失败:
400 BAD_REQUEST - SpacetimeDB 调用失败:
502 BAD_GATEWAY - JWT 缺失或失效:沿用当前
401 UNAUTHORIZED
错误 details.provider 固定为:
browse-historyspacetimedb
7. 测试策略
7.1 必跑测试
module-runtime- 宽松 theme 归一化
visitedAt默认值- 去重与倒序逻辑
api-server- 未登录返回
401 - 兼容路径与主路径一致
POST同时支持单条和批量- envelope 打开时错误结构稳定
- 未登录返回
7.2 可选联调测试
保留 #[ignore] 的本地 SpacetimeDB 集成测试:
POST -> GETDELETE -> GET
8. 本文完成定义
当以下条件成立时,本设计视为完成:
user_browse_history表字段、主键和排序规则已冻结。- 双路径 facade、请求 shape 和错误契约已冻结。
- 后续编码不再需要猜测:
themeMode是否严格校验POST是否支持单条/批量双 shape- 去重时机与排序依据
9. 2026-04-22 实际落地进度
module-runtime已切换为“API 入口严格校验 + 领域层静默过滤”的旧 Node 对齐模式。api-server已补齐双路径 browse history handler,并补401、400、批量 shape、兼容路径一致性测试。- 剩余阻塞主要在工作树内其他并行任务带来的 Rust 编译占用与跨模块联调,不属于 browse history 方案本身。