放宽更新模式发布草稿的 gameId/versionId 配对校验
`isDraft` 不再要求两个服务端 id 同时存在或缺失:更新模式在请求前只会冻结 `gameId`,`versionId` 要等响应,丢弃该草稿会在重载后把冻结版本号换成「线上最近版本 +1」而重发成第二个版本 补充草稿读取与更新模式沿用冻结版本号的回归测试
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<GamePublishDraft>;
|
||||
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'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user