From 80e910dfe514f8a8ab71195da3abc69e39d32a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 7 Oct 2026 19:55:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E7=BD=91=E9=A1=B5=E5=8F=91=E5=B8=83):=20?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E8=B7=AF=E5=BE=84=E6=94=B9=E7=94=A8=20versio?= =?UTF-8?q?nId=20=E6=B4=BE=E7=94=9F=E5=B9=82=E7=AD=89=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleResumeSubmit 原先用每会话随机的 resolvePublishKey(),与提交模块的 `${versionId}:upload`/`:submit` 不一致,刷新页面后重试无法被服务端去重 - 统一成同一组 versionId 派生键,并删除不再使用的 createPublishKey / publishKeyRef / publishKeySeed - 补 GamePublishPage 断言恢复上传与送审确实发送 ${versionId}:upload / :submit --- .../GamePublishPage.test.tsx | 10 ++++++ .../game-distribution/GamePublishPage.tsx | 32 +++++-------------- 2 files changed, 18 insertions(+), 24 deletions(-) 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 : '继续发布失败',