作品管理与Phaser4客户端发布闭环
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m21s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m18s
Project CI / Frontend tests (pull_request) Successful in 2m8s
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m56s
Project CI / Native shell tests (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m21s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m18s
Project CI / Frontend tests (pull_request) Successful in 2m8s
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m56s
Project CI / Native shell tests (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
补齐 Phaser4 客户端发布前校验、阶段进度与原生幂等账本 增加 Web 作者私有作品管理、资料编辑、版本历史与软删除 增加后台作品筛选、游标分页与删除治理 同步 SpacetimeDB schema、bindings、DTO、文档与定向测试
This commit is contained in:
@@ -6,6 +6,8 @@ import type {
|
||||
GameDistributionCreateVersionRequest,
|
||||
GameDistributionDeviceSupport,
|
||||
GameDistributionGame,
|
||||
GameDistributionRecoveryAction,
|
||||
GameDistributionUpdateGameMetadataRequest,
|
||||
GameDistributionVersionDetail,
|
||||
GameDistributionVersionStatus as GameDistributionVersionStatusValue,
|
||||
GameDistributionVersionSummary,
|
||||
@@ -119,8 +121,40 @@ export type GameDistributionVersionStatusEntry = {
|
||||
reviewReason?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/**
|
||||
* 版本私有的发行入口;只有已公开版本有值,作者详情用它判定当前是否能在线游玩。
|
||||
*/
|
||||
entryUrl?: string | null;
|
||||
/**
|
||||
* 服务端派生的下一步动作。作者列表投影目前不带这一项(只有版本回读带),
|
||||
* 缺失时前端只展示状态、不自行推断动作。
|
||||
*/
|
||||
recoveryAction?: GameDistributionRecoveryAction | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 作者自有游戏条目一定是「公开投影 + 私有版本状态」的形状;服务端缺字段时按空值兜底,
|
||||
* 避免页面对半成品响应直接崩掉。
|
||||
*/
|
||||
function normalizeOwnerGameEntry(
|
||||
value: unknown,
|
||||
): GameDistributionMyGame | null {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const entry = value as Partial<GameDistributionMyGame>;
|
||||
if (!entry.id) return null;
|
||||
const versions = Array.isArray(entry.versions)
|
||||
? entry.versions.filter(
|
||||
(item): item is GameDistributionVersionStatusEntry =>
|
||||
Boolean(item && typeof item === 'object' && item.versionId),
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
...(entry as GameDistributionMyGame),
|
||||
latestVersion: entry.latestVersion ?? null,
|
||||
versions,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listMyGames() {
|
||||
const payload = await requestJson<{ games?: unknown[] }>(
|
||||
'/my-games',
|
||||
@@ -128,9 +162,114 @@ export async function listMyGames() {
|
||||
'读取我的游戏失败',
|
||||
);
|
||||
const games = Array.isArray(payload?.games) ? payload.games : [];
|
||||
return games.filter((item): item is GameDistributionMyGame =>
|
||||
Boolean(item && typeof item === 'object' && (item as { id?: string }).id),
|
||||
return games
|
||||
.map(normalizeOwnerGameEntry)
|
||||
.filter((game): game is GameDistributionMyGame => game !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者读取自己名下单个作品:审核中 / 被驳回 / 已下架 / 已撤回的作品都能打开,
|
||||
* 公开详情只服务已公开投影(作者点自己的未公开作品会拿到 404)。
|
||||
*/
|
||||
export async function getMyGame(
|
||||
gameId: string,
|
||||
): Promise<GameDistributionMyGame | null> {
|
||||
const normalizedGameId = gameId.trim();
|
||||
if (!normalizedGameId) return null;
|
||||
const payload = await requestJson<{ game?: unknown }>(
|
||||
`/my-games/${encodeURIComponent(normalizedGameId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取作品详情失败',
|
||||
);
|
||||
return normalizeOwnerGameEntry(payload?.game);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存作品资料的成功响应:`game` 是公开投影形状(不含版本列表),
|
||||
* `replayed` 表示服务端按同一个 Idempotency-Key 重放了这次写入。
|
||||
*/
|
||||
export type GameDistributionUpdateMyGameResponse = {
|
||||
game: GameDistributionGame;
|
||||
replayed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存作品展示资料;包内容不在这里改动,仍随版本冻结。
|
||||
*
|
||||
* 服务端复用创建游戏的资料校验(含"必须有封面、截图必须是本人素材"),
|
||||
* 因此编辑也必须带上当前封面/截图的素材 ID,否则资料不完整或会把线上截图清空。
|
||||
*/
|
||||
export async function updateMyGame(
|
||||
gameId: string,
|
||||
payload: GameDistributionUpdateGameMetadataRequest,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedGameId = gameId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedGameId) throw new Error('缺少作品编号');
|
||||
if (!normalizedKey) throw new Error('保存作品资料缺少幂等键');
|
||||
return requestJson<GameDistributionUpdateMyGameResponse>(
|
||||
`/my-games/${encodeURIComponent(normalizedGameId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'保存作品资料失败',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除作品:成功后该作品从作者列表与公开投影里消失,历史版本包留在回收窗口内。
|
||||
*
|
||||
* 与其它写动作一致要带 `Idempotency-Key`;CAS 修订号走查询串(删除本身没有请求体),
|
||||
* 并发下架/审核后过期由服务端拒绝。
|
||||
*/
|
||||
export async function deleteMyGame(
|
||||
gameId: string,
|
||||
expectedPublicationRevision: number,
|
||||
idempotencyKey: string,
|
||||
) {
|
||||
const normalizedGameId = gameId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedGameId) throw new Error('缺少作品编号');
|
||||
if (!normalizedKey) throw new Error('删除作品缺少幂等键');
|
||||
const params = new URLSearchParams({
|
||||
expectedPublicationRevision: String(expectedPublicationRevision),
|
||||
});
|
||||
return requestJson<{ game?: GameDistributionGame; replayed?: boolean }>(
|
||||
`/my-games/${encodeURIComponent(normalizedGameId)}?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
},
|
||||
'删除作品失败',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 补读版本回读里的 `recoveryAction`。
|
||||
*
|
||||
* 作者列表投影不带这一步动作,只为「还需要作者动作」的最新版本各回读一次;单个版本
|
||||
* 回读失败只丢失该版本的下一步提示,不影响列表与其它作品。
|
||||
*/
|
||||
export async function loadVersionRecoveryActions(
|
||||
versionIds: readonly string[],
|
||||
): Promise<Record<string, GameDistributionRecoveryAction>> {
|
||||
const uniqueVersionIds = [
|
||||
...new Set(versionIds.map((versionId) => versionId.trim()).filter(Boolean)),
|
||||
];
|
||||
const details = await Promise.allSettled(
|
||||
uniqueVersionIds.map((versionId) => getGameVersion(versionId)),
|
||||
);
|
||||
const actions: Record<string, GameDistributionRecoveryAction> = {};
|
||||
uniqueVersionIds.forEach((versionId, index) => {
|
||||
const detail = details[index];
|
||||
if (detail?.status === 'fulfilled') {
|
||||
actions[versionId] = detail.value.version.recoveryAction;
|
||||
}
|
||||
});
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function unpublishGame(
|
||||
|
||||
Reference in New Issue
Block a user