From e04032b7d9afb71b962113424aa4432e54bd9fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 8 Oct 2026 12:44:18 +0800 Subject: [PATCH] =?UTF-8?q?projectKey=20=E5=8F=AA=E5=9C=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E4=BD=9C=E5=93=81=E6=97=B6=E8=90=BD=E5=BA=93=E5=B9=B6?= =?UTF-8?q?=E6=A0=B8=E5=AF=B9=E7=A1=AE=E5=AE=9A=E6=80=A7=20gameId=20`get?= =?UTF-8?q?=5For=5Fcreate=5Fgame=5Fdistribution=5Fgame=5Ffor=5Fpublish=5Ft?= =?UTF-8?q?x`=20=E6=8C=89=20owner=20+=20projectKey=20=E5=85=9C=E5=BA=95?= =?UTF-8?q?=E5=91=BD=E4=B8=AD=E6=97=B6=E6=A0=B8=E5=AF=B9=20`game=5Fid`=20?= =?UTF-8?q?=E7=9B=B8=E7=AD=89=EF=BC=8C=E5=90=A6=E5=88=99=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=85=B3=E9=97=AD=EF=BC=8C=E9=81=BF=E5=85=8D=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=8C=82=E5=88=B0=E4=B8=8E=E6=B4=BE=E7=94=9F=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E4=BD=9C=E5=93=81=20?= =?UTF-8?q?=E8=BD=AF=E5=88=A0=E5=A4=8D=E6=B4=BB=E4=B8=8D=E5=86=8D=E7=94=A8?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=9A=84=20projectKey=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E6=97=A2=E6=9C=89=E4=BD=9C=E5=93=81=E7=9A=84=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E9=94=9A=EF=BC=88=E8=AF=B7=E6=B1=82=E4=B8=8D=E5=B8=A6=E6=97=B6?= =?UTF-8?q?=E6=B2=BF=E7=94=A8=E5=8E=9F=E5=80=BC=EF=BC=89=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=BA=90=E7=A0=81=E7=BB=93=E6=9E=84=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spacetime-module/src/game_distribution.rs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/server-rs/crates/spacetime-module/src/game_distribution.rs b/server-rs/crates/spacetime-module/src/game_distribution.rs index 9c70afcd2..8b4a47d5d 100644 --- a/server-rs/crates/spacetime-module/src/game_distribution.rs +++ b/server-rs/crates/spacetime-module/src/game_distribution.rs @@ -3801,6 +3801,10 @@ fn get_or_create_game_distribution_game_for_publish_tx( .as_deref() .map(str::trim) .filter(|value| !value.is_empty()); + // `project_key` 只是**创建作品**的身份锚(`POST /games` 无路径槽时用):同一 + // `(owner, project_key)` 必须确定性派生同一个 `game_id`,追加版本一律走路径 `game_id`、 + // 不读 `project_key`。这里按 owner + key 兜底命中时必须核对 game_id 相等,否则历史分叉会 + // 让版本行挂到与派生身份不一致的作品上,写出「版本属于 A、作品是 B」的孤儿数据。 if let Some(project_key) = project_key { if let Some(game) = ctx .db @@ -3811,6 +3815,9 @@ fn get_or_create_game_distribution_game_for_publish_tx( game.deleted_at.is_none() && game.project_key.as_deref() == Some(project_key) }) { + if game.game_id != input.game_id { + return Err("projectKey 已绑定到另一个作品".to_string()); + } ensure_game_distribution_game_publishable(&game)?; return Ok(game); } @@ -4091,12 +4098,16 @@ fn revived_game_distribution_game_for_publish( game.orientation = input.orientation.clone(); game.cover_object_key = input.cover_object_key.clone(); game.screenshots_json = input.screenshots_json.clone(); - game.project_key = input + // `project_key` 是作品的身份锚,只在**创建**时落库:请求带非空 key 才覆盖,否则沿用既有行的值。 + // 追加版本按设计不带 `projectKey`,不能因此把存活作品的身份锚清空。 + if let Some(project_key) = input .project_key .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) - .map(str::to_string); + { + game.project_key = Some(project_key.to_string()); + } game.publication_revision = 0; game.active_version_id = None; game.visibility = GAME_DISTRIBUTION_VISIBILITY_UNPUBLISHED.to_string(); @@ -8540,6 +8551,20 @@ mod tests { ); } + /// `projectKey` 兜底命中时 `game_id` 必须一致,否则拒绝,避免孤儿版本。 + #[test] + fn project_key_fallback_refuses_a_different_game_id() { + let source = include_str!("game_distribution.rs"); + let body = function_body( + source, + "fn get_or_create_game_distribution_game_for_publish_tx(", + ); + assert!( + body.contains("game.game_id != input.game_id"), + "projectKey 兜底必须核对确定性派生的 game_id" + ); + } + /// 版本表的 `change_summary` 必须留在**表尾**并带默认值。 /// /// schema guard 只允许「末尾追加 + 显式默认」:插在中间会让既有行的列序整体错位(不是迁移能补