docs(游戏分发): 路径即身份,发布请求体去掉 gameId/versionNumber

- 明确规则:路径里有 {game_id}/{version_number} 时,body 不得重复同名字段;handler 只从 Path 取值,不存在“路径优先还是 body 优先”的校验分支。
- NewGameVersionRequest 删除 gameId 与 versionNumber:POST /games 只带 projectKey、首版号固定 1;嵌套路由身份全在路径。
- 复核其余写端点(submit/cancel/purchase/reviews/theme members/admin review/fork-authorization)请求体均不含路径身份字段;唯二重复的是待删 legacy GameDistributionCreateVersionRequest.versionNumber。
- 端点表 Response 列同步到改名后的新 DTO 名;clean cut 删除清单补 GameDistributionCreateGameRequest / GameDistributionCreateVersionRequest。
This commit is contained in:
2026-10-07 23:37:37 +08:00
parent 515b681d24
commit 1fc7c26d0b
3 changed files with 368 additions and 346 deletions
@@ -39,29 +39,30 @@ URL 里那串随机 UUID 既不可读,客户端也无法在提交前独立构
对外(作者 / 公开)版本一律用 `{version_number}`;后台与内部(`/admin/...`、收据、媒体命名空间)
继续用 `{version_id}`。完整端点表:
| 方法 | 路径 | 请求 | 响应 | 鉴权 / 头 |
|-------------------------------|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|---------------------------------------|
| POST | `/api/game-distribution/games` | multipart:`metadata` = `NewGameVersionRequest`(JSON),`cover` / `screenshot` 二进制 part | `GameDistributionPublishVersionResponse` | Bearer + `Idempotency-Key` + 发布灰度 |
| POST | `/api/game-distribution/games/{game_id}/versions/{version_number}` | 同上,路径值优先并与 body 校验一致 | `GameDistributionPublishVersionResponse` | Bearer + `Idempotency-Key` + 发布灰度 |
| GET | `/api/game-distribution/games/{game_id}/versions/{version_number}` | – | 版本详情(`GameDistributionPrivateVersion` + 作品上下文) | Bearer |
| PATCH | `/api/game-distribution/games/{game_id}/versions/{version_number}` | JSON:`GameMetadata` + `expectedPublicationRevision` CAS | 版本详情 | Bearer + `Idempotency-Key` |
| PUT | `…/versions/{version_number}/package` | `application/octet-stream` | 上传 ack | Bearer + `Idempotency-Key` |
| GET | `…/versions/{version_number}/package/upload-state` | – | 上传状态 | Bearer |
| PUT | `…/versions/{version_number}/package/chunk` | `application/octet-stream` + 偏移头 | 分片 ack | Bearer |
| POST | `…/versions/{version_number}/package/complete` | JSON | ack | Bearer + `Idempotency-Key` |
| POST | `…/versions/{version_number}/package/reset` | JSON | ack | Bearer |
| 方法 | 路径 | 请求 | 响应 | 鉴权 / 头 |
| ----------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- |
| POST | `/api/game-distribution/games` | multipart:`metadata` = `NewGameVersionRequest`(JSON),`cover` / `screenshot` 二进制 part | `GameDistributionPublishVersionResponse` | Bearer + `Idempotency-Key` + 发布灰度 |
| POST | `/api/game-distribution/games/{game_id}/versions/{version_number}` | 同上;body 不含 `gameId` / `versionNumber`,身份全部取自路径 | `GameDistributionPublishVersionResponse` | Bearer + `Idempotency-Key` + 发布灰度 |
| GET | `/api/game-distribution/games/{game_id}/versions/{version_number}` | – | 版本详情(`GameDistributionPrivateVersion` + 作品上下文) | Bearer |
| PATCH | `/api/game-distribution/games/{game_id}/versions/{version_number}` | JSON:`GameMetadata` + `expectedPublicationRevision` CAS | 版本详情 | Bearer + `Idempotency-Key` |
| PUT | `…/versions/{version_number}/package` | `application/octet-stream` | 上传 ack | Bearer + `Idempotency-Key` |
| GET | `…/versions/{version_number}/package/upload-state` | – | 上传状态 | Bearer |
| PUT | `…/versions/{version_number}/package/chunk` | `application/octet-stream` + 偏移头 | 分片 ack | Bearer |
| POST | `…/versions/{version_number}/package/complete` | JSON | ack | Bearer + `Idempotency-Key` |
| POST | `…/versions/{version_number}/package/reset` | JSON | ack | Bearer |
| PUT / GET / PUT / POST / POST | `…/versions/{version_number}/source`(`/upload-state` `/chunk` `/complete` `/reset`) | 与发行包族逐条对齐 | 同发行包族 | Bearer(幂等键同族) |
| POST | `…/versions/{version_number}/submit` | JSON:`expectedPublicationRevision` | 版本详情 | Bearer + `Idempotency-Key` |
| POST | `…/versions/{version_number}/cancel` | JSON:`CancelVersionRequest` | 版本详情 | Bearer + `Idempotency-Key` |
| PATCH | `/api/game-distribution/my-games/{game_id}` | multipart:`metadata` = `GameDistributionUpdateGameMetadataRequest`(并入 `forkAuthorization`)+ 媒体 part | `{ game, replayed }` | Bearer + `Idempotency-Key` |
| DELETE | `/api/game-distribution/my-games/{game_id}` | – | 软删 ack | Bearer |
| GET | `/api/game-distribution/games/{game_id}/forks` | – | `GameDistributionDerivedResponse` | 公开(原 `/derived` 改名) |
| GET | `/api/game-distribution/games/{game_id}/network` | – | `GameDistributionLineageResponse`(整棵改编树) | 公开(原 `/lineage` 改名) |
| GET | `/api/game-distribution/games/{game_id}/source`(`/package` `/project`) | – | `GameDistributionForkSourceResponse` / 二进制 | Bearer(原路径改名) |
| GET | `/api/game-distribution/games/{game_id}/network/stats` | – | `GameDistributionContributionResponse`(子树归集) | Bearer(原路径改名) |
| POST | `…/versions/{version_number}/submit` | JSON:`expectedPublicationRevision` | 版本详情 | Bearer + `Idempotency-Key` |
| POST | `…/versions/{version_number}/cancel` | JSON:`CancelVersionRequest` | 版本详情 | Bearer + `Idempotency-Key` |
| PATCH | `/api/game-distribution/my-games/{game_id}` | multipart:`metadata` = `GameDistributionUpdateGameMetadataRequest`(并入 `forkAuthorization`)+ 媒体 part | `{ game, replayed }` | Bearer + `Idempotency-Key` |
| DELETE | `/api/game-distribution/my-games/{game_id}` | – | 软删 ack | Bearer |
| GET | `/api/game-distribution/games/{game_id}/forks` | – | `GameDistributionForksResponse` | 公开(原 `/derived` 改名) |
| GET | `/api/game-distribution/games/{game_id}/network` | – | `GameDistributionNetworkResponse`(整棵改编树) | 公开(原 `/lineage` 改名) |
| GET | `/api/game-distribution/games/{game_id}/source`(`/package` `/project`) | – | `GameDistributionSourceResponse` / 二进制 | Bearer(原路径改名) |
| GET | `/api/game-distribution/games/{game_id}/network/stats` | – | `GameDistributionNetworkStatsResponse`(子树归集) | Bearer(原路径改名) |
- `POST /games` 首次发布只强制 `project_key`,`game_id` 由
`derive_game_distribution_game_id(owner, project_key)` 派生;带 `fork` 时是衍生首版。
`derive_game_distribution_game_id(owner, project_key)` 派生;**首版号固定为 `1`**(版本身份只在
路径 `…/versions/{version_number}` 里出现);带 `fork` 时是衍生首版。
- 作品资料编辑走既有通用 `PATCH /api/game-distribution/my-games/{game_id}`(自带
`expected_publication_revision` CAS);共创授权 `forkAuthorization` 并入该 PATCH,删除单字段
`PUT /api/game-distribution/games/{game_id}/fork-authorization`(`game_distribution.rs:721`)与
@@ -71,15 +72,25 @@ URL 里那串随机 UUID 既不可读,客户端也无法在提交前独立构
资料编辑,则此行删除,改资料仍走 `PATCH /my-games/{game_id}`。
- 后台族继续按 `{version_id}`(`/admin/api/game-distribution/versions/{version_id}/…`),不参与本次换号。
#### 路径即身份(body 不重复路径参数)
- 只要路径里有 `{game_id}` / `{version_number}`,请求体就**不得**再出现同名字段:handler 只从
`Path(…)` 取值,不存在「路径优先还是 body 优先」的校验分支。
- `NewGameVersionRequest` 因此**去掉** `gameId` 与 `versionNumber`:`POST /games` 只带 `projectKey`
(无路径槽,首版号固定 `1`);`POST /games/{game_id}/versions/{version_number}` 的身份全在路径。
- 复核:game-distribution 其余带路径参数的写端点(`submit` / `cancel` / `purchase` / `reviews` /
`theme members` / `admin review` / `fork-authorization`)请求体都不含路径身份字段;唯二重复的是
本请求与 **待删** 的 legacy `GameDistributionCreateVersionRequest.versionNumber`。
#### GitHub 术语对齐
| 现名 | 语义 | GitHub 术语 | 新名 |
| --- | --- | --- | --- |
| `derived` | 直接子代列表 | forks | `forks` |
| `lineage` | 以根为顶的整棵改编树 | fork network(UI「network members」) | `network` |
| `contribution` | 子树各代游玩数归集(`own` / `inherited` / `total`) | Insights / stats(`/stats/…`) | `network/stats` |
| `fork-source` | 拿本作品的成品包 / 工程源包去改编 | `source`(fork 的来源仓库) | `source` |
| `project-bundle` | 某版本作者上传的**可编辑工程源包**(与成品 `package` 并列的上行资产) | source archive(tarball / zipball) | 版本级 `source` |
| 现名 | 语义 | GitHub 术语 | 新名 |
| ---------------- | --------------------------------------------------------------------- | ------------------------------------- | --------------- |
| `derived` | 直接子代列表 | forks | `forks` |
| `lineage` | 以根为顶的整棵改编树 | fork network(UI「network members」) | `network` |
| `contribution` | 子树各代游玩数归集(`own` / `inherited` / `total`) | Insights / stats(`/stats/…`) | `network/stats` |
| `fork-source` | 拿本作品的成品包 / 工程源包去改编 | `source`(fork 的来源仓库) | `source` |
| `project-bundle` | 某版本作者上传的**可编辑工程源包**(与成品 `package` 并列的上行资产) | source archive(tarball / zipball) | 版本级 `source` |
- `forks` 只列直接子代,`network` 是整棵树,刻意分开,与 GitHub 的 forks / network members 同构。
- 游戏级 `source`(可 fork 的来源内容)与版本级 `source`(可编辑工程源包)同名但不同层级;若嫌重名,
@@ -87,31 +98,31 @@ URL 里那串随机 UUID 既不可读,客户端也无法在提交前独立构
#### fork 字段的可写位置
- `forkAuthorization`(作品级共创授权档位)是**作品属性**,并入通用
- `forkAuthorization`(作品级共创授权档位)是 **作品属性**,并入通用
`PATCH /api/game-distribution/my-games/{game_id}`;衍生作品继承父档位后是终态(再改 409)。
- `fork`(`parentGameId` / `parentVersionId`)只在 `POST /games` 创建时写一次,之后不可改——GitHub
也不允许改一个 fork 的 parent。
- `/forks` 是只读集合,不设 `PATCH /forks`;要改的授权属于**本作品**,不属于子代。
- `/forks` 是只读集合,不设 `PATCH /forks`;要改的授权属于 **本作品**,不属于子代。
#### projectKey 语义与必填状态
`projectKey` 是**客户端本地工程标识**(网页 / AGC 的本地 project id),只做**首次发布的身份锚**:服务端
`projectKey` 是 **客户端本地工程标识**(网页 / AGC 的本地 project id),只做 **首次发布的身份锚**:服务端
用 `derive_game_distribution_game_id(owner, projectKey)` 确定性派生 `gameId`,保证同一工程断网重试 /
多次发布落到同一作品。作品一旦建立,身份权威是路径里的 `gameId`,`projectKey` 不再参与对外寻址。
AGC 不是每次靠 `projectKey` 反查:它把远端身份**存在本地**——`.agent/manifest.json` 的
AGC 不是每次靠 `projectKey` 反查:它把远端身份 **存在本地**——`.agent/manifest.json` 的
`publication`(`GameCreationAppPublicationBinding`:`accountId` / `apiBaseUrl` / `gameId` / `revision` /
`latestVersionId` / `latestVersionNumber` / `status`,按「账号 + origin」隔离)。发布时:
**有绑定 → 只传 `gameId`**(走 `POST /games/{game_id}/versions/{version_number}`);**无绑定 → 只传
`latestVersionId` / `latestVersionNumber` / `status`,按「账号 + origin」隔离)。发布时: **有绑定 → 只传 `gameId`**(走
`POST /games/{game_id}/versions/{version_number}`); **无绑定 → 只传
`projectKey`**(走 `POST /games`)。无绑定时的恢复:`GET /my-games` 按 `projectKey` 精确匹配;绑定作品
线上 404 → 清掉绑定、回到首次发布。
| 接口 | `projectKey` | 说明 |
| --- | --- | --- |
| `POST /games` | **必填** | 没有 `gameId` 可参考,靠它派生作品身份;缺失 400 |
| `POST /games/{game_id}/versions/{version_number}` | 可选 | 身份由路径 `game_id` 定;AGC 已绑定时根本不传,只在恢复时兜底 |
| `PATCH /games/{game_id}/versions/{version_number}` | 不需要 | 不换身份 |
| `PATCH /my-games/{game_id}` | 不需要 | 资料编辑不换身份 |
| 接口 | `projectKey` | 说明 |
| -------------------------------------------------- | ------------ | ------------------------------------------------------------- |
| `POST /games` | **必填** | 没有 `gameId` 可参考,靠它派生作品身份;缺失 400 |
| `POST /games/{game_id}/versions/{version_number}` | 可选 | 身份由路径 `game_id` 定;AGC 已绑定时根本不传,只在恢复时兜底 |
| `PATCH /games/{game_id}/versions/{version_number}` | 不需要 | 不换身份 |
| `PATCH /my-games/{game_id}` | 不需要 | 资料编辑不换身份 |
### 3. 请求体与类型只做收敛,不新增第二套
@@ -131,26 +142,26 @@ AGC 不是每次靠 `projectKey` 反查:它把远端身份**存在本地**—
#### DTO / 记录类型改名(与路由术语一致)
| 现名 | 新名 |
| --- | --- |
| `GameDistributionDerivedResponse` | `GameDistributionForksResponse` |
| `GameDistributionDerivedGamesRecord` | `GameDistributionForksRecord` |
| `GameDistributionLineageResponse` | `GameDistributionNetworkResponse` |
| `GameDistributionLineage` | `GameDistributionNetwork`(作品在改编网中的位置摘要;JSON 字段名保持 `lineage` 不变) |
| `GameDistributionLineageRecord` | `GameDistributionNetworkRecord` |
| `GameDistributionLineageNode` | `GameDistributionNetworkNode` |
| `GameDistributionLineageNodeRecord` | `GameDistributionNetworkNodeRecord` |
| `GameDistributionLineageTreeRecord` | `GameDistributionNetworkTreeRecord` |
| `GameDistributionContributionResponse` | `GameDistributionNetworkStatsResponse` |
| `GameDistributionContributionTotals` | `GameDistributionNetworkStatsTotals` |
| `GameDistributionContributionGeneration` | `GameDistributionNetworkStatsGeneration` |
| `GameDistributionContributionChild` | `GameDistributionNetworkStatsChild` |
| `GameDistributionForkSource` | `GameDistributionSource` |
| `GameDistributionForkSourceKind` | `GameDistributionSourceKind` |
| `GameDistributionForkSourceResponse` | `GameDistributionSourceResponse` |
| `GameDistributionForkSourceRecord` | `GameDistributionSourceRecord` |
| `GameDistributionForkDeclaration` | `GameDistributionForkMetadata`(抽成独立子对象) |
| `GameDistributionSetForkAuthorizationRequest` | 删除(并入 `PATCH /my-games/{game_id}`) |
| 现名 | 新名 |
| --------------------------------------------- | ------------------------------------------------------------------------------------- |
| `GameDistributionDerivedResponse` | `GameDistributionForksResponse` |
| `GameDistributionDerivedGamesRecord` | `GameDistributionForksRecord` |
| `GameDistributionLineageResponse` | `GameDistributionNetworkResponse` |
| `GameDistributionLineage` | `GameDistributionNetwork`(作品在改编网中的位置摘要;JSON 字段名保持 `lineage` 不变) |
| `GameDistributionLineageRecord` | `GameDistributionNetworkRecord` |
| `GameDistributionLineageNode` | `GameDistributionNetworkNode` |
| `GameDistributionLineageNodeRecord` | `GameDistributionNetworkNodeRecord` |
| `GameDistributionLineageTreeRecord` | `GameDistributionNetworkTreeRecord` |
| `GameDistributionContributionResponse` | `GameDistributionNetworkStatsResponse` |
| `GameDistributionContributionTotals` | `GameDistributionNetworkStatsTotals` |
| `GameDistributionContributionGeneration` | `GameDistributionNetworkStatsGeneration` |
| `GameDistributionContributionChild` | `GameDistributionNetworkStatsChild` |
| `GameDistributionForkSource` | `GameDistributionSource` |
| `GameDistributionForkSourceKind` | `GameDistributionSourceKind` |
| `GameDistributionForkSourceResponse` | `GameDistributionSourceResponse` |
| `GameDistributionForkSourceRecord` | `GameDistributionSourceRecord` |
| `GameDistributionForkDeclaration` | `GameDistributionForkMetadata`(抽成独立子对象) |
| `GameDistributionSetForkAuthorizationRequest` | 删除(并入 `PATCH /my-games/{game_id}`) |
- 改名同步落 `packages/shared` 手写契约、`contracts/generated`、`check:game-distribution-dto-parity`
注册表与 `spacetime-client` 记录类型;旧名一律不留别名(clean cut)。
@@ -164,6 +175,9 @@ AGC 不是每次靠 `projectKey` 反查:它把远端身份**存在本地**—
所有 `/api/game-distribution/versions/{version_id}…` 对外路径(`:651-705`)、
`/api/game-distribution/games/{game_id}/derived`(`:796`)与
`PUT /api/game-distribution/games/{game_id}/fork-authorization`(`:721`)。
- 一并删除只服务旧两步的 DTO:`GameDistributionCreateGameRequest`
(`shared-contracts/src/game_distribution.rs:727`)、`GameDistributionCreateVersionRequest`
(`:790`,其 body 里带 `versionNumber`)、`GameDistributionSetForkAuthorizationRequest`(`:318`)。
- 改名的旧路径一并删:`/lineage`、`/fork-source`(含 `/package` `/project`)、`/contribution`、
`/versions/{version_id}/project-bundle`(由 `/network`、`/source`、`/network/stats`、
`/versions/{version_number}/source` 取代)。
@@ -25,27 +25,27 @@
改名表(Rust DTO / 记录 + TS 手写契约 + parity 注册表 + handler 名;旧名不留别名):
| 现名 | 新名 |
| --- | --- |
| `GameDistributionDerivedResponse` | `GameDistributionForksResponse` |
| `GameDistributionDerivedGamesRecord` | `GameDistributionForksRecord` |
| `GameDistributionLineage` | `GameDistributionNetwork`(JSON 字段名保持 `lineage` 不变) |
| `GameDistributionLineageRecord` | `GameDistributionNetworkRecord` |
| `GameDistributionLineageNode` | `GameDistributionNetworkNode` |
| `GameDistributionLineageNodeRecord` | `GameDistributionNetworkNodeRecord` |
| `GameDistributionLineageResponse` | `GameDistributionNetworkResponse` |
| `GameDistributionLineageTreeRecord` | `GameDistributionNetworkTreeRecord` |
| `GameDistributionContributionTotals` | `GameDistributionNetworkStatsTotals` |
| `GameDistributionContributionGeneration` | `GameDistributionNetworkStatsGeneration` |
| `GameDistributionContributionChild` | `GameDistributionNetworkStatsChild` |
| `GameDistributionContributionResponse` | `GameDistributionNetworkStatsResponse` |
| `GameDistributionContribution*Record` | `GameDistributionNetworkStats*Record` |
| `GameDistributionForkSource` | `GameDistributionSource` |
| `GameDistributionForkSourceKind` | `GameDistributionSourceKind` |
| `GameDistributionForkSourceResponse` | `GameDistributionSourceResponse` |
| `GameDistributionForkSourceRecord` | `GameDistributionSourceRecord` |
| `GameDistributionForkDeclaration` | `GameDistributionForkMetadata` |
| `GameDistributionSetForkAuthorizationRequest` | 删除 |
| 现名 | 新名 |
| --------------------------------------------- | ----------------------------------------------------------- |
| `GameDistributionDerivedResponse` | `GameDistributionForksResponse` |
| `GameDistributionDerivedGamesRecord` | `GameDistributionForksRecord` |
| `GameDistributionLineage` | `GameDistributionNetwork`(JSON 字段名保持 `lineage` 不变) |
| `GameDistributionLineageRecord` | `GameDistributionNetworkRecord` |
| `GameDistributionLineageNode` | `GameDistributionNetworkNode` |
| `GameDistributionLineageNodeRecord` | `GameDistributionNetworkNodeRecord` |
| `GameDistributionLineageResponse` | `GameDistributionNetworkResponse` |
| `GameDistributionLineageTreeRecord` | `GameDistributionNetworkTreeRecord` |
| `GameDistributionContributionTotals` | `GameDistributionNetworkStatsTotals` |
| `GameDistributionContributionGeneration` | `GameDistributionNetworkStatsGeneration` |
| `GameDistributionContributionChild` | `GameDistributionNetworkStatsChild` |
| `GameDistributionContributionResponse` | `GameDistributionNetworkStatsResponse` |
| `GameDistributionContribution*Record` | `GameDistributionNetworkStats*Record` |
| `GameDistributionForkSource` | `GameDistributionSource` |
| `GameDistributionForkSourceKind` | `GameDistributionSourceKind` |
| `GameDistributionForkSourceResponse` | `GameDistributionSourceResponse` |
| `GameDistributionForkSourceRecord` | `GameDistributionSourceRecord` |
| `GameDistributionForkDeclaration` | `GameDistributionForkMetadata` |
| `GameDistributionSetForkAuthorizationRequest` | 删除 |
- handler / facade 名:`get_game_derived_games`→`get_game_forks`、`get_game_lineage`→`get_game_network`、
`get_game_contribution`→`get_game_network_stats`、`get_fork_source*`→`get_source*`。
@@ -74,8 +74,9 @@
## 5. M3 路由收敛(新代码拆小文件)
- 新端点:`POST /api/game-distribution/games`(无 `game_id`,强制 `project_key`)复用 `game_distribution_publish.rs`
的统一发布深模块;`POST /games/{game_id}/versions/{version_number}` 由路径覆盖 body 的 `game_id` / `version_number`。
- 新端点:`POST /api/game-distribution/games`(无 `game_id`,强制 `project_key`,首版号固定 `1`)复用
`game_distribution_publish.rs` 的统一发布深模块;`POST /games/{game_id}/versions/{version_number}`
的 `game_id` / `version_number` **只从路径取**——`NewGameVersionRequest` 删除这两个字段,body 不再重复路径身份。
- 平台化:把 `publish_version` 收成 `publish_version_core(game_id: Option<String>, version_number: Option<u64>, multipart)`,
两个路由只做参数提取与鉴权,事务逻辑不变。
- 版本子资源 `package / source / submit / cancel / GET / PATCH` 迁到 `…/versions/{version_number}`;
@@ -84,6 +85,7 @@
- 退役:统一 `POST /versions`、旧两步 `POST /games/{game_id}/versions`、所有对外 `/versions/{version_id}…`、
`/derived`、`/lineage`、`/contribution`、`/fork-source`、`PUT .../fork-authorization` 与
`GameDistributionSetForkAuthorizationRequest`;`forkAuthorization` 并入 `PATCH /my-games/{game_id}`。
- 旧两步 DTO `GameDistributionCreateGameRequest` / `GameDistributionCreateVersionRequest` 一并删除。
- 后台 `/admin/api/game-distribution/versions/{version_id}/…` 保持不动。
## 6. M4 客户端
File diff suppressed because it is too large Load Diff