From 172bc22404ebc47d1c2953c91eb015224bb20e5c Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 16:47:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=B8=B8=E6=88=8F=E5=85=B1=E5=88=9B):=20?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=90=8E=E4=BF=AE=E6=AD=A3=20game=5Fdistribu?= =?UTF-8?q?tion=5Fgame=20=E5=88=97=E9=A1=BA=E5=BA=8F=EF=BC=88master=20?= =?UTF-8?q?=E7=9A=84=20price=5Fmud=5Fpoints=20=E5=9B=9E=E5=88=B0=E7=AC=AC?= =?UTF-8?q?=2026=20=E4=BD=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI(Backend tests 与 Repository checks)在 `check:spacetime-schema` 红: 「SpacetimeDB 表 game_distribution_game 的第 26 个字段从 price_mud_points 变为 fork_authorization,疑似字段顺序被调整」。 根因:本次合并把两侧各自「在表尾追加」的列并成了「我们的列在 master 的列之前」——schema guard 对表的字段 相对顺序是按 index 逐位比对的(只允许在结构体末尾追加)。 - `game_distribution_game` 表:`price_mud_points`(master 先追加,第 26 位)回到原位,本分支的 `fork_authorization` 追加在它**之后**(表尾); - `GameDistributionGameSnapshot` 按同一纪律排(同一 game 行的 RPC 形状,避免表与快照两套顺序); - 重新生成绑定:`module_bindings/game_distribution_game_type.rs`、`..._game_snapshot_type.rs` (列顺序属 wire format,客户端绑定必须与模块同步); - 两处结构体注释写明该纪律(master 的列保持 index、我们的列追加在后),避免下一次合并再踩; - 文档:`docs/project-memory/shared-memory/pitfalls.md` 增一条「合并 master 后 schema guard 的基线盲区」—— 本地守卫默认基线是 `git merge-base HEAD origin/master`,**merge 提交尚未建立时看不见**这类重排, 因此合并后必须用 `SPACETIME_SCHEMA_BASE_REF=` 复跑。 门禁:`SPACETIME_SCHEMA_BASE_REF=9f4c7d76 npm run check:spacetime-schema` 0(98 表);wasm build 0; `cargo check --all-targets` 0;`cargo test -p module-game-distribution` 113 passed;`cargo test -p api-server game_distribution` 101 passed;`cargo test -p module-runtime` 0;`cargo test -p spacetime-client` 43 passed; `cargo test -p spacetime-module` 294 passed / 1 ignored;DTO parity 0(62 组 / 17 构建器 / 15 手拼类型); `check:project-bundle-policy-parity` OK;`check:production-ops` OK;`check:doc-index` OK(261 份);`check:encoding` 0(5525 files);`cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check` 0;`git diff --check` 0。 --- docs/project-memory/shared-memory/pitfalls.md | 9 ++++++++ .../game_distribution_game_snapshot_type.rs | 2 +- .../game_distribution_game_type.rs | 6 ++--- .../spacetime-module/src/game_distribution.rs | 23 +++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0c6d227a4..70ecdb6e0 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6545,3 +6545,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **CPU 边界**:发行网关是公开无鉴权端点,压缩按请求实时算。release 构建实测 level 6 为 1.3 MiB→10 ms、8 MiB→59 ms、64 MiB(单文件上限)→522 ms 纯 CPU,因此在 `RELEASE_COMPRESSION_FAST_ABOVE_BYTES`(2 MiB)以上改用 level 1(zlib 端实测 level 1 约为 level 6 的 1/3 耗时、压缩比只差约 3%)。若后续要再做减法,优先把压缩结果按 `(对象键, 资源路径)` 缓存,而不是放宽级别。 - **验证**:`cargo test -p api-server --bin api-server -- game_distribution`(新增 ETag 作用域、`If-None-Match` 列表/弱校验命中、`gzip;q=0` 拒绝、文本压缩与二进制/小文件不压、304 无正文、`*` 对包内缺失路径仍 404 等用例);`npx vitest run packages/shared/src/components/PlatformGameLoadingSurface.test.tsx`、`src/components/game-distribution/GameDistributionPages.test.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx`;`npm run check:game-distribution-ops-rollback-e2e` 47 项通过(含压缩/Vary/ETag/304/不接受 gzip 四条新断言)。真实栈同链路复跑:`phaser.min.js` 1,375,976 B → 353,336 B(gzip,4 Mbps 下 2724 ms → 774 ms),用户端游戏画面 3298 ms → 1543 ms、后台试玩 4587 ms → 2694 ms。 - **关联**:`packages/shared/src/components/PlatformGameLoadingSurface.tsx`、`src/components/game-distribution/GamePlayPage.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、`deploy/nginx/README.md`。 + +## 2026-10-06 合并 master 后 schema guard 的基线盲区:本地全绿、CI 红(表字段相对顺序) + +- **现象**:合并 master(merge 提交 `eeb101458`,PR base `9f4c7d76`)后本地整套门禁全绿,CI 的 **Backend tests** 与 **Repository checks** 却都在 `check:spacetime-schema` 红:`表 game_distribution_game 的第 26 个字段从 price_mud_points 变为 fork_authorization,疑似字段顺序被调整`。 +- **原因**:schema guard 对**表的字段相对顺序**是按 index 逐位比对的(`baseTable.fields[index]` vs `currentTable.fields[index]`);而**不显式给基线时它取 `git merge-base HEAD origin/master`**——merge 提交还没建时那还是**旧 merge-base**,于是「两侧各自在表尾追加」被并成「我们的列插到了 master 的列前面」这件事本地完全看不见;merge 提交一建,基线变成 PR base,CI 立刻红。 +- **处理(现行口径)**:合并 master 后**先建 merge 提交**,再用 PR base 复跑 `SPACETIME_SCHEMA_BASE_REF= npm run check:spacetime-schema`。列顺序规则:**master 先追加的列保持原 index,我们的追加排它之后**(`price_mud_points` 第 26 位、`fork_authorization` 表尾);对应的 `SpacetimeType` 快照(`GameDistributionGameSnapshot`)按同一条纪律排,避免只改表不改快照。 +- **别踩**:`migration.rs` 白名单、DTO parity 成员、nginx SPA 路由、Pingora 路由清单都是**集合/无序**判定(各脚本内部 `Set`/`difference`),只有 **SpacetimeDB 表字段顺序**(以及同步生成的 `module_bindings/*` 里的 wire 顺序)对相对顺序敏感;改列序后必须重跑 `spacetime generate` 并只回写受影响文件。 +- **判据/取证**:`SPACETIME_SCHEMA_BASE_REF=9f4c7d76 npm run check:spacetime-schema`(exit 0)、`cargo check --all-targets` 0、`cargo test -p api-server game_distribution`(101 passed)、`cargo test -p spacetime-module`(294 passed)。 +- **关联**:`server-rs/crates/spacetime-module/src/game_distribution.rs`(game 表与 game 快照的列序注释)、`server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_{type,snapshot_type}.rs`、`scripts/check-spacetime-schema-guard.mjs`、`scripts/check-repository-ci.sh`(CI 侧基线传法)。 diff --git a/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_snapshot_type.rs index 296b150c2..3eb397958 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_snapshot_type.rs @@ -31,8 +31,8 @@ pub struct GameDistributionGameSnapshot { pub local_project_id: Option, pub cover_object_key: Option, pub screenshots_json: Option, - pub fork_authorization: String, pub price_mud_points: u64, + pub fork_authorization: String, } impl __sdk::InModule for GameDistributionGameSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_type.rs index cb361ae1c..657279b3a 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/game_distribution_game_type.rs @@ -32,8 +32,8 @@ pub struct GameDistributionGame { pub cover_object_key: Option, pub screenshots_json: Option, pub deleted_at: Option<__sdk::Timestamp>, - pub fork_authorization: String, pub price_mud_points: u64, + pub fork_authorization: String, } impl __sdk::InModule for GameDistributionGame { @@ -69,8 +69,8 @@ pub struct GameDistributionGameCols { pub cover_object_key: __sdk::__query_builder::Col>, pub screenshots_json: __sdk::__query_builder::Col>, pub deleted_at: __sdk::__query_builder::Col>, - pub fork_authorization: __sdk::__query_builder::Col, pub price_mud_points: __sdk::__query_builder::Col, + pub fork_authorization: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for GameDistributionGame { @@ -114,8 +114,8 @@ impl __sdk::__query_builder::HasCols for GameDistributionGame { cover_object_key: __sdk::__query_builder::Col::new(table_name, "cover_object_key"), screenshots_json: __sdk::__query_builder::Col::new(table_name, "screenshots_json"), deleted_at: __sdk::__query_builder::Col::new(table_name, "deleted_at"), - fork_authorization: __sdk::__query_builder::Col::new(table_name, "fork_authorization"), price_mud_points: __sdk::__query_builder::Col::new(table_name, "price_mud_points"), + fork_authorization: __sdk::__query_builder::Col::new(table_name, "fork_authorization"), } } } diff --git a/server-rs/crates/spacetime-module/src/game_distribution.rs b/server-rs/crates/spacetime-module/src/game_distribution.rs index 8415299a4..47992f662 100644 --- a/server-rs/crates/spacetime-module/src/game_distribution.rs +++ b/server-rs/crates/spacetime-module/src/game_distribution.rs @@ -765,14 +765,20 @@ pub struct GameDistributionGame { /// 版本行、发行包对象与冻结资料保持不可变,删除只改游戏行的可见性状态。 #[default(None::)] pub(crate) deleted_at: Option, - /// 作品级共创(Fork)授权档位:`forbidden` / `nonCommercial` / `full`。 - /// 只允许单向提升开放度;旧行按默认「禁止共创」解释。 - #[default("forbidden")] - pub(crate) fork_authorization: String, /// 买断制价格(泥点):`0` 表示免费。只在审核通过时随版本冻结资料整体生效, - /// 因此未通过审核的价格不会改变当前公开价格。必须保持在结构体末尾。 + /// 因此未通过审核的价格不会改变当前公开价格。 + /// + /// **列序是 wire format 的一部分**:这一列是 master 侧先追加的(第 26 位),序号必须保持不变; + /// 本分支后追加的 `fork_authorization` 必须排它**之后**——`check:spacetime-schema` 只允许在 + /// 结构体末尾追加,任何相对顺序变化都会被判成字段重排(合并时正是踩了这一条)。 #[default(0u64)] pub(crate) price_mud_points: u64, + /// 作品级共创(Fork)授权档位:`forbidden` / `nonCommercial` / `full`。 + /// 只允许单向提升开放度;旧行按默认「禁止共创」解释。 + /// + /// 本分支较 master 后追加,因此留在表尾(见上一列的注释)。 + #[default("forbidden")] + pub(crate) fork_authorization: String, } /// 作品之间的改编(Fork)血缘:一个作品最多有一个父,主键即子作品 ID。 @@ -1572,10 +1578,13 @@ pub struct GameDistributionGameSnapshot { pub local_project_id: Option, pub cover_object_key: Option, pub screenshots_json: Option, + /// 买断制价格(泥点);`0` 表示免费。 + /// + /// 与表同一条纪律:这一列是 master 侧先追加的,序号保持不变;本分支的 + /// `fork_authorization` 追加在它**之后**(客户端绑定与之同步生成)。 + pub price_mud_points: u64, /// 作品级共创授权档位;旧行由表默认值补齐为 `forbidden`。 pub fork_authorization: String, - /// 买断制价格(泥点);`0` 表示免费。 - pub price_mud_points: u64, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]