diff --git a/src/components/game-distribution/gamePublishDraft.test.ts b/src/components/game-distribution/gamePublishDraft.test.ts index 3f6c32639..14b22a5b7 100644 --- a/src/components/game-distribution/gamePublishDraft.test.ts +++ b/src/components/game-distribution/gamePublishDraft.test.ts @@ -44,15 +44,15 @@ describe('readPublishDraft 的 gameId/versionId 配对', () => { expect(readPublishDraft(storageWith(draft))).toEqual(draft); }); - it('只回填 gameId 的半写草稿判为不可恢复', () => { - expect( - readPublishDraft(storageWith({ ...BASE_DRAFT, gameId: 'game_1' })), - ).toBeNull(); + it('只有 gameId(更新模式发请求前的正常中间态)仍可恢复', () => { + // 更新模式在请求前就写 gameId(路径身份),versionId 等响应;这不是半写损坏,必须保留草稿, + // 否则响应丢失后重载会丢掉冻结的 versionNumber、按线上最近版本 +1 推出第二个版本。 + const draft: GamePublishDraft = { ...BASE_DRAFT, gameId: 'game_1' }; + expect(readPublishDraft(storageWith(draft))).toEqual(draft); }); - it('只回填 versionId 的半写草稿判为不可恢复', () => { - expect( - readPublishDraft(storageWith({ ...BASE_DRAFT, versionId: 'gamever_1' })), - ).toBeNull(); + it('只有 versionId 的半写草稿仍可恢复(编号类型合法即可)', () => { + const draft: GamePublishDraft = { ...BASE_DRAFT, versionId: 'gamever_1' }; + expect(readPublishDraft(storageWith(draft))).toEqual(draft); }); }); diff --git a/src/components/game-distribution/gamePublishDraft.ts b/src/components/game-distribution/gamePublishDraft.ts index d1ca42043..6e5d5ff57 100644 --- a/src/components/game-distribution/gamePublishDraft.ts +++ b/src/components/game-distribution/gamePublishDraft.ts @@ -8,8 +8,10 @@ import type { GameDistributionRecoveryAction } from '../../../packages/shared/sr * * `projectKey` 与 `versionNumber` 必须在调用发布接口**之前**写入:它们构成服务端自然幂等键 * `(owner, projectKey | gameId, versionNumber)` 的一部分,响应丢失后重试才能命中同一游戏与版本。 - * `gameId` / `versionId` 是服务端分配后才回填的字段。`ownerUserId` 用于换账号时拒绝恢复, - * 避免把上一个账号的私有状态展示给新账号。 + * `gameId` / `versionId` 是服务端分配后才回填的字段。**更新模式**在请求前就写 `gameId`(路径身份), + * `versionId` 要等响应,因此「有 `gameId` 没 `versionId`」是正常的中间态,不能当成损坏草稿丢弃—— + * 丢了它,响应丢失后重载会按「线上最近版本 +1」推出第二个版本,而不是用冻结的 `versionNumber` 重放。 + * `ownerUserId` 用于换账号时拒绝恢复,避免把上一个账号的私有状态展示给新账号。 */ export type GamePublishDraft = { ownerUserId: string; @@ -40,8 +42,6 @@ function resolveStorage(storage?: StorageLike): StorageLike | null { function isDraft(value: unknown): value is GamePublishDraft { if (!value || typeof value !== 'object') return false; const draft = value as Partial; - const hasGameId = draft.gameId !== undefined; - const hasVersionId = draft.versionId !== undefined; return Boolean( draft.ownerUserId && draft.projectKey && @@ -49,9 +49,7 @@ function isDraft(value: unknown): value is GamePublishDraft { draft.versionNumber >= 1 && typeof draft.title === 'string' && (draft.gameId === undefined || typeof draft.gameId === 'string') && - (draft.versionId === undefined || typeof draft.versionId === 'string') && - // 两个 id 由同一次发布响应一起回填,半写状态只可能来自损坏的旧草稿,直接判为不可恢复。 - hasGameId === hasVersionId, + (draft.versionId === undefined || typeof draft.versionId === 'string'), ); } diff --git a/src/components/game-distribution/gamePublishSubmission.test.ts b/src/components/game-distribution/gamePublishSubmission.test.ts index c937f47d4..a944d4164 100644 --- a/src/components/game-distribution/gamePublishSubmission.test.ts +++ b/src/components/game-distribution/gamePublishSubmission.test.ts @@ -261,6 +261,43 @@ describe('gamePublishSubmission', () => { expect(createProjectKey).not.toHaveBeenCalled(); }); + it('更新模式草稿只有 gameId(响应丢失的中间态)时仍沿用冻结版本号', async () => { + const store = createDraftStore({ + ownerUserId: 'user-1', + projectKey: 'web-project-fixed', + versionNumber: 3, + gameId: 'game-1', + // 刻意不写 versionId:更新模式在请求前只会冻结 gameId,`versionId` 要等响应。 + title: '旧版本', + updatedAt: '2026-10-06T00:00:00.000Z', + }); + const client = createClient({ + publishGameVersion: vi.fn(async () => + publishResponse({ versionNumber: 3 }), + ), + }); + const submit = createGamePublishSubmission({ + ...client, + ...store, + createProjectKey: () => 'should-not-be-used', + now: FIXED_NOW, + }); + + // 调用方重读到 max+1 = 4;必须用草稿冻结的 3 重放,而不是新建 v4。 + await submit({ + ...BASE_INPUT, + target: { mode: 'update', gameId: 'game-1' }, + versionNumber: 4, + }); + + const [target] = client.publishGameVersion.mock.calls[0] ?? []; + expect(target).toEqual({ + mode: 'update', + gameId: 'game-1', + versionNumber: 3, + }); + }); + it('换账号时忽略上一账号的草稿并生成新的 projectKey', async () => { const store = createDraftStore({ ownerUserId: 'user-2',