From 3455b7a00f8bc03a480abd22ec53a62689d7eee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 24 Sep 2026 17:31:28 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E6=8F=90=E4=BA=A4=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B9=9F=E5=88=B7=E6=96=B0=E9=92=B1=E5=8C=85?= =?UTF-8?q?=E4=BD=99=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts:请求失败与「2xx 但没有任务状态」两条路径都调用 onWalletBalanceMayHaveChanged,与其它画布生成链路的 runEditorGenerationWithWalletRefresh 同一口径(超时或空 body 的 2xx 都可能已经扣费) - src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx:补两条失败路径各刷新一次钱包的断言 --- .../useModel3dGenerationTask.test.tsx | 31 ++++++++++++++++++- .../useModel3dGenerationTask.ts | 5 +++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx index ddd1b3065..c59097a23 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.test.tsx @@ -99,11 +99,13 @@ function renderTask({ projectId = 'project-1', assetFolderId, applyQueuedGeneration = vi.fn(async () => true), + onWalletBalanceMayHaveChanged = vi.fn(), }: { dialog: GenerateDialogState; projectId?: string | null; assetFolderId?: string | null; applyQueuedGeneration?: (...args: unknown[]) => Promise; + onWalletBalanceMayHaveChanged?: () => void; }) { const onQueuedGenerationTask = vi.fn(); const view = renderHook(() => { @@ -126,6 +128,7 @@ function renderTask({ currentDialog.placeholder ?? null, applyQueuedGeneration: applyQueuedGeneration as never, onQueuedGenerationTask, + onWalletBalanceMayHaveChanged, }); return { submitModel3dGeneration, @@ -133,7 +136,7 @@ function renderTask({ dialog: dialogs[0] as GenerateDialogState, }; }); - return { ...view, onQueuedGenerationTask }; + return { ...view, onQueuedGenerationTask, onWalletBalanceMayHaveChanged }; } describe('useModel3dGenerationTask', () => { @@ -293,6 +296,32 @@ describe('useModel3dGenerationTask', () => { expect(submitModel3dTextToModelRequestMock).not.toHaveBeenCalled(); }); + it('提交失败与「2xx 无任务状态」都按可能已扣费刷新钱包', async () => { + submitModel3dTextToModelRequestMock.mockRejectedValue( + new ApiClientError({ + message: '3D 模型生成失败,请稍后重试。', + status: 502, + code: 'tripo-upstream-error', + }), + ); + const failed = renderTask({ dialog: createDialog() }); + + await act(async () => { + await failed.result.current.submitModel3dGeneration(createDialog()); + }); + + expect(failed.onWalletBalanceMayHaveChanged).toHaveBeenCalledTimes(1); + + submitModel3dTextToModelRequestMock.mockResolvedValue(null); + const emptyBody = renderTask({ dialog: createDialog() }); + + await act(async () => { + await emptyBody.result.current.submitModel3dGeneration(createDialog()); + }); + + expect(emptyBody.onWalletBalanceMayHaveChanged).toHaveBeenCalledTimes(1); + }); + it('定价段缺失时不发请求,只把不可提交的原因写回面板', async () => { applyEditorGenerationPricingConfig({ models: EDITOR_MODEL_MUD_POINT_CONFIG, diff --git a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts index 1a8c19925..00349e2e9 100644 --- a/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts +++ b/src/components/image-editor/model3d-generation/useModel3dGenerationTask.ts @@ -127,6 +127,8 @@ export function useModel3dGenerationTask({ if (!queueState) { // 2xx 但没有任务状态(空 body 或缺 status 的 payload)不能当成功静默复位: // 用户既看不到任务也没有任何提示,服务端细节被丢掉。 + // 服务端在这条路上也可能已经扣过费,钱包余额同样要按「可能变了」刷新。 + onWalletBalanceMayHaveChanged?.(); updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ ...currentDialog, status: 'failed', @@ -155,6 +157,9 @@ export function useModel3dGenerationTask({ errorMessage: undefined, })); } catch (error) { + // 超时等失败也可能意味着服务端已经扣费(与其它生成链路的 + // runEditorGenerationWithWalletRefresh 同一口径),失败路径也要刷新钱包。 + onWalletBalanceMayHaveChanged?.(); updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({ ...currentDialog, status: 'failed',