diff --git a/src/components/game-distribution/GamePublishPage.test.tsx b/src/components/game-distribution/GamePublishPage.test.tsx index e519fbdcf..e5ea9632f 100644 --- a/src/components/game-distribution/GamePublishPage.test.tsx +++ b/src/components/game-distribution/GamePublishPage.test.tsx @@ -523,7 +523,13 @@ test('按统一发布、上传、送审顺序提交并展示审核中', async () 'shot-2.png', ]); expect(vi.mocked(uploadGamePackage).mock.calls[0]?.[0]).toBe('gamever-1'); + expect(vi.mocked(uploadGamePackage).mock.calls[0]?.[2]).toBe( + 'gamever-1:upload', + ); expect(vi.mocked(submitGameVersion).mock.calls[0]?.[1]).toBe(0); + expect(vi.mocked(submitGameVersion).mock.calls[0]?.[2]).toBe( + 'gamever-1:submit', + ); expect( await screen.findByText('已提交审核,审核通过后会自动出现在游戏广场。'), @@ -586,6 +592,10 @@ test('同一账号回到发布页可以沿用原版本继续送审', async () => await waitFor(() => expect(submitGameVersion).toHaveBeenCalledTimes(1)); expect(vi.mocked(submitGameVersion).mock.calls[0]?.[0]).toBe('gamever-1'); expect(vi.mocked(submitGameVersion).mock.calls[0]?.[1]).toBe(2); + // 恢复送审与提交模块共用同一套 versionId 派生键,刷新页面后重试也能被服务端去重。 + expect(vi.mocked(submitGameVersion).mock.calls[0]?.[2]).toBe( + 'gamever-1:submit', + ); expect( await screen.findByText('已提交审核,审核通过后会自动出现在游戏广场。'), ).toBeTruthy(); diff --git a/src/components/game-distribution/GamePublishPage.tsx b/src/components/game-distribution/GamePublishPage.tsx index fec5fa657..4031c0824 100644 --- a/src/components/game-distribution/GamePublishPage.tsx +++ b/src/components/game-distribution/GamePublishPage.tsx @@ -68,16 +68,6 @@ type GameUpdateContext = { title: string; }; -let publishKeySeed = 0; -function createPublishKey() { - publishKeySeed += 1; - const random = - typeof crypto !== 'undefined' && 'randomUUID' in crypto - ? crypto.randomUUID() - : `${Date.now()}-${publishKeySeed}`; - return `web-publish-${random}`; -} - /** * 网页发布编排:UI 只组输入与展示结果,定 key、调接口、写草稿、传包、送审都在提交模块内。 * 依赖在模块加载时注入一次,单测替换 `gameDistributionClient` 即可覆盖整条链路。 @@ -150,19 +140,14 @@ export function GamePublishPage({ const [error, setError] = useState(''); const [publishedGameId, setPublishedGameId] = useState(''); /** - * 同步的在途提交标记与恢复上传/送审用的稳定幂等键。 + * 同步的在途提交标记。 * * `isSubmitting` 是 React 状态,双击发生在同一次渲染窗口里时还没生效;再叠上 * `prepareGamePackage` 的压缩耗时,两次点击会各建一份游戏。这里用 ref 同步拦截。 - * 新建/更新发布的自然幂等由提交模块冻结的 `projectKey`/`versionNumber` 负责; - * 恢复已有版本继续上传时仍复用同一组 `Idempotency-Key`。 + * 幂等键全部由提交模块按不可变 `versionId` 派生(`{versionId}:upload` / `:submit`), + * 恢复路径沿用同一组,不再用每会话随机键,刷新页面后重试也能被服务端去重。 */ const submitInFlightRef = useRef(false); - const publishKeyRef = useRef(null); - const resolvePublishKey = () => { - if (!publishKeyRef.current) publishKeyRef.current = createPublishKey(); - return publishKeyRef.current; - }; const [draft, setDraft] = useState(null); const [draftDetail, setDraftDetail] = useState(null); @@ -390,28 +375,27 @@ export function GamePublishPage({ setIsSubmitting(true); setError(''); try { - const resumeKey = resolvePublishKey(); + const resumeVersionId = draftDetail.version.versionId; if (draftDetail.version.recoveryAction !== 'submit') { const prepared = await prepareGamePackage(packageFile as File); await uploadGamePackage( - draftDetail.version.versionId, + resumeVersionId, new Blob([prepared.bytes as unknown as BlobPart], { type: 'application/zip', }), - `${resumeKey}:upload`, + `${resumeVersionId}:upload`, ); } await submitGameVersion( - draftDetail.version.versionId, + resumeVersionId, draftDetail.game.publicationRevision, - `${resumeKey}:submit`, + `${resumeVersionId}:submit`, ); clearPublishDraft(); setDraft(null); setDraftDetail(null); setIsResumingDraft(false); setPublishedGameId(draftDetail.game.id); - publishKeyRef.current = null; } catch (resumeError: unknown) { setError( resumeError instanceof Error ? resumeError.message : '继续发布失败',