From 46d87685295e592686400b787fbff12e4ae40eb4 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:58:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=B8=B8=E6=88=8F=E5=88=86?= =?UTF-8?q?=E5=8F=91=E5=93=8D=E5=BA=94=20DTO=20=E7=9A=84=20Rust=20?= =?UTF-8?q?=E4=B8=8E=20TypeScript=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 给 GameDistributionGameSummary 补 coverObjectKey / screenshots / publicationRevision,给 GameDistributionPrivateVersion 补 publicationRevision,与 game_payload / private_version_payload 实际发出的键一致 - packages/shared 的 GameDistributionGame.currentVersion 改为可选,并写明作者侧响应自带同一份 game_payload 但不发这一项 - 删掉 DTO 一致性门禁里豁免这 4 个字段的 TS_ONLY_FIELDS 白名单,现在任一方向多出字段都会失败 - 同步游戏分发里程碑第 6 条的取证与剩余缺口 --- ...‹碑】游戏分发目录详情与在线游玩-2026-09-18.md | 2 +- .../shared/src/contracts/gameDistribution.ts | 6 +++++- .../check-game-distribution-dto-parity.mjs | 21 +++---------------- .../shared-contracts/src/game_distribution.rs | 10 +++++++++ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index 114b35c2d..046d0fa24 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -163,4 +163,4 @@ - 第 1 条(owner 不能由请求伪造;其他账号不能读取私有版本、上传、提交或撤销):领域侧有 `owner_is_required_for_version_and_package_mutations`;接口侧代码在越权时返回 403/404,但**没有跨账号读/写/撤销的接口级用例**——缺的就是这一层。 - 第 4 条(一份版本只接受一份已确认内容;同 key 同请求幂等、不同请求冲突;同版本并发上传不混写):前两半已覆盖——`idempotency_replays_same_snapshot_and_rejects_digest_conflict`、`validation_failure_can_retry_same_confirmed_package`、api-server 的 `idempotency_key_requires_a_bounded_non_empty_header`;**本轮新增** `a_version_accepts_only_one_confirmed_package`(不同摘要或字节数的确认被拒为 `PackageMismatch`,重复确认同一份内容返回同一包身份;变异去掉该守卫后只有它变红)。**仍缺**「同版本并发上传不混写」:串行化在 `spacetime-module` / api-server 的 CAS 那一层,领域服务本身是同步的,需要在那一层写用例。 - 第 5 条(校验可异步恢复;响应丢失、服务进程退出与客户端重试回到原版本;确定失败与未知结果可区分):只有 api-server 的 `recovery_action_covers_every_version_status` 覆盖"状态 → 恢复动作"的映射;后两句在 game-distribution 链路没有用例。 - - 第 6 条(状态/私有查询/错误 envelope 的 Rust 与 TS DTO 一致;新增 schema、迁移、表目录与绑定一致):后半句有门禁(`npm run lint` 内的 SpacetimeDB schema guard 覆盖 85 张表、生成绑定校验通过)。**本轮新增** `check:game-distribution-dto-parity`(已接进 `npm run lint`):按显式映射表逐字段/逐变体比对 14 组 Rust `shared-contracts` DTO 与手写 `packages/shared/src/contracts/gameDistribution.ts`,两个方向都做过变异验证——TS 侧把 `name` 改成 `displayName`、Rust 侧给 `GameDistributionAuthor` 加 `extra_field`,各自都让门禁失败并指出缺哪个字段;脚本同时登记了 7 个「服务端逐字段手拼 JSON、没有 Rust 结构体」的 TS 类型。**缺口**:`GameDistributionGame` / `GameDistributionPrivateVersion` 这两个响应仍有 4 个附加字段(`coverObjectKey` / `screenshots` / `publicationRevision`)不在 Rust 结构体里——`public_game_payload` / `game_payload` 是 `serde_json::json!` 手拼的;要彻底满足这一句,需要先把响应改成结构化类型,届时把脚本里的 `TS_ONLY_FIELDS` 清空即可。 + - 第 6 条(状态/私有查询/错误 envelope 的 Rust 与 TS DTO 一致;新增 schema、迁移、表目录与绑定一致):后半句有门禁(`npm run lint` 内的 SpacetimeDB schema guard 覆盖 85 张表、生成绑定校验通过)。**本轮新增** `check:game-distribution-dto-parity`(已接进 `npm run lint`):按显式映射表逐字段/逐变体比对 14 组 Rust `shared-contracts` DTO 与手写 `packages/shared/src/contracts/gameDistribution.ts`,两个方向都做过变异验证——TS 侧把 `name` 改成 `displayName`、Rust 侧给 `GameDistributionAuthor` 加 `extra_field`,各自都让门禁失败并指出缺哪个字段;脚本同时登记了 7 个「服务端逐字段手拼 JSON、没有 Rust 结构体」的 TS 类型。**本轮补齐** `coverObjectKey` / `screenshots` / `publicationRevision` 四个字段进 Rust 结构体(依据是 `game_payload` 与 `private_version_payload` 实际发出的键),并删掉脚本里用来豁免它们的 `TS_ONLY_FIELDS` 白名单:现在任一方向多出字段都会让门禁失败,作者侧响应省略 `currentVersion` 这一条差异改用 TS 可选字段描述。**剩余缺口**:这两个响应仍是 `serde_json::json!` 手拼,Rust 侧没有构建它们的结构体,所以门禁只能保证类型字段一致,不能保证构建器真的按类型发键;要彻底满足这一句,需要先把响应改成结构化构建。 diff --git a/packages/shared/src/contracts/gameDistribution.ts b/packages/shared/src/contracts/gameDistribution.ts index 2d2391b48..388a3ac3a 100644 --- a/packages/shared/src/contracts/gameDistribution.ts +++ b/packages/shared/src/contracts/gameDistribution.ts @@ -94,7 +94,11 @@ export type GameDistributionGame = { status: Extract; /** 公开切换 CAS 版本号;作者下架与管理员审核都必须回传当前值。 */ publicationRevision: number; - currentVersion: GameDistributionVersionSummary | null; + /** + * 公开目录与公开详情一定带这一项(没有已公开版本时为 null);作者侧响应(我的作品列表、 + * 创建、提交、审核、撤回)走服务端同一个 `game_payload`,不带这一项。 + */ + currentVersion?: GameDistributionVersionSummary | null; playCount: number; createdAt: string; }; diff --git a/scripts/check-game-distribution-dto-parity.mjs b/scripts/check-game-distribution-dto-parity.mjs index a3d578f22..6d5bb3b2a 100644 --- a/scripts/check-game-distribution-dto-parity.mjs +++ b/scripts/check-game-distribution-dto-parity.mjs @@ -5,7 +5,7 @@ // Rust 侧加字段而 TS 侧忘改时,只有跑起来才会发现。这里用一张显式映射表逐字段/逐变体比对: // - 映射表同时是「哪些 Rust DTO 必须在 TS 里有对应类型」的清单; // - `TS_ONLY_TYPES` 是「服务端手拼 JSON、没有 Rust 结构体」的说明清单; -// - `TS_ONLY_FIELDS` 记录已知的、有原因的响应侧附加字段。 +// - 两侧字段必须逐一对齐,任一方向多出字段都会失败(没有白名单)。 // 任何一处没有分类的新类型、新字段都会让检查失败,避免静默漂移。 import fs from 'node:fs'; @@ -52,17 +52,6 @@ const TS_ONLY_TYPES = [ 'GameDistributionCancelVersionResponse', ]; -// 已知的响应侧附加字段:Rust 结构体描述请求侧视图,TS 类型描述响应视图。 -// 只允许「TS 多出」这些字段;Rust 多出字段一律报错(TS 必须跟上)。 -const TS_ONLY_FIELDS = { - GameDistributionGame: [ - 'coverObjectKey', - 'screenshots', - 'publicationRevision', - ], - GameDistributionPrivateVersion: ['publicationRevision'], -}; - function camelCase(value) { return value.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase()); } @@ -160,14 +149,10 @@ for (const [rustName, tsName] of PAIRS) { `${tsName} 缺少字段/变体:${missingInTs.join(', ')}(${rustName} 已有)`, ); } - const allowlist = new Set(TS_ONLY_FIELDS[tsName] ?? []); - const extraInTs = difference( - tsDefinition.members, - rustDefinition.members, - ).filter((value) => !allowlist.has(value)); + const extraInTs = difference(tsDefinition.members, rustDefinition.members); if (extraInTs.length > 0) { failures.push( - `${tsName} 多出未登记字段/变体:${extraInTs.join(', ')}(要么补进 TS_ONLY_FIELDS 并写明原因,要么在 Rust 侧补上)`, + `${tsName} 多出未登记字段/变体:${extraInTs.join(', ')}(要么在 Rust 侧补上,要么先确认真实响应不再发出)`, ); } } diff --git a/server-rs/crates/shared-contracts/src/game_distribution.rs b/server-rs/crates/shared-contracts/src/game_distribution.rs index 4eb118b9e..edfa2b6f1 100644 --- a/server-rs/crates/shared-contracts/src/game_distribution.rs +++ b/server-rs/crates/shared-contracts/src/game_distribution.rs @@ -107,6 +107,12 @@ pub struct GameDistributionGameSummary { pub tags: Vec, pub cover_color: String, pub icon: String, + /// 公开封面的 OSS objectKey;未上传封面时为空。 + #[serde(default)] + pub cover_object_key: Option, + /// 随版本冻结的公开截图 objectKey 列表。 + #[serde(default)] + pub screenshots: Vec, pub author: GameDistributionAuthor, pub device_support: GameDistributionDeviceSupport, #[serde(default)] @@ -114,6 +120,8 @@ pub struct GameDistributionGameSummary { #[serde(default)] pub orientation: Option, pub status: GameDistributionVisibility, + /// 公开切换 CAS 版本号:作者下架与管理员审核都必须回传当前值。 + pub publication_revision: u64, pub current_version: Option, pub play_count: u64, pub created_at: String, @@ -170,6 +178,8 @@ pub struct GameDistributionPrivateVersion { pub package_sha256: String, pub package_bytes: u64, pub status: GameDistributionVersionStatus, + /// 这一版所属游戏的公开切换版本号(撤回与审核回传用)。 + pub publication_revision: u64, #[serde(default)] pub review_reason: Option, pub created_at: String,