diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 8b858c361..e03783cd5 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1495,6 +1495,14 @@ - 验证:发布前运行 `npm run check:spacetime-schema`,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。 - 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。 +## SpacetimeDB 持久化 enum 新 variant 只能末尾追加 + +- 现象:生产发布时 schema 迁移失败,或旧数据中的 enum 判别序号被新代码解释成其它业务枚举值。 +- 原因:SpacetimeDB schema 会保存 enum variant 顺序;在已有持久化 enum 中间插入新 variant,会让后续 variant 的判别序号整体移动。即使 Rust 代码能编译,发布到已有数据库也可能炸。 +- 处理:给已发布并持久化的 enum 增加 variant 时,只能追加到 enum 末尾;同步运行 `npm run spacetime:generate` 刷新 bindings,不能手工把 generated bindings 改成另一套顺序。需要调整既有 variant 顺序、删除或重命名时,必须先确认数据迁移方案。 +- 验证:`npm run check:spacetime-schema` 应通过;对照本次修改前后的 enum,所有旧 variant 顺序必须完全不变,新 variant 只出现在末尾。 +- 关联:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-client/src/module_bindings/`、`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`。 + ## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature - 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 3d5b98aff..787c33a56 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -168,13 +168,14 @@ npm run check:server-rs-ddd 1. 任何 table、view、reducer、procedure、row shape 或 bindings 变化,都必须同步本文件表 / view 目录和生成绑定;真实 table 变化还必须同步 `server-rs/crates/spacetime-module/src/migration.rs`,view 属于派生投影,不写入迁移导入导出表清单。 2. 已有表新增字段必须放在 Rust 表结构体最后,并设置明确 `#[default(...)]`。 -3. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。 -4. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option>` 加 `#[default(None::>)]`,业务层归一为空数组。 -5. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。 -6. 面向公开列表的只读投影优先做成 public view / public 读模型表,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。跨玩法公开作品统一主读模型是 `public_work_gallery_entry` 和 `public_work_detail_entry`;各玩法既有 `*_gallery_card_view` / `*_gallery_view` / `custom_world_gallery_entry` 保留为 source view 和兼容路径。短期不把作品列表整体交给浏览器前端直接订阅;不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表。需要请求时间窗口的轻量统计可订阅 `public_work_play_daily_stat` 后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍走玩法 procedure / reducer。前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得自己做 join、聚合或权限逻辑。首屏、排序、字段归一、权限降级和 HTTP fallback 由 `api-server` BFF 维持。 -7. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。 -8. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json`、`levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。 -9. 修改后运行: +3. 已发布并持久化的 enum 新增 variant 只能追加到末尾,不能插入中间、删除、重命名或重排;否则会移动既有 variant 的判别序号,导致旧数据解释错误或生产发布 schema 迁移失败。 +4. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。 +5. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option>` 加 `#[default(None::>)]`,业务层归一为空数组。 +6. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。 +7. 面向公开列表的只读投影优先做成 public view / public 读模型表,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。跨玩法公开作品统一主读模型是 `public_work_gallery_entry` 和 `public_work_detail_entry`;各玩法既有 `*_gallery_card_view` / `*_gallery_view` / `custom_world_gallery_entry` 保留为 source view 和兼容路径。短期不把作品列表整体交给浏览器前端直接订阅;不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表。需要请求时间窗口的轻量统计可订阅 `public_work_play_daily_stat` 后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍走玩法 procedure / reducer。前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得自己做 join、聚合或权限逻辑。首屏、排序、字段归一、权限降级和 HTTP fallback 由 `api-server` BFF 维持。 +8. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。 +9. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json`、`levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。 +10. 修改后运行: ```bash npm run spacetime:generate diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index 0e15ab799..879e3ec3f 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -1070,13 +1070,13 @@ pub enum RuntimeProfileWalletLedgerSourceType { InviteInviterReward, InviteInviteeReward, PointsRecharge, - MembershipPeriodGrant, - MembershipPeriodReset, AssetOperationConsume, AssetOperationRefund, RedeemCodeReward, PuzzleAuthorIncentiveClaim, DailyTaskReward, + MembershipPeriodGrant, + MembershipPeriodReset, } impl RuntimeProfileWalletLedgerSourceType { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs index 33443323b..aef62c997 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs @@ -18,10 +18,6 @@ pub enum RuntimeProfileWalletLedgerSourceType { PointsRecharge, - MembershipPeriodGrant, - - MembershipPeriodReset, - AssetOperationConsume, AssetOperationRefund, @@ -31,6 +27,10 @@ pub enum RuntimeProfileWalletLedgerSourceType { PuzzleAuthorIncentiveClaim, DailyTaskReward, + + MembershipPeriodGrant, + + MembershipPeriodReset, } impl __sdk::InModule for RuntimeProfileWalletLedgerSourceType {