From df1071848f73988de419f963d47f628f2f7ee4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 6 Aug 2026 15:13:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AD=E6=AD=A2=E8=B7=A8=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=85=85=E5=80=BC=E7=A1=AE=E8=AE=A4=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为充值订单绑定 owner、账号 revision 与同一 AbortController。 在延迟确认、确认请求和 SSE watch 前后校验生命周期并透传 signal。 账号切换和卸载先中止旧链,再清理订单状态与支付回调。 补充 fake timer 回归并记录并发异步生命周期约束。 --- docs/project-memory/shared-memory/pitfalls.md | 12 + ...mProfileCenterController.recharge.test.tsx | 56 +++++ .../usePlatformProfileCenterController.ts | 219 +++++++++++++++--- 3 files changed, 257 insertions(+), 30 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index be3961396..dbf7a6230 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -61,6 +61,7 @@ - 处理:旧 action 继续禁止重放或伪造 observation;旧 child 与父 Run 先真实终态。新 Supervisor continuation 仅扫描同 Session、同 source、同有效任务合同的历史根 Run,并要求对应 ready-task 同时存在 `failed / needs-reconciliation` 记录、最终 `cancelled` 记录和 durable cancel tombstone,才把当前 manifest 的同一 failed 节点恢复为 pending,让 scheduler 创建新 child Run。manifest 的读取、筛选、child 证据重验和写回放在同一项目写锁内;每个 task journal 只读取一次并按 parent Run 建索引。较新的无 child Run 默认阻断旧凭证,只有其 root journal 精确证明为旧 failed Graph 在进入 scheduler 前即失败时才允许向前查找;scheduler 自身失败不得被当成该兼容场景。 - 验证:构造 reconciliation child、人工 cancel tombstone、failed manifest 和终态父 Run,证明同源 continuation 只重排该节点;并列普通 failed 节点保持 failed,完成合同继续继承原任务 SHA 与项目 baseline,旧 pending action 不恢复。追加覆盖“旧 failed Graph 未调度”的中间 Run 可以跨过,而较新的 scheduler failure 即使没有 child journal 也会阻断更老 tombstone。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs`。 + ## `timeout_at` 不能替代显式的预算耗尽预检 - 现象:给完美像素加端点级并发闸后,预算已经耗尽的请求仍然能拿到许可,白占一个名额继续去打几轮全账号 SpacetimeDB 扫描,直到下载那步才失败。 @@ -76,6 +77,7 @@ - 处理:把递增封进一个 guard 结构体,递减放在它的 `Drop` 实现里;递增本身用 `fetch_update` 的 CAS,不能用「先读后加」——两个线程同时读到 `max - 1` 各自加一就会越界。拿到资源后立即 `drop(guard)` 让出队列名额,不要让它跟着许可一起活到请求结束。 - 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。 - 关联:`server-rs/crates/api-server/src/editor_project.rs`(`try_enter_bounded_queue`、`EditorPixelArtSnapQueueGuard`)。 + ## Linux 生产脚本门禁不能假设本地也是 GNU userland - 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。 @@ -4021,6 +4023,7 @@ - 处理:灰度页只能以 `/admin/api/feature-gates` 为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧 `creation-entry:*` 目标、接口和页面保持退役。 - 验证:`adminRoutes` 必须包含 `gray-release`,admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。 - 关联:`apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx`、`apps/admin-web/src/app/adminRoutes.ts`、`server-rs/crates/api-server/src/modules/admin.rs`、`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。 + ## 历史钱包消费不能从最近流水或通用订单快照推算 - 现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的 `list_profile_wallet_ledger`,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。 @@ -4143,6 +4146,7 @@ - 处理:从当前 root source 的 seed lane 动态解析全部零依赖首波任务,只对这些 child 容忍 hydration `Pending`,后续 code prototype / preview 仍严格要求 Running/Completed。`streaming / ready` 仍要求当前 revision,`committed` 回复改为依据 finalization 的稳定身份查询,不随后续项目 revision 失效。 - 验证:覆盖 `design-director / art-director / code-director` 三个 Pending 首波 child 均可投影 Completed、`code-prototype` Pending 仍被拒绝;非流式专业 Agent 在 finalization 前无 stream,提交后形成 committed stream,再推进项目 revision 后仍可查询且正文不变。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/response_stream.rs`。 + ## 异步生成结果未知时不能换幂等键重提(2026-07-31) - 现象:生成提交发生客户端超时、连接中断或响应丢失后,调用方创建新的 `Idempotency-Key` 再提交一次;原任务其实已经入队,最终造成重复生成、重复扣费和重复画布 / 素材库写入。 @@ -4187,6 +4191,13 @@ - 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配 `AbortController`,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有 `then / catch / finally` 在更新状态前都要检查 signal 与 revision。 - 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。 +## 账号级轮询和并发 bootstrap 必须中止整条旧生命周期(2026-08-06) + +- 现象:任务列表 `Promise.all` 一侧失败后,另一侧请求可能跨过重试和卸载继续悬挂;微信充值第一次确认返回 pending 后切换账号,旧订单的延迟重试可能使用新账号 Token 再次请求,401 路径还会影响新账号登录态。 +- 原因:只用 React state 或最终回调里的 owner 判断,无法阻止已安排的 timer、下一次 HTTP 请求和同轮未完成分支继续执行;每轮重试覆盖单个 controller ref,也会遗失更早的悬挂请求。 +- 处理:并发 bootstrap 每次 attempt 使用独立 `AbortController`,任一分支失败时先中止同轮 controller 再安排有界重试,卸载时中止当前 attempt。充值订单从创建成功起持有同一个 owner、账号 revision 和 `AbortController`;每次 delay、confirm 和 SSE watch 前后都校验生命周期,并把同一 signal 传到请求层;账号切换和卸载先 abort,再清理 ref、state 与旧支付回调 hash。 +- 验证:bootstrap 用例覆盖“一侧 reject、另一侧 pending、重试后卸载”,并断言每轮 signal 都已中止;充值用 fake timer 证明首次确认 pending 后切换账号会中止 signal,推进全部退避时间也不会产生第二个确认请求或清理新账号 Token。 + ## 下游 manifest 回调测试不能冒充实时数据源(2026-08-05) - 现象:工作台的资源、任务与版本重投影单测保持绿色,但后台 Agent 已更新 `.agent/manifest.json` 后,打开中的工作台仍长期显示旧快照,只有重开项目才更新。 @@ -4247,6 +4258,7 @@ - 现象:parent wake 的 200 次瞬态预算耗尽后 Runtime 仍长期显示 running,或 lane 忙、取消、child 前进、manifest 损坏时 reconciliation 被静默丢弃或覆盖新状态。 - 处理:预算耗尽错误必须向上传递;lane 忙先持久化 deferred signal,再在 lane + 项目锁内重检最新事实。结构损坏路径使用不依赖 manifest hydration 的专用 journal/state 写入,CAS 失败转为继续对账,绝不覆写并发取消或 DAG 进展。可解析的空对象/空 runId 仍是损坏身份,只有完整有效的新 Run 才能阻止旧 marker;event/audit 的同键记录必须完整比对并拒绝冲突或重复。旧 task 已终态、Runtime 非 waiting 或新 Run 接管时,deferred signal 必须写 resolved/superseded,不能留给后续 wake 永久重复 settle。 - 测试注意:autonomous child fixture 先 linked Pending、后正式 Running;终态 runId 必须拒绝复用。判断 Completed-only 诊断时按每个 seed task 的实际状态分析,不能因为 `code-prototype` Pending 就忽略已经 Completed 的 `art-asset-plan` 深验。 + ## macOS 安全路径测试必须使用规范化临时目录(2026-08-05) - 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发 `WouldBlock`。 diff --git a/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx b/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx index edc2708c6..ab759f3a3 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx +++ b/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx @@ -178,6 +178,62 @@ describe('usePlatformProfileCenterController recharge fallback', () => { }); }); + test('aborts delayed confirmation retries when the account changes', async () => { + vi.useFakeTimers(); + try { + profileClientMocks.createPlatformProfileRechargeOrder.mockResolvedValue( + nativeRechargeResponse(), + ); + profileClientMocks.confirmWechatPlatformProfileRechargeOrder.mockResolvedValue( + { + order: nativeRechargeResponse().order, + center: nativeRechargeResponse().center, + }, + ); + usePlatformWalletStore.getState().setWalletOwner('user-a'); + const { result, rerender } = renderController(userA); + + await act(async () => { + result.current.buyRechargeProduct(rechargeProduct); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(result.current.nativeWechatPayment?.orderId).toBe('order-a'); + + await act(async () => { + result.current.confirmNativeWechatPayment(); + await Promise.resolve(); + await Promise.resolve(); + }); + expect( + profileClientMocks.confirmWechatPlatformProfileRechargeOrder, + ).toHaveBeenCalledTimes(1); + const confirmationSignal = profileClientMocks + .confirmWechatPlatformProfileRechargeOrder.mock.calls[0]?.[1] + ?.signal as AbortSignal; + expect(confirmationSignal.aborted).toBe(false); + expect( + profileClientMocks.watchWechatPlatformProfileRechargeOrder, + ).toHaveBeenCalledWith('order-a', { signal: confirmationSignal }); + + act(() => { + rerender({ user: userB }); + usePlatformWalletStore.getState().setWalletOwner('user-b'); + }); + expect(confirmationSignal.aborted).toBe(true); + + await act(async () => { + await vi.advanceTimersByTimeAsync(5000); + }); + expect( + profileClientMocks.confirmWechatPlatformProfileRechargeOrder, + ).toHaveBeenCalledTimes(1); + expect(apiClientMocks.clearStoredAccessToken).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + test('does not clear the new owner token from an old owner payment error', async () => { const pendingOrder = deferred(); paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue( diff --git a/src/components/platform-entry/usePlatformProfileCenterController.ts b/src/components/platform-entry/usePlatformProfileCenterController.ts index 517a3e0c0..5076c5712 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.ts +++ b/src/components/platform-entry/usePlatformProfileCenterController.ts @@ -84,6 +84,12 @@ type AccountLifecycle = Readonly<{ type WechatRechargeOrderLifecycle = Readonly<{ orderId: string; account: AccountLifecycle; + abortController: AbortController; +}>; + +type WechatRechargeConfirmationOptions = Readonly<{ + signal: AbortSignal; + isCurrent: () => boolean; }>; function isWechatJsapiMissingIdentityError(error: unknown) { @@ -182,9 +188,36 @@ function readWechatPayResultFromHash(): WechatPayResult | null { }; } -function waitWechatPayConfirmDelay(delayMs: number) { - return new Promise((resolve) => { - window.setTimeout(resolve, delayMs); +function createWechatRechargeAbortError(signal: AbortSignal) { + return signal.reason instanceof Error + ? signal.reason + : new DOMException('Wechat recharge confirmation aborted', 'AbortError'); +} + +function assertWechatRechargeConfirmationActive( + options: WechatRechargeConfirmationOptions, +) { + if (options.signal.aborted || !options.isCurrent()) { + throw createWechatRechargeAbortError(options.signal); + } +} + +function waitWechatPayConfirmDelay(delayMs: number, signal?: AbortSignal) { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(createWechatRechargeAbortError(signal)); + return; + } + const timerId = window.setTimeout(() => { + signal?.removeEventListener('abort', handleAbort); + resolve(); + }, delayMs); + const handleAbort = () => { + window.clearTimeout(timerId); + signal?.removeEventListener('abort', handleAbort); + reject(createWechatRechargeAbortError(signal!)); + }; + signal?.addEventListener('abort', handleAbort, { once: true }); }); } @@ -204,42 +237,71 @@ function isWechatRechargeOrderTerminalForConfirmation( async function confirmWechatRechargeOrderUntilSettled( orderId: string, + options: WechatRechargeConfirmationOptions, ): Promise { - let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId); + assertWechatRechargeConfirmationActive(options); + let latestResponse = await confirmWechatPlatformProfileRechargeOrder( + orderId, + { signal: options.signal }, + ); + assertWechatRechargeConfirmationActive(options); if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) { return latestResponse; } for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) { - await waitWechatPayConfirmDelay(delayMs); + assertWechatRechargeConfirmationActive(options); + await waitWechatPayConfirmDelay(delayMs, options.signal); + assertWechatRechargeConfirmationActive(options); - latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId); + latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId, { + signal: options.signal, + }); + assertWechatRechargeConfirmationActive(options); if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) { return latestResponse; } } try { - const streamedResponse = - await watchWechatPlatformProfileRechargeOrder(orderId); + assertWechatRechargeConfirmationActive(options); + const streamedResponse = await watchWechatPlatformProfileRechargeOrder( + orderId, + { + signal: options.signal, + }, + ); + assertWechatRechargeConfirmationActive(options); return streamedResponse; } catch { + assertWechatRechargeConfirmationActive(options); return latestResponse; } } async function confirmWechatRechargeOrderQuickly( orderId: string, + options: WechatRechargeConfirmationOptions, ): Promise { - let latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId); + assertWechatRechargeConfirmationActive(options); + let latestResponse = await confirmWechatPlatformProfileRechargeOrder( + orderId, + { signal: options.signal }, + ); + assertWechatRechargeConfirmationActive(options); if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) { return latestResponse; } for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) { - await waitWechatPayConfirmDelay(delayMs); + assertWechatRechargeConfirmationActive(options); + await waitWechatPayConfirmDelay(delayMs, options.signal); + assertWechatRechargeConfirmationActive(options); - latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId); + latestResponse = await confirmWechatPlatformProfileRechargeOrder(orderId, { + signal: options.signal, + }); + assertWechatRechargeConfirmationActive(options); if (isWechatRechargeOrderTerminalForConfirmation(latestResponse.order)) { return latestResponse; } @@ -404,6 +466,38 @@ export function usePlatformProfileCenterController({ }, [], ); + const abortRechargeOrders = useCallback( + (...orders: Array) => { + const controllers = new Set( + orders.flatMap((order) => (order ? [order.abortController] : [])), + ); + controllers.forEach((controller) => controller.abort()); + }, + [], + ); + const createRechargeOrderLifecycle = useCallback( + ( + orderId: string, + account: AccountLifecycle, + ): WechatRechargeOrderLifecycle => ({ + orderId, + account, + abortController: new AbortController(), + }), + [], + ); + const createRechargeConfirmationOptions = useCallback( + ( + order: WechatRechargeOrderLifecycle, + ): WechatRechargeConfirmationOptions => ({ + signal: order.abortController.signal, + isCurrent: () => + isRechargeOrderCurrent(order) && + (isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order) || + isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order)), + }), + [isRechargeOrderCurrent, isSameRechargeOrder], + ); const walletOwnerUserId = usePlatformWalletStore( (state) => state.ownerUserId, ); @@ -429,6 +523,15 @@ export function usePlatformProfileCenterController({ rechargeCenterReadRevisionRef.current += 1; accountLifecycleRevisionRef.current += 1; walletLedgerReadRevisionRef.current += 1; + const pendingOrder = pendingWechatRechargeOrderRef.current; + const confirmingOrder = confirmingWechatRechargeOrderRef.current; + abortRechargeOrders(pendingOrder, confirmingOrder); + if ( + (pendingOrder && pendingOrder.account.ownerUserId !== currentUserId) || + (confirmingOrder && confirmingOrder.account.ownerUserId !== currentUserId) + ) { + clearWechatPayResultHash(); + } pendingWechatRechargeOrderRef.current = null; confirmingWechatRechargeOrderRef.current = null; setRechargeCenter(null); @@ -448,7 +551,7 @@ export function usePlatformProfileCenterController({ setRewardCodeError(null); setRewardCodeSuccess(null); setIsSubmittingReferralRedeem(false); - }, [currentUserId]); + }, [abortRechargeOrders, currentUserId]); const rechargeCenterReadAbortControllerRef = useRef( null, ); @@ -457,10 +560,16 @@ export function usePlatformProfileCenterController({ return () => { rechargeCenterReadRevisionRef.current += 1; accountLifecycleRevisionRef.current += 1; + abortRechargeOrders( + pendingWechatRechargeOrderRef.current, + confirmingWechatRechargeOrderRef.current, + ); + pendingWechatRechargeOrderRef.current = null; + confirmingWechatRechargeOrderRef.current = null; rechargeCenterReadAbortControllerRef.current?.abort(); rechargeCenterReadAbortControllerRef.current = null; }; - }, []); + }, [abortRechargeOrders]); // 中文注释:支持带邀请码 query 的直达场景,登录成功后自动打开兑换面板并复用同一套输入状态。 useEffect(() => { @@ -620,12 +729,16 @@ export function usePlatformProfileCenterController({ } loadRechargeCenter(); setSubmittingRechargeProductId(null); + abortRechargeOrders( + pendingWechatRechargeOrderRef.current, + confirmingWechatRechargeOrderRef.current, + ); pendingWechatRechargeOrderRef.current = null; confirmingWechatRechargeOrderRef.current = null; setWechatRechargeOrderConfirmationState(null); setNativeWechatPayment(null); }, - [isAccountLifecycleCurrent, loadRechargeCenter], + [abortRechargeOrders, isAccountLifecycleCurrent, loadRechargeCenter], ); const handleWechatPayResult = useCallback(() => { @@ -657,7 +770,8 @@ export function usePlatformProfileCenterController({ clearWechatPayResultHash(); return true; } - const order = pendingOrder ?? { orderId, account }; + const order = + pendingOrder ?? createRechargeOrderLifecycle(orderId, account); if ( isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) ) { @@ -668,7 +782,10 @@ export function usePlatformProfileCenterController({ setWechatRechargeOrderConfirmationState({ orderId }); setSubmittingRechargeProductId(null); setRechargePaymentResult(null); - void confirmWechatRechargeOrderUntilSettled(orderId) + void confirmWechatRechargeOrderUntilSettled( + orderId, + createRechargeConfirmationOptions(order), + ) .then((response) => { if ( !isRechargeOrderCurrent(order) || @@ -690,6 +807,7 @@ export function usePlatformProfileCenterController({ pendingWechatRechargeOrderRef.current = null; } confirmingWechatRechargeOrderRef.current = null; + order.abortController.abort(); setWechatRechargeOrderConfirmationState(null); setRechargePaymentResult(result); if (isPaid) { @@ -742,6 +860,8 @@ export function usePlatformProfileCenterController({ }, [ applyRechargeCenter, captureAccountLifecycle, + createRechargeConfirmationOptions, + createRechargeOrderLifecycle, isAccountLifecycleCurrent, isRechargeOrderCurrent, isSameRechargeOrder, @@ -770,7 +890,10 @@ export function usePlatformProfileCenterController({ confirmingWechatRechargeOrderRef.current = order; setWechatRechargeOrderConfirmationState({ orderId: order.orderId }); setRechargePaymentResult(null); - void confirmWechatRechargeOrderUntilSettled(order.orderId) + void confirmWechatRechargeOrderUntilSettled( + order.orderId, + createRechargeConfirmationOptions(order), + ) .then((response) => { if ( !isRechargeOrderCurrent(order) || @@ -785,6 +908,7 @@ export function usePlatformProfileCenterController({ } pendingWechatRechargeOrderRef.current = null; confirmingWechatRechargeOrderRef.current = null; + order.abortController.abort(); setWechatRechargeOrderConfirmationState(null); setSubmittingRechargeProductId(null); setRechargePaymentResult(result); @@ -810,6 +934,7 @@ export function usePlatformProfileCenterController({ return true; }, [ applyRechargeCenter, + createRechargeConfirmationOptions, isRechargeOrderCurrent, isSameRechargeOrder, nativeWechatPayment, @@ -852,6 +977,7 @@ export function usePlatformProfileCenterController({ pendingOrder?.orderId === current.orderId && isRechargeOrderCurrent(pendingOrder) ) { + pendingOrder.abortController.abort(); pendingWechatRechargeOrderRef.current = null; } return null; @@ -885,12 +1011,17 @@ export function usePlatformProfileCenterController({ if (!isAccountLifecycleCurrent(account)) { return; } - const order: WechatRechargeOrderLifecycle = { - orderId: response.order.orderId, + const order = createRechargeOrderLifecycle( + response.order.orderId, account, - }; + ); + abortRechargeOrders( + pendingWechatRechargeOrderRef.current, + confirmingWechatRechargeOrderRef.current, + ); createdOrder = order; pendingWechatRechargeOrderRef.current = order; + confirmingWechatRechargeOrderRef.current = null; if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) { if ( @@ -944,7 +1075,10 @@ export function usePlatformProfileCenterController({ title: '支付处理中', message: '正在查询微信支付到账状态。', }); - void confirmWechatRechargeOrderUntilSettled(response.order.orderId) + void confirmWechatRechargeOrderUntilSettled( + response.order.orderId, + createRechargeConfirmationOptions(order), + ) .then((confirmResponse) => { if (!isRechargeOrderCurrent(order)) { return; @@ -968,6 +1102,7 @@ export function usePlatformProfileCenterController({ ) { pendingWechatRechargeOrderRef.current = null; } + order.abortController.abort(); } if (isPaid) { void onRechargeSuccess?.(); @@ -1056,6 +1191,7 @@ export function usePlatformProfileCenterController({ createdOrder, ) ) { + createdOrder.abortController.abort(); pendingWechatRechargeOrderRef.current = null; } setNativeWechatPayment(null); @@ -1114,8 +1250,11 @@ export function usePlatformProfileCenterController({ }, [ applyRechargeCenter, + abortRechargeOrders, captureAccountLifecycle, captureWalletBalanceSnapshot, + createRechargeConfirmationOptions, + createRechargeOrderLifecycle, isAccountLifecycleCurrent, isRechargeOrderCurrent, isSameRechargeOrder, @@ -1141,7 +1280,10 @@ export function usePlatformProfileCenterController({ ? { ...current, isConfirming: true, confirmMessage: undefined } : current, ); - void confirmWechatRechargeOrderQuickly(nativeWechatPayment.orderId) + void confirmWechatRechargeOrderQuickly( + nativeWechatPayment.orderId, + createRechargeConfirmationOptions(order), + ) .then((response) => { if (!isRechargeOrderCurrent(order)) { return; @@ -1158,6 +1300,7 @@ export function usePlatformProfileCenterController({ ) { pendingWechatRechargeOrderRef.current = null; } + order.abortController.abort(); setRechargePaymentResult(result); if (isPaid) { void onRechargeSuccess?.(); @@ -1195,6 +1338,7 @@ export function usePlatformProfileCenterController({ }); }, [ applyRechargeCenter, + createRechargeConfirmationOptions, isRechargeOrderCurrent, isSameRechargeOrder, nativeWechatPayment, @@ -1215,20 +1359,22 @@ export function usePlatformProfileCenterController({ } let cancelled = false; - const abortController = new AbortController(); + const confirmationOptions = createRechargeConfirmationOptions(order); const watchUntilSettled = async () => { while ( !cancelled && Date.now() < expiresAtMs && - isRechargeOrderCurrent(order) + confirmationOptions.isCurrent() ) { try { + assertWechatRechargeConfirmationActive(confirmationOptions); const response = await watchWechatPlatformProfileRechargeOrder( orderId, { - signal: abortController.signal, + signal: confirmationOptions.signal, }, ); + assertWechatRechargeConfirmationActive(confirmationOptions); if ( cancelled || !response || @@ -1242,7 +1388,11 @@ export function usePlatformProfileCenterController({ return; } if (result.kind === 'pending') { - await waitWechatPayConfirmDelay(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS); + await waitWechatPayConfirmDelay( + WECHAT_NATIVE_WATCH_RETRY_DELAY_MS, + confirmationOptions.signal, + ); + assertWechatRechargeConfirmationActive(confirmationOptions); continue; } @@ -1252,6 +1402,7 @@ export function usePlatformProfileCenterController({ ) { confirmingWechatRechargeOrderRef.current = null; } + order.abortController.abort(); setNativeWechatPayment((current) => current?.orderId === orderId ? null : current, ); @@ -1264,26 +1415,34 @@ export function usePlatformProfileCenterController({ } catch { if ( cancelled || - abortController.signal.aborted || - !isRechargeOrderCurrent(order) + confirmationOptions.signal.aborted || + !confirmationOptions.isCurrent() ) { return; } } - await waitWechatPayConfirmDelay(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS); + try { + await waitWechatPayConfirmDelay( + WECHAT_NATIVE_WATCH_RETRY_DELAY_MS, + confirmationOptions.signal, + ); + assertWechatRechargeConfirmationActive(confirmationOptions); + } catch { + return; + } } }; void watchUntilSettled(); return () => { cancelled = true; - abortController.abort(); }; }, [ nativeWechatPayment?.expiresAt, nativeWechatPayment?.orderId, applyRechargeCenter, + createRechargeConfirmationOptions, isRechargeOrderCurrent, isSameRechargeOrder, onRechargeSuccess,