feat(游戏共创): M2a 受鉴权内容下发通道(fork-source 元数据 + 整包取件)
- 契约:新增 `GameDistributionForkSourceKind` / `GameDistributionForkSource` / `GameDistributionForkSourceResponse`(shared-contracts + TS 镜像 + parity 登记),字段 `gameId / versionId / source / sha256 / bytes / downloadPath`,**不含对象键** - 读取:spacetime-module 新增 `GameDistributionForkSourceInput`、扁平结果类型与 procedure `get_game_distribution_fork_source_and_return`;tx 把「行是否存在 / 能否作为来源(未删+已公开+有当前公开版本)/ 授权档位原值」分开回传——既有 owner-or-public 读会把「已软删除」与「未公开」都折成 None,HTTP 层就没法区分 404 与 409;`sha256` / `bytes` 取自版本行,不重算 - 客户端:spacetime-client 新增 record + mapper + `get_game_distribution_fork_source`(生成绑定把 `package_sha256` 折成 `package_sha_256`,与版本快照同约定) - 路由:新增 `fork_sources` 子路由(`require_bearer_auth` + `add_no_store_response_headers`,**不叠加发布灰度**),两条:`GET …/fork-source`(元数据)与 `GET …/fork-source/package`(ZIP 字节) - 校验:抽出纯函数 `fork_source_target` 供两个 handler 共用——404 `FORK_SOURCE_NOT_FOUND` / 409 `FORK_SOURCE_NOT_AVAILABLE` / 403 `FORK_NOT_AUTHORIZED`(未知档位按禁止解释);`available` 为真却缺版本元数据时失败关闭为 409 - 取件本体:复用发行网关的 `release_package_bytes`(整包进内存 + 进程内缓存,4 条 / 256 MiB),不新造 OSS 客户端;缓存值由 `Arc<Vec<u8>>` 改为 `Bytes`,整包交给响应体只加引用计数、不复制;响应**不做**引用归一化与 `RELEASE_STORAGE_BOOTSTRAP` 注入(下发原始构建产物,客户端按摘要校验后离线解压);`RELEASE_PACKAGE_CACHE` 顺手改用 `LazyLock`(初始值编译期已知) - 路径:`downloadPath` 为同源相对路径,复用新抽出的 `is_path_safe_game_id`(与发行入口同一判据),绝不下发对象键 - 测试:新增 4 条——两条路由未带 Bearer → 401;`fork_source_target` 的 404 / 409 / 403 / 未知档位 / 缺元数据失败关闭逐项断言(409 先于 403);元数据 payload 的 sha256/bytes 取自行、downloadPath 形状、序列化后不含对象键;取件包头与长度(`application/zip`、`Content-Length` = 行 `package_bytes`、`Content-Disposition`、`no-store`) - 文档:技术方案 §3.4 把两个接口从「规划」改成与实现一致的形状,并单独拆出「登录用户(Bearer,不叠加发布灰度)」小节;§3.5.1 的「下载通道(当前不存在)」改为 M2a 已实现,写明它只服务当前公开版本、不做归一化、大包续传仍待做
This commit is contained in:
@@ -312,9 +312,17 @@ pub(crate) project_bundle_sha256: Option<String>,
|
||||
| --- | --- |
|
||||
| `PUT /games/{gameId}/fork-authorization`(新) | body `{ expectedForkAuthorization, forkAuthorization }` + `Idempotency-Key`;只允许提升;返回最新 `forkAuthorization` 与 `replayed` |
|
||||
| `PUT /versions/{versionId}/project-bundle`(新) | `application/octet-stream`,整包或复用现役分片族(`upload-state` / `chunk` / `complete`);服务端校验 zip 门禁后写 OSS 并确认。前置:调用者是该版本作者,且该版本尚**没有**工程包;**发布阶段**上传只要求版本归属,**补齐**场景额外要求该版本是作品当前公开版本且 `fork_authorization != forbidden` |
|
||||
| `GET /games/{gameId}/fork-source`(新) | 校验授权与来源可用性,返回 `{ gameId, versionId, bundleSha256, bundleBytes, downloadPath }`;`downloadPath` 为受鉴权网关路径,不是可匿名访问的对象键 |
|
||||
| `POST /games`(**既有,请求增量**) | 追加可选 `forkedFromGameId` / `forkedFromVersionId` |
|
||||
|
||||
#### 登录用户(Bearer,**不叠加**发布灰度)
|
||||
|
||||
灰度只针对「发布」,改编不应当被发布开关挡住:任何已登录用户都能改编已授权公开的作品。两个接口共用同一条校验(同一个函数,规则不分叉),错误码沿用 `FORK_*`:
|
||||
|
||||
| 方法 / 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `GET /games/{gameId}/fork-source`(**M2a 已实现**) | 受鉴权元数据。404 `FORK_SOURCE_NOT_FOUND`(作品不存在)/ 409 `FORK_SOURCE_NOT_AVAILABLE`(已软删除、未公开、或没有当前公开版本)/ 403 `FORK_NOT_AUTHORIZED`(授权为 `forbidden` 或未知档位,未知按禁止解释);通过时返回 `{ forkSource: { gameId, versionId, source, sha256, bytes, downloadPath } }`。`sha256` / `bytes` 取自版本行已存的发行包摘要(不重算),`source` 当前恒为 `package`(M2b 有工程源包时优先 `project`),`downloadPath` 是同源相对路径,**绝不下发 OSS 对象键** |
|
||||
| `GET /games/{gameId}/fork-source/package`(**M2a 已实现**) | 取件本体:直接回该版本发行包 ZIP 的字节,头为 `Content-Type: application/zip`、`Content-Length`、`Content-Disposition: attachment; filename="{gameId}-{versionId}.zip"`、`Cache-Control: no-store`。数据复用现役发行网关的读包路径(整包进内存 + 进程内缓存,上限 4 条 / 256 MiB),**不做**网关那套引用归一化与 `RELEASE_STORAGE_BOOTSTRAP` 注入——下发的是原始构建产物,客户端要按摘要校验后离线解压,任何改写都会让摘要对不上 |
|
||||
|
||||
#### 后台(admin)
|
||||
|
||||
| 方法 / 路径 | 说明 |
|
||||
@@ -335,7 +343,7 @@ pub(crate) project_bundle_sha256: Option<String>,
|
||||
#### 3.5.1 成品包路径(零新增上传资产,但只到「可玩参考」)
|
||||
|
||||
- **内容来源**:父作品当前公开版本的 `package_object_key`(已存在,无需作者做任何额外动作)。服务端按现有发行网关同一套对象读取与校验复用该 ZIP,不新造存储。包内容是纯运行产物:`collect_project_export_package_files`(`src-tauri/src/project/export.rs:770-804`)只收 `dist` 树(条目统一改名成 `game/...`)+ 根 `assets/` 兜底 + `exports/README.md`,**根 `package.json` 与源码不进 ZIP**。
|
||||
- **下载通道(当前不存在,M2a 必须新增)**:平台今天**没有**「下载某个版本发行包」的接口——作者侧路由只有 create/upload/chunk/complete/reset/submit/get_owner_version/cancel,`private_version_payload`(`api-server/src/modules/game_distribution.rs:3029-3041`)也不返回对象键或下载地址;对外只有公开发行网关逐文件读取 `/api/game-distribution/releases/{game_id}/{*asset_path}`(`:395-421`),且**只服务当前已公开版本**(`:800-863`,非公开/下架 404),扩展名限制在白名单内(`module-game-distribution/src/release.rs:36-64`:html/js/mjs/css/json/wasm/svg/png/jpg/webp/gif/avif/ico/mp3/ogg/wav/m4a/mp4/webm/woff/woff2/ttf/otf/txt/xml)。因此 §3.4 规划的受鉴权入口 `GET /games/{gameId}/fork-source`(返回网关路径而非裸对象键)是**待实现项**,不是既有能力;同时要注意服务端读包是整包进内存 + 进程内缓存(`api-server/src/modules/game_distribution.rs:926-958`,缓存上限 4 条 / 256 MiB,`:101-104`),客户端侧 `fetch_limited_bytes`(`apps/ai-game-creator-shell/src-tauri/src/template_library.rs:564-591`)同样整包进内存、上限 512 MiB、无 Range 无续传;分片能力只有**上传侧**(`game_package_upload.rs:96-105`、`runtime.rs:353-363`),下载侧完全没有对应能力。
|
||||
- **下载通道(M2a 已实现)**:`GET /games/{gameId}/fork-source`(元数据)+ `GET /games/{gameId}/fork-source/package`(ZIP 字节)两条受鉴权路由,**要求 Bearer、不叠加发布灰度**(灰度只针对发布),校验顺序与错误码见 §3.4「登录用户」小节。服务的是**当前已公开版本**的发行包(与公开发行网关同一份对象),元数据里的 `sha256` / `bytes` 直接取该版本行,`downloadPath` 是同源相对路径、不下发对象键;本体复用发行网关的读包路径(整包进内存 + 进程内缓存,上限 4 条 / 256 MiB,`:926-958`、`:101-104`),但**不做**引用归一化与 bootstrap 注入——取件下发的是原始构建产物,客户端按摘要校验后离线解压,任何改写都会让摘要对不上。客户端侧仍是整包读取(`fetch_limited_bytes`,上限 512 MiB、无 Range 无续传;分片能力只有上传侧),大包的续传/分片下载是后续议题。**未实现**的是按版本直取任意历史版本:取件只服务当前公开版本,与发行网关同口径。
|
||||
- **AGC 建项:能试玩,不能发布。**
|
||||
- 试玩成立:`validate_project_game_entry`(`src-tauri/src/project/verification.rs:49-66`)只要求入口是 HTML 文档、引用可解析(`:68-100`),与 `package.json`、构建脚本无关;把包内文件铺进可玩入口目录即可试玩。
|
||||
- 发布必然被拦:导出/发布的唯一入口 `export_local_project_package`(`src-tauri/src/commands/desktop.rs:1148-1156`)→ `export_local_project_package_for_publish_at`(`export.rs:259-306`),其中「已有可玩入口」的静态入口分支(`export.rs:262-265`)**仍然无条件调用 `ensure_publish_project_stack(root)`**(实检在 `export.rs:413-459`,调用点在 `export.rs:463-468`):`package.json` 必须声明 phaser 主版本 4(`:440-453`)且声明 vite 依赖或存在 `vite.config.*`(`:398-410`、`:454-458`)。注释直接写明「无 `package.json` 的单文件 HTML 项目不属于首版 Phaser 4 发布合同」(`:407-408`),并有反例测试(`:1239-1243`)。该分支**不要求 build 脚本**——`resolve_publish_build_plan` 只在「无入口」分支调用(`export.rs:266-273`)——所以「能试玩」确实不需要构建,但「能发布」需要依赖声明。
|
||||
|
||||
@@ -225,6 +225,33 @@ export type GameDistributionDerivedResponse = {
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fork 取件内容的形态。M2a 只有已构建的发行成品包;M2b 引入工程源包后,同一版本同时存在
|
||||
* 两者时优先 `project`。
|
||||
*/
|
||||
export type GameDistributionForkSourceKind = 'package' | 'project';
|
||||
|
||||
/**
|
||||
* Fork 取件元数据:客户端据此校验取到的内容并决定建项形态。
|
||||
*
|
||||
* 只含版本身份与摘要,**不含 OSS 对象键**——下载走同源受鉴权路径 `downloadPath`,
|
||||
* 对象键只留在服务端。`sha256` / `bytes` 取自版本行已存的发行包摘要,不重新计算。
|
||||
*/
|
||||
export type GameDistributionForkSource = {
|
||||
gameId: string;
|
||||
versionId: string;
|
||||
source: GameDistributionForkSourceKind;
|
||||
sha256: string;
|
||||
bytes: number;
|
||||
/** 同源相对路径;客户端按当前 origin 解析后带 Bearer 请求,不接受绝对 URL。 */
|
||||
downloadPath: string;
|
||||
};
|
||||
|
||||
/** Fork 取件元数据接口(`GET …/fork-source`)的响应体。 */
|
||||
export type GameDistributionForkSourceResponse = {
|
||||
forkSource: GameDistributionForkSource;
|
||||
};
|
||||
|
||||
export type GameDistributionGame = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
@@ -63,6 +63,10 @@ const PAIRS = [
|
||||
['GameDistributionLineageNode', 'GameDistributionLineageNode'],
|
||||
['GameDistributionLineageResponse', 'GameDistributionLineageResponse'],
|
||||
['GameDistributionDerivedResponse', 'GameDistributionDerivedResponse'],
|
||||
// M2a 取件通道:元数据响应与内容形态同样逐字段对齐(不含对象键是契约的一部分)。
|
||||
['GameDistributionForkSourceKind', 'GameDistributionForkSourceKind'],
|
||||
['GameDistributionForkSource', 'GameDistributionForkSource'],
|
||||
['GameDistributionForkSourceResponse', 'GameDistributionForkSourceResponse'],
|
||||
['GameDistributionForkDeclaration', 'GameDistributionForkDeclaration'],
|
||||
[
|
||||
'GameDistributionSetForkAuthorizationRequest',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -300,6 +300,41 @@ pub struct GameDistributionDerivedResponse {
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Fork 取件内容的形态。M2a 只有已构建的发行成品包;M2b 引入工程源包后,同一版本同时存在
|
||||
/// 两者时优先 `project`。
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum GameDistributionForkSourceKind {
|
||||
/// 已构建的发行成品包(静态可玩产物)。
|
||||
Package,
|
||||
/// 可编辑的工程源包(M2b 引入)。
|
||||
Project,
|
||||
}
|
||||
|
||||
/// Fork 取件元数据:客户端据此校验取到的内容并决定建项形态。
|
||||
///
|
||||
/// 只含版本身份与摘要,**不含 OSS 对象键**——下载走同源受鉴权路径 `download_path`,
|
||||
/// 对象键只留在服务端。`sha256` / `bytes` 取自版本行已存的发行包摘要,不重新计算。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionForkSource {
|
||||
pub game_id: String,
|
||||
pub version_id: String,
|
||||
pub source: GameDistributionForkSourceKind,
|
||||
pub sha256: String,
|
||||
pub bytes: u64,
|
||||
/// 同源相对路径(如 `/api/game-distribution/games/game_1/fork-source/package`);
|
||||
/// 客户端按当前 origin 解析后带 Bearer 请求,不接受绝对 URL。
|
||||
pub download_path: String,
|
||||
}
|
||||
|
||||
/// Fork 取件元数据接口(`GET /api/game-distribution/games/{gameId}/fork-source`)的响应体。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionForkSourceResponse {
|
||||
pub fork_source: GameDistributionForkSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameDistributionListResponse {
|
||||
|
||||
@@ -97,14 +97,14 @@ pub use self::game_distribution::{
|
||||
GameDistributionAdminGameRecord, GameDistributionAdminUserReviewDetailRecord,
|
||||
GameDistributionAdminUserReviewListRecord, GameDistributionAdminUserReviewRecord,
|
||||
GameDistributionAdminVersionRecord, GameDistributionDerivedGamesRecord,
|
||||
GameDistributionGameRecord, GameDistributionLineageNodeRecord,
|
||||
GameDistributionLineageRecord, GameDistributionLineageTreeRecord,
|
||||
GameDistributionOwnerGameRecord, GameDistributionPublicGameRecord,
|
||||
GameDistributionRatingSummaryRecord, GameDistributionReviewGameListRecord,
|
||||
GameDistributionReviewGameRecord, GameDistributionReviewModerationOperationRecord,
|
||||
GameDistributionReviewModerationRecord, GameDistributionUserReviewListRecord,
|
||||
GameDistributionUserReviewRecord, GameDistributionUserReviewSaveRecord,
|
||||
GameDistributionVersionRecord,
|
||||
GameDistributionForkSourceRecord, GameDistributionGameRecord,
|
||||
GameDistributionLineageNodeRecord, GameDistributionLineageRecord,
|
||||
GameDistributionLineageTreeRecord, GameDistributionOwnerGameRecord,
|
||||
GameDistributionPublicGameRecord, GameDistributionRatingSummaryRecord,
|
||||
GameDistributionReviewGameListRecord, GameDistributionReviewGameRecord,
|
||||
GameDistributionReviewModerationOperationRecord, GameDistributionReviewModerationRecord,
|
||||
GameDistributionUserReviewListRecord, GameDistributionUserReviewRecord,
|
||||
GameDistributionUserReviewSaveRecord, GameDistributionVersionRecord,
|
||||
};
|
||||
pub use self::llm_router_account::{LlmRouterAccountRecord, LlmRouterAccountUpsertRecordInput};
|
||||
pub use self::payment::{
|
||||
@@ -170,10 +170,11 @@ pub(crate) use self::external_generation::{
|
||||
pub(crate) use self::game_distribution::{
|
||||
ensure_admin_user_review_result, ensure_user_review_result, map_admin_user_review,
|
||||
map_game_distribution_admin_game_list_result, map_game_distribution_derived_games_result,
|
||||
map_game_distribution_game_result, map_game_distribution_lineage_result,
|
||||
map_game_distribution_owner_game_list_result, map_game_distribution_public_game_list_result,
|
||||
map_game_distribution_public_game_result, map_game_distribution_review_list_result,
|
||||
map_game_distribution_version_result, map_review_moderation_operation, map_user_review,
|
||||
map_game_distribution_fork_source_result, map_game_distribution_game_result,
|
||||
map_game_distribution_lineage_result, map_game_distribution_owner_game_list_result,
|
||||
map_game_distribution_public_game_list_result, map_game_distribution_public_game_result,
|
||||
map_game_distribution_review_list_result, map_game_distribution_version_result,
|
||||
map_review_moderation_operation, map_user_review,
|
||||
};
|
||||
pub(crate) use self::payment::{
|
||||
map_payment_api_key_list_result, map_payment_api_key_result, map_payment_app_list_result,
|
||||
|
||||
@@ -312,6 +312,21 @@ pub struct GameDistributionDerivedGamesRecord {
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Fork 取件信息(受鉴权内容下发通道的元数据)。
|
||||
///
|
||||
/// `found == false` 表示游戏行不存在(api-server → 404);`available == false` 表示行存在但
|
||||
/// 不可作为改编来源(已软删除 / 未公开 / 没有当前公开版本 → 409)。**不含对象键**:
|
||||
/// 只带版本身份与摘要,下载路径由 api-server 拼同源相对路径。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GameDistributionForkSourceRecord {
|
||||
pub found: bool,
|
||||
pub available: bool,
|
||||
pub fork_authorization: String,
|
||||
pub version_id: Option<String>,
|
||||
pub package_sha256: Option<String>,
|
||||
pub package_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
fn map_game(
|
||||
value: crate::module_bindings::GameDistributionGameSnapshot,
|
||||
) -> GameDistributionGameRecord {
|
||||
@@ -478,6 +493,25 @@ pub(crate) fn map_game_distribution_derived_games_result(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fork 取件信息:`ok == false` 是服务端失败(走既有错误映射);`found` / `available` 原样透传,
|
||||
/// HTTP 语义(404 / 409 / 403)由 api-server 决定。
|
||||
pub(crate) fn map_game_distribution_fork_source_result(
|
||||
result: crate::module_bindings::GameDistributionForkSourceResult,
|
||||
) -> Result<GameDistributionForkSourceRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
Ok(GameDistributionForkSourceRecord {
|
||||
found: result.found,
|
||||
available: result.available,
|
||||
fork_authorization: result.fork_authorization,
|
||||
version_id: result.version_id,
|
||||
// SpacetimeDB 生成绑定把 `package_sha256` 折成 `package_sha_256`(与版本快照同约定)。
|
||||
package_sha256: result.package_sha_256,
|
||||
package_bytes: result.package_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_public_game(
|
||||
value: crate::module_bindings::GameDistributionPublicGameSnapshot,
|
||||
) -> GameDistributionPublicGameRecord {
|
||||
|
||||
@@ -673,6 +673,30 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 读取某作品的 Fork 取件信息(受鉴权内容下发通道的元数据)。
|
||||
///
|
||||
/// `found` / `available` 原样回传给 api-server,由它映射 404 / 409 / 403;这里不做 HTTP 语义。
|
||||
pub async fn get_game_distribution_fork_source(
|
||||
&self,
|
||||
game_id: String,
|
||||
) -> Result<GameDistributionForkSourceRecord, SpacetimeClientError> {
|
||||
let input = crate::module_bindings::GameDistributionForkSourceInput { game_id };
|
||||
self.call_after_connect(
|
||||
"get_game_distribution_fork_source",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.get_game_distribution_fork_source_and_return_then(input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_game_distribution_fork_source_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 批量累加已公开游戏的游玩次数;非公开游戏由事务静默跳过。
|
||||
pub async fn increment_game_distribution_game_play_counts(
|
||||
&self,
|
||||
|
||||
@@ -424,6 +424,8 @@ pub mod game_distribution_delete_game_input_type;
|
||||
pub mod game_distribution_derived_games_input_type;
|
||||
pub mod game_distribution_derived_games_result_type;
|
||||
pub mod game_distribution_fail_upload_input_type;
|
||||
pub mod game_distribution_fork_source_input_type;
|
||||
pub mod game_distribution_fork_source_result_type;
|
||||
pub mod game_distribution_game_list_procedure_result_type;
|
||||
pub mod game_distribution_game_procedure_result_type;
|
||||
pub mod game_distribution_game_snapshot_type;
|
||||
@@ -493,6 +495,7 @@ pub mod get_external_generation_job_result_and_return_procedure;
|
||||
pub mod get_external_generation_job_summary_and_return_procedure;
|
||||
pub mod get_external_generation_queue_stats_and_return_procedure;
|
||||
pub mod get_feature_gate_config_procedure;
|
||||
pub mod get_game_distribution_fork_source_and_return_procedure;
|
||||
pub mod get_game_distribution_game_and_return_procedure;
|
||||
pub mod get_game_distribution_lineage_and_return_procedure;
|
||||
pub mod get_game_distribution_my_review_and_return_procedure;
|
||||
@@ -1297,6 +1300,8 @@ pub use game_distribution_delete_game_input_type::GameDistributionDeleteGameInpu
|
||||
pub use game_distribution_derived_games_input_type::GameDistributionDerivedGamesInput;
|
||||
pub use game_distribution_derived_games_result_type::GameDistributionDerivedGamesResult;
|
||||
pub use game_distribution_fail_upload_input_type::GameDistributionFailUploadInput;
|
||||
pub use game_distribution_fork_source_input_type::GameDistributionForkSourceInput;
|
||||
pub use game_distribution_fork_source_result_type::GameDistributionForkSourceResult;
|
||||
pub use game_distribution_game_list_procedure_result_type::GameDistributionGameListProcedureResult;
|
||||
pub use game_distribution_game_procedure_result_type::GameDistributionGameProcedureResult;
|
||||
pub use game_distribution_game_snapshot_type::GameDistributionGameSnapshot;
|
||||
@@ -1366,6 +1371,7 @@ pub use get_external_generation_job_result_and_return_procedure::get_external_ge
|
||||
pub use get_external_generation_job_summary_and_return_procedure::get_external_generation_job_summary_and_return;
|
||||
pub use get_external_generation_queue_stats_and_return_procedure::get_external_generation_queue_stats_and_return;
|
||||
pub use get_feature_gate_config_procedure::get_feature_gate_config;
|
||||
pub use get_game_distribution_fork_source_and_return_procedure::get_game_distribution_fork_source_and_return;
|
||||
pub use get_game_distribution_game_and_return_procedure::get_game_distribution_game_and_return;
|
||||
pub use get_game_distribution_lineage_and_return_procedure::get_game_distribution_lineage_and_return;
|
||||
pub use get_game_distribution_my_review_and_return_procedure::get_game_distribution_my_review_and_return;
|
||||
|
||||
@@ -1188,6 +1188,12 @@ pub struct GameDistributionDerivedGamesInput {
|
||||
pub game_id: String,
|
||||
}
|
||||
|
||||
/// 读取某作品的 Fork 取件信息(受鉴权内容下发通道的元数据);只按作品 ID 查询。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct GameDistributionForkSourceInput {
|
||||
pub game_id: String,
|
||||
}
|
||||
|
||||
/// 单次游玩计数增量;api-server 已按 `game_id` 聚合,同一输入内不重复。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct GameDistributionPlayCountIncrement {
|
||||
@@ -1384,6 +1390,61 @@ pub struct GameDistributionDerivedGamesResult {
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Fork 取件的可判定状态。
|
||||
///
|
||||
/// 单独成类型而不是让 api-server 自己读游戏行:`get_game_distribution_game_for_owner_or_public_tx`
|
||||
/// 那条读路径把「已软删除」与「未公开」都折叠成 `None`,HTTP 层就没法区分 404 与 409。
|
||||
/// 这里把「行是否存在」「能否作为改编来源」「授权档位」三件事分开回传,映射留给 api-server。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct GameDistributionForkSourceSnapshot {
|
||||
/// 游戏行是否存在(含已软删除的行;`false` 时其余字段无意义)。
|
||||
pub found: bool,
|
||||
/// 是否可作为改编来源:`can_serve_as_fork_source`(未软删除 + 已公开)且存在当前公开版本。
|
||||
pub available: bool,
|
||||
/// 作品级共创授权档位原值;api-server 按「未知按禁止」解释。
|
||||
pub fork_authorization: String,
|
||||
/// 当前公开版本;不可用或没有公开版本时为 None。
|
||||
pub version_id: Option<String>,
|
||||
/// 当前公开版本的发行包摘要;取自版本行,不重新计算。
|
||||
pub package_sha256: Option<String>,
|
||||
pub package_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
/// Fork 取件读取结果。
|
||||
///
|
||||
/// 扁平列出全部字段而不是用 `Option<Snapshot>`:`ok == true` 却没有任何判定结果是不可能状态,
|
||||
/// 扁平结构让它无法表达。`found == false`(行不存在 → api-server 404)与
|
||||
/// `available == false`(行存在但不可作为来源 → 409)由 api-server 分别映射。
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct GameDistributionForkSourceResult {
|
||||
pub ok: bool,
|
||||
pub found: bool,
|
||||
pub available: bool,
|
||||
/// 作品级共创授权档位原值;api-server 按「未知按禁止」解释后映射 403。
|
||||
pub fork_authorization: String,
|
||||
/// 当前公开版本;不可用或没有公开版本时为 None。
|
||||
pub version_id: Option<String>,
|
||||
pub package_sha256: Option<String>,
|
||||
pub package_bytes: Option<u64>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl GameDistributionForkSourceResult {
|
||||
/// 参数非法等服务端失败:`ok = false`,api-server 走既有错误映射。
|
||||
fn failed(error: String) -> Self {
|
||||
Self {
|
||||
ok: false,
|
||||
found: false,
|
||||
available: false,
|
||||
fork_authorization: String::new(),
|
||||
version_id: None,
|
||||
package_sha256: None,
|
||||
package_bytes: None,
|
||||
error_message: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameDistributionLineageResult {
|
||||
/// 目标作品不存在或已软删除:`ok` 仍为 true,由 api-server 映射成 404。
|
||||
fn not_found() -> Self {
|
||||
@@ -2005,6 +2066,35 @@ pub fn list_game_distribution_derived_games_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取某作品的 Fork 取件信息;受信服务身份只读,api-server 侧另外要求 Bearer 登录。
|
||||
///
|
||||
/// 行不存在时 `found = false`(api-server → 404);行存在但不可作为来源(已软删除 / 未公开 /
|
||||
/// 没有当前公开版本)时 `found = true, available = false`(→ 409);授权为禁止或未知由
|
||||
/// api-server 按 `fork_authorization` 判成 403。响应只含版本身份与摘要,不含对象键。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_game_distribution_fork_source_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: GameDistributionForkSourceInput,
|
||||
) -> GameDistributionForkSourceResult {
|
||||
let caller = ctx.sender();
|
||||
match ctx.try_with_tx(|tx| {
|
||||
require_editor_generation_runtime_service_identity(tx, caller)?;
|
||||
get_game_distribution_fork_source_tx(tx, input.clone())
|
||||
}) {
|
||||
Ok(source) => GameDistributionForkSourceResult {
|
||||
ok: true,
|
||||
found: source.found,
|
||||
available: source.available,
|
||||
fork_authorization: source.fork_authorization,
|
||||
version_id: source.version_id,
|
||||
package_sha256: source.package_sha256,
|
||||
package_bytes: source.package_bytes,
|
||||
error_message: None,
|
||||
},
|
||||
Err(error) => GameDistributionForkSourceResult::failed(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员读取任意版本状态;admin 权限由 api-server 在调用前校验,procedure 只接受受信服务身份。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_game_distribution_version_and_return(
|
||||
@@ -4099,6 +4189,48 @@ fn game_distribution_derived_games_tx(
|
||||
}))
|
||||
}
|
||||
|
||||
/// 读取某作品的 Fork 取件信息(受鉴权内容下发通道的元数据)。
|
||||
///
|
||||
/// 与族谱 / 衍生列表不同,这里**必须**区分「行不存在」与「行存在但不可用」:HTTP 合同要求
|
||||
/// 前者 404、后者 409,所以不复用「折叠成 `None`」的写法,而是把三件事分开回传——行是否存在、
|
||||
/// 能否作为改编来源(`can_serve_as_fork_source` **且**存在当前公开版本)、授权档位原值。
|
||||
/// 授权是否生效、以及「未知档位按禁止解释」留给 api-server,与 procedure 侧创建血缘时同一口径。
|
||||
/// 只回传版本身份与摘要,不回传对象键。
|
||||
fn get_game_distribution_fork_source_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: GameDistributionForkSourceInput,
|
||||
) -> Result<GameDistributionForkSourceSnapshot, String> {
|
||||
let game_id = required_game_distribution_text(input.game_id, "game_id")?;
|
||||
let Some(game) = ctx.db.game_distribution_game().game_id().find(&game_id) else {
|
||||
return Ok(GameDistributionForkSourceSnapshot {
|
||||
found: false,
|
||||
available: false,
|
||||
fork_authorization: String::new(),
|
||||
version_id: None,
|
||||
package_sha256: None,
|
||||
package_bytes: None,
|
||||
});
|
||||
};
|
||||
// 取件必须有「当前公开版本」,否则没有可下发的内容。
|
||||
let version = public_game_distribution_version(ctx, &game);
|
||||
let available = module_game_distribution::can_serve_as_fork_source(
|
||||
game.deleted_at.is_some(),
|
||||
game.visibility == GAME_DISTRIBUTION_VISIBILITY_PUBLISHED,
|
||||
) && version.is_some();
|
||||
Ok(GameDistributionForkSourceSnapshot {
|
||||
found: true,
|
||||
available,
|
||||
fork_authorization: game.fork_authorization.clone(),
|
||||
version_id: version
|
||||
.as_ref()
|
||||
.map(|version| version.version_id.clone()),
|
||||
package_sha256: version
|
||||
.as_ref()
|
||||
.map(|version| version.package_sha256.clone()),
|
||||
package_bytes: version.map(|version| version.package_bytes),
|
||||
})
|
||||
}
|
||||
|
||||
/// 折叠族谱树的输入行:根作品自身 + 所有以该根为祖先的血缘行。
|
||||
///
|
||||
/// 只读行事实,不做任何可见性判断——「哪些节点能出现、身份字段要不要降级」全部由
|
||||
|
||||
Reference in New Issue
Block a user