diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index a765d685e..567cd954f 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -65,7 +65,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当 4. 首充加赠资格按泥点商品档位独立计算。用户买过 `points_180` 后,只影响 `points_180` 的首充展示和结算,其它未购买档位仍保留各自首充加赠资格。 5. 前端不得用 `hasPointsRecharged` 统一隐藏所有泥点档位首充权益;该字段只表示账号是否发生过任一泥点充值。 6. 充值支付渠道只允许由设备平台隔离层解析为 `wechat_mp`、`wechat_mp_virtual`、`wechat_jsapi`、`wechat_h5` 或 `wechat_native`;生产真实支付不得默认落到 `mock`,缺失或未知 `paymentChannel` 必须拒绝。 -7. 小程序 WebView 充值使用 `wechat_mp_virtual` 调起小程序虚拟支付;微信内浏览器使用 `wechat_jsapi` 调起微信支付 JSAPI;普通 Web 使用 `wechat_native` 二维码支付,避免因移动 UA、触控能力或窄屏误入 `wechat_h5`。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。 +7. 小程序 WebView 充值使用 `wechat_mp_virtual` 调起小程序虚拟支付;微信内浏览器使用 `wechat_jsapi` 调起微信支付 JSAPI;普通 Web 使用 `wechat_native` 二维码支付,避免因移动 UA、触控能力或窄屏误入 `wechat_h5`。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。一次充值从下单、宿主 / JSAPI / H5 / Native 调起到查单确认与 watch 必须始终携带同一个 `ownerUserId + account lifecycle revision`;pending / confirming order、二维码、提交状态、错误结果、成功回调以及清 token、重新登录等认证副作用在每次写入前都必须校验该令牌,账号切换或卸载后旧链路不得再影响新账号。 8. 后端必须按 access JWT 中的最小设备快照拦截真实微信充值路径,不能只依赖前端隐藏入口或请求体传入的 `paymentChannel`。 9. 后台“充值商品”页继续维护泥点和会员商品配置,保存后影响新的充值中心快照、下单和支付确认;历史订单保留下单时快照。会员商品配置保留不表示当前版本开放公开购买或升级入口。 diff --git a/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx b/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx index d333e061d..edc2708c6 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx +++ b/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx @@ -3,18 +3,77 @@ import { act, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, test, vi } from 'vitest'; +import type { ProfileRechargeProduct } from '../../../packages/shared/src/contracts/runtime'; import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore'; import { + apiClientMocks, + hostBridgeMocks, + paymentPlatformMocks, profileClientMocks, renderController, userA, + userB, } from './usePlatformProfileCenterController.testSupport'; +const rechargeProduct = { + productId: 'points-60', + kind: 'points', +} as ProfileRechargeProduct; + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +function mudPointBalance(totalPoints: number) { + return { + totalPoints, + permanentPoints: totalPoints, + limitedPoints: 0, + limitedExpiresAt: null, + dailyFreePoints: 0, + dailyFreeResetPoints: 20, + dailyFreeResetsAt: '2026-08-07T00:00:00+08:00', + }; +} + +function nativeRechargeResponse(totalPoints = 80, orderId = 'order-a') { + return { + order: { + orderId, + productTitle: '60 泥点', + amountCents: 600, + status: 'pending', + expirationCheckedAt: null, + }, + center: { + walletBalance: totalPoints, + mudPointBalance: mudPointBalance(totalPoints), + }, + wechatNativePayment: { + codeUrl: `weixin://wxpay/${orderId}`, + expiresAt: '2099-08-07T00:00:00+08:00', + }, + }; +} + describe('usePlatformProfileCenterController recharge fallback', () => { beforeEach(() => { window.history.replaceState(null, '', '/profile'); usePlatformWalletStore.getState().resetWalletBalance(); vi.clearAllMocks(); + paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue( + 'wechat_native', + ); + hostBridgeMocks.getHostRuntime.mockReturnValue({ kind: 'browser' }); + profileClientMocks.watchWechatPlatformProfileRechargeOrder.mockReturnValue( + new Promise(() => undefined), + ); }); test('keeps the response legacy balance when no shared snapshot exists', async () => { @@ -36,4 +95,129 @@ describe('usePlatformProfileCenterController recharge fallback', () => { expect(result.current.rechargeCenter?.walletBalance).toBe(37); }); }); + + test('ignores an old owner order response after the account changes', async () => { + const pendingOrder = deferred>(); + profileClientMocks.createPlatformProfileRechargeOrder + .mockReturnValueOnce(pendingOrder.promise) + .mockResolvedValueOnce(nativeRechargeResponse(90, 'order-b')); + usePlatformWalletStore.getState().setWalletOwner('user-a'); + const { result, rerender } = renderController(userA); + + act(() => result.current.buyRechargeProduct(rechargeProduct)); + expect(result.current.submittingRechargeProductId).toBe('points-60'); + + act(() => { + rerender({ user: userB }); + usePlatformWalletStore.getState().setWalletOwner('user-b'); + }); + act(() => result.current.buyRechargeProduct(rechargeProduct)); + await waitFor(() => { + expect(result.current.nativeWechatPayment?.orderId).toBe('order-b'); + }); + await act(async () => { + pendingOrder.resolve(nativeRechargeResponse()); + await pendingOrder.promise; + await Promise.resolve(); + }); + + expect(result.current.submittingRechargeProductId).toBeNull(); + expect(result.current.nativeWechatPayment?.orderId).toBe('order-b'); + expect(result.current.rechargePaymentResult).toBeNull(); + expect(result.current.rechargeCenter?.walletBalance).toBe(90); + expect(usePlatformWalletStore.getState()).toMatchObject({ + ownerUserId: 'user-b', + mudPointBalance: mudPointBalance(90), + }); + }); + + test('ignores an old owner native confirmation after the account changes', async () => { + const pendingConfirmation = deferred<{ + order: ReturnType['order']; + center: ReturnType['center']; + }>(); + const onRechargeSuccess = vi.fn(); + profileClientMocks.createPlatformProfileRechargeOrder.mockResolvedValue( + nativeRechargeResponse(), + ); + profileClientMocks.confirmWechatPlatformProfileRechargeOrder.mockReturnValue( + pendingConfirmation.promise, + ); + usePlatformWalletStore.getState().setWalletOwner('user-a'); + const { result, rerender } = renderController(userA, onRechargeSuccess); + + act(() => result.current.buyRechargeProduct(rechargeProduct)); + await waitFor(() => { + expect(result.current.nativeWechatPayment?.orderId).toBe('order-a'); + }); + act(() => result.current.confirmNativeWechatPayment()); + + act(() => { + rerender({ user: userB }); + usePlatformWalletStore.getState().setWalletOwner('user-b'); + }); + await act(async () => { + pendingConfirmation.resolve({ + order: { + ...nativeRechargeResponse().order, + status: 'paid', + }, + center: nativeRechargeResponse(100).center, + }); + await pendingConfirmation.promise; + await Promise.resolve(); + }); + + expect(result.current.nativeWechatPayment).toBeNull(); + expect(result.current.rechargePaymentResult).toBeNull(); + expect(result.current.wechatRechargeOrderConfirmationState).toBeNull(); + expect(onRechargeSuccess).not.toHaveBeenCalled(); + expect(usePlatformWalletStore.getState()).toMatchObject({ + ownerUserId: 'user-b', + mudPointBalance: null, + }); + }); + + test('does not clear the new owner token from an old owner payment error', async () => { + const pendingOrder = deferred(); + paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue( + 'wechat_mp_virtual', + ); + hostBridgeMocks.getHostRuntime.mockReturnValue({ + kind: 'wechat_mini_program', + }); + profileClientMocks.createPlatformProfileRechargeOrder + .mockReturnValueOnce(pendingOrder.promise) + .mockResolvedValueOnce(nativeRechargeResponse(70, 'order-b')); + usePlatformWalletStore.getState().setWalletOwner('user-a'); + const { result, rerender } = renderController(userA); + + act(() => result.current.buyRechargeProduct(rechargeProduct)); + act(() => { + rerender({ user: userB }); + usePlatformWalletStore.getState().setWalletOwner('user-b'); + }); + paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue( + 'wechat_native', + ); + act(() => result.current.buyRechargeProduct(rechargeProduct)); + await waitFor(() => { + expect(result.current.nativeWechatPayment?.orderId).toBe('order-b'); + }); + await act(async () => { + pendingOrder.reject(new Error('当前登录设备不支持充值')); + await pendingOrder.promise.catch(() => undefined); + await Promise.resolve(); + }); + + expect(apiClientMocks.clearStoredAccessToken).not.toHaveBeenCalled(); + expect(hostBridgeMocks.requestHostLogin).not.toHaveBeenCalled(); + expect(result.current.rechargeError).toBeNull(); + expect(result.current.rechargePaymentResult).toBeNull(); + expect(result.current.nativeWechatPayment?.orderId).toBe('order-b'); + expect(usePlatformWalletStore.getState()).toMatchObject({ + ownerUserId: 'user-b', + mudPointBalance: mudPointBalance(70), + }); + }); }); diff --git a/src/components/platform-entry/usePlatformProfileCenterController.testSupport.tsx b/src/components/platform-entry/usePlatformProfileCenterController.testSupport.tsx index 77fc1b13c..a66a341cd 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.testSupport.tsx +++ b/src/components/platform-entry/usePlatformProfileCenterController.testSupport.tsx @@ -16,11 +16,52 @@ const profileClientMocks = vi.hoisted(() => ({ watchWechatPlatformProfileRechargeOrder: vi.fn(), })); -vi.mock('../../services/platform-entry/platformProfileClient', () => - profileClientMocks, +const apiClientMocks = vi.hoisted(() => ({ + clearStoredAccessToken: vi.fn(), +})); + +const hostBridgeMocks = vi.hoisted(() => ({ + getHostRuntime: vi.fn(() => ({ kind: 'browser' })), + requestHostLogin: vi.fn(), + requestHostPayment: vi.fn(), +})); + +const paymentPlatformMocks = vi.hoisted(() => ({ + resolveProfileRechargeProductPaymentChannel: vi.fn(() => 'wechat_native'), +})); + +vi.mock( + '../../services/platform-entry/platformProfileClient', + () => profileClientMocks, ); -export { profileClientMocks }; +vi.mock('../../services/apiClient', async () => { + const actual = await vi.importActual< + typeof import('../../services/apiClient') + >('../../services/apiClient'); + return { ...actual, ...apiClientMocks }; +}); + +vi.mock('../../services/host-bridge/hostBridge', async () => { + const actual = await vi.importActual< + typeof import('../../services/host-bridge/hostBridge') + >('../../services/host-bridge/hostBridge'); + return { ...actual, ...hostBridgeMocks }; +}); + +vi.mock('../../services/payment/paymentPlatform', async () => { + const actual = await vi.importActual< + typeof import('../../services/payment/paymentPlatform') + >('../../services/payment/paymentPlatform'); + return { ...actual, ...paymentPlatformMocks }; +}); + +export { + apiClientMocks, + hostBridgeMocks, + paymentPlatformMocks, + profileClientMocks, +}; import { usePlatformProfileCenterController } from './usePlatformProfileCenterController'; diff --git a/src/components/platform-entry/usePlatformProfileCenterController.ts b/src/components/platform-entry/usePlatformProfileCenterController.ts index 6711ce8c3..517a3e0c0 100644 --- a/src/components/platform-entry/usePlatformProfileCenterController.ts +++ b/src/components/platform-entry/usePlatformProfileCenterController.ts @@ -76,6 +76,16 @@ export type WechatRechargeOrderConfirmationState = { export type NativeWechatPaymentState = PlatformProfileRechargeNativePaymentState; +type AccountLifecycle = Readonly<{ + ownerUserId: string; + revision: number; +}>; + +type WechatRechargeOrderLifecycle = Readonly<{ + orderId: string; + account: AccountLifecycle; +}>; + function isWechatJsapiMissingIdentityError(error: unknown) { return ( error instanceof Error && @@ -348,14 +358,52 @@ export function usePlatformProfileCenterController({ const [referralSuccess, setReferralSuccess] = useState(null); const { copyState: inviteCopyState, copyText: copyInviteText } = useCopyFeedback(); - const pendingWechatRechargeOrderIdRef = useRef(null); - const confirmingWechatRechargeOrderIdRef = useRef(null); + const pendingWechatRechargeOrderRef = + useRef(null); + const confirmingWechatRechargeOrderRef = + useRef(null); const rechargeCenterReadRevisionRef = useRef(0); const accountLifecycleRevisionRef = useRef(0); const walletLedgerReadRevisionRef = useRef(0); const currentUserId = currentUser?.id ?? ''; const currentUserIdRef = useRef(currentUserId); currentUserIdRef.current = currentUserId; + const captureAccountLifecycle = useCallback( + (): AccountLifecycle => ({ + ownerUserId: currentUserId, + revision: accountLifecycleRevisionRef.current, + }), + [currentUserId], + ); + const isAccountLifecycleCurrent = useCallback( + (account: AccountLifecycle) => + account.revision === accountLifecycleRevisionRef.current && + account.ownerUserId === currentUserIdRef.current, + [], + ); + const isRechargeOrderCurrent = useCallback( + ( + order: WechatRechargeOrderLifecycle | null | undefined, + ): order is WechatRechargeOrderLifecycle => + Boolean(order) && isAccountLifecycleCurrent(order!.account), + [isAccountLifecycleCurrent], + ); + const isSameRechargeOrder = useCallback( + ( + left: WechatRechargeOrderLifecycle | null | undefined, + right: WechatRechargeOrderLifecycle | null | undefined, + ) => { + if (!left || !right) { + return false; + } + return ( + left.orderId === right.orderId && + left.account.ownerUserId === right.account.ownerUserId && + left.account.revision === right.account.revision + ); + }, + [], + ); const walletOwnerUserId = usePlatformWalletStore( (state) => state.ownerUserId, ); @@ -381,8 +429,8 @@ export function usePlatformProfileCenterController({ rechargeCenterReadRevisionRef.current += 1; accountLifecycleRevisionRef.current += 1; walletLedgerReadRevisionRef.current += 1; - pendingWechatRechargeOrderIdRef.current = null; - confirmingWechatRechargeOrderIdRef.current = null; + pendingWechatRechargeOrderRef.current = null; + confirmingWechatRechargeOrderRef.current = null; setRechargeCenter(null); setIsLoadingRechargeCenter(false); setRechargeError(null); @@ -408,6 +456,7 @@ export function usePlatformProfileCenterController({ useEffect(() => { return () => { rechargeCenterReadRevisionRef.current += 1; + accountLifecycleRevisionRef.current += 1; rechargeCenterReadAbortControllerRef.current?.abort(); rechargeCenterReadAbortControllerRef.current = null; }; @@ -482,12 +531,13 @@ export function usePlatformProfileCenterController({ const applyRechargeCenter = useCallback( ( center: ProfileRechargeCenterResponse, + account: AccountLifecycle, refreshWallet = false, - walletSnapshot = captureWalletBalanceSnapshot(currentUserId), + walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId), ) => { if ( - !currentUserId || - usePlatformWalletStore.getState().ownerUserId !== currentUserId + !isAccountLifecycleCurrent(account) || + usePlatformWalletStore.getState().ownerUserId !== account.ownerUserId ) { return false; } @@ -508,13 +558,14 @@ export function usePlatformProfileCenterController({ [ applyWalletBalanceSnapshot, captureWalletBalanceSnapshot, - currentUserId, + isAccountLifecycleCurrent, onWalletBalanceMayHaveChanged, ], ); const loadRechargeCenter = useCallback(() => { - const walletSnapshot = captureWalletBalanceSnapshot(currentUserId); + const account = captureAccountLifecycle(); + const walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId); const revision = ++rechargeCenterReadRevisionRef.current; rechargeCenterReadAbortControllerRef.current?.abort(); const abortController = new AbortController(); @@ -525,15 +576,17 @@ export function usePlatformProfileCenterController({ .then((center) => { if ( !abortController.signal.aborted && - revision === rechargeCenterReadRevisionRef.current + revision === rechargeCenterReadRevisionRef.current && + isAccountLifecycleCurrent(account) ) { - applyRechargeCenter(center, false, walletSnapshot); + applyRechargeCenter(center, account, false, walletSnapshot); } }) .catch((error: unknown) => { if ( abortController.signal.aborted || - revision !== rechargeCenterReadRevisionRef.current + revision !== rechargeCenterReadRevisionRef.current || + !isAccountLifecycleCurrent(account) ) { return; } @@ -546,20 +599,34 @@ export function usePlatformProfileCenterController({ if (rechargeCenterReadAbortControllerRef.current === abortController) { rechargeCenterReadAbortControllerRef.current = null; } - if (revision === rechargeCenterReadRevisionRef.current) { + if ( + revision === rechargeCenterReadRevisionRef.current && + isAccountLifecycleCurrent(account) + ) { setIsLoadingRechargeCenter(false); } }); - }, [applyRechargeCenter, captureWalletBalanceSnapshot, currentUserId]); + }, [ + applyRechargeCenter, + captureAccountLifecycle, + captureWalletBalanceSnapshot, + isAccountLifecycleCurrent, + ]); - const refreshRechargeState = useCallback(() => { - loadRechargeCenter(); - setSubmittingRechargeProductId(null); - pendingWechatRechargeOrderIdRef.current = null; - confirmingWechatRechargeOrderIdRef.current = null; - setWechatRechargeOrderConfirmationState(null); - setNativeWechatPayment(null); - }, [loadRechargeCenter]); + const refreshRechargeState = useCallback( + (account: AccountLifecycle) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } + loadRechargeCenter(); + setSubmittingRechargeProductId(null); + pendingWechatRechargeOrderRef.current = null; + confirmingWechatRechargeOrderRef.current = null; + setWechatRechargeOrderConfirmationState(null); + setNativeWechatPayment(null); + }, + [isAccountLifecycleCurrent, loadRechargeCenter], + ); const handleWechatPayResult = useCallback(() => { const payResult = readWechatPayResultFromHash(); @@ -567,38 +634,62 @@ export function usePlatformProfileCenterController({ return false; } + const pendingOrder = pendingWechatRechargeOrderRef.current; + if (pendingOrder && !isRechargeOrderCurrent(pendingOrder)) { + return false; + } if ( - pendingWechatRechargeOrderIdRef.current && + pendingOrder && payResult.orderId && - payResult.orderId !== pendingWechatRechargeOrderIdRef.current + payResult.orderId !== pendingOrder.orderId ) { return false; } + const account = pendingOrder?.account ?? captureAccountLifecycle(); + if (!account.ownerUserId || !isAccountLifecycleCurrent(account)) { + return false; + } + if (payResult.status === 'success') { - const orderId = - payResult.orderId || pendingWechatRechargeOrderIdRef.current; + const orderId = payResult.orderId || pendingOrder?.orderId; if (!orderId) { clearWechatPayResultHash(); return true; } - if (confirmingWechatRechargeOrderIdRef.current === orderId) { + const order = pendingOrder ?? { orderId, account }; + if ( + isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) + ) { clearWechatPayResultHash(); return true; } - confirmingWechatRechargeOrderIdRef.current = orderId; + confirmingWechatRechargeOrderRef.current = order; setWechatRechargeOrderConfirmationState({ orderId }); setSubmittingRechargeProductId(null); setRechargePaymentResult(null); void confirmWechatRechargeOrderUntilSettled(orderId) .then((response) => { - const result = buildRechargePaymentResultForOrder(response.order); - const isPaid = result.kind === 'success'; - if (!applyRechargeCenter(response.center, true)) { + if ( + !isRechargeOrderCurrent(order) || + !isSameRechargeOrder( + confirmingWechatRechargeOrderRef.current, + order, + ) + ) { return; } - pendingWechatRechargeOrderIdRef.current = null; - confirmingWechatRechargeOrderIdRef.current = null; + const result = buildRechargePaymentResultForOrder(response.order); + const isPaid = result.kind === 'success'; + if (!applyRechargeCenter(response.center, account, true)) { + return; + } + if ( + isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order) + ) { + pendingWechatRechargeOrderRef.current = null; + } + confirmingWechatRechargeOrderRef.current = null; setWechatRechargeOrderConfirmationState(null); setRechargePaymentResult(result); if (isPaid) { @@ -607,7 +698,16 @@ export function usePlatformProfileCenterController({ clearWechatPayResultHash(); }) .catch(() => { - confirmingWechatRechargeOrderIdRef.current = null; + if ( + !isRechargeOrderCurrent(order) || + !isSameRechargeOrder( + confirmingWechatRechargeOrderRef.current, + order, + ) + ) { + return; + } + confirmingWechatRechargeOrderRef.current = null; setWechatRechargeOrderConfirmationState(null); setRechargePaymentResult({ kind: 'pending', @@ -623,7 +723,7 @@ export function usePlatformProfileCenterController({ message: '本次没有扣款,泥点余额未发生变化。', }); setWechatRechargeOrderConfirmationState(null); - refreshRechargeState(); + refreshRechargeState(account); } else { const detail = payResult.errorMessage ? `微信返回:${payResult.errorMessage}` @@ -634,12 +734,20 @@ export function usePlatformProfileCenterController({ message: detail, }); setWechatRechargeOrderConfirmationState(null); - refreshRechargeState(); + refreshRechargeState(account); } clearWechatPayResultHash(); return true; - }, [applyRechargeCenter, onRechargeSuccess, refreshRechargeState]); + }, [ + applyRechargeCenter, + captureAccountLifecycle, + isAccountLifecycleCurrent, + isRechargeOrderCurrent, + isSameRechargeOrder, + onRechargeSuccess, + refreshRechargeState, + ]); const pollWechatPayResultFromHash = useCallback( () => handleWechatPayResult(), @@ -651,23 +759,32 @@ export function usePlatformProfileCenterController({ return false; } - const orderId = pendingWechatRechargeOrderIdRef.current; - if (!orderId || confirmingWechatRechargeOrderIdRef.current === orderId) { + const order = pendingWechatRechargeOrderRef.current; + if ( + !isRechargeOrderCurrent(order) || + isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) + ) { return false; } - confirmingWechatRechargeOrderIdRef.current = orderId; - setWechatRechargeOrderConfirmationState({ orderId }); + confirmingWechatRechargeOrderRef.current = order; + setWechatRechargeOrderConfirmationState({ orderId: order.orderId }); setRechargePaymentResult(null); - void confirmWechatRechargeOrderUntilSettled(orderId) + void confirmWechatRechargeOrderUntilSettled(order.orderId) .then((response) => { - const result = buildRechargePaymentResultForOrder(response.order); - const isPaid = result.kind === 'success'; - if (!applyRechargeCenter(response.center, true)) { + if ( + !isRechargeOrderCurrent(order) || + !isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) + ) { return; } - pendingWechatRechargeOrderIdRef.current = null; - confirmingWechatRechargeOrderIdRef.current = null; + const result = buildRechargePaymentResultForOrder(response.order); + const isPaid = result.kind === 'success'; + if (!applyRechargeCenter(response.center, order.account, true)) { + return; + } + pendingWechatRechargeOrderRef.current = null; + confirmingWechatRechargeOrderRef.current = null; setWechatRechargeOrderConfirmationState(null); setSubmittingRechargeProductId(null); setRechargePaymentResult(result); @@ -676,7 +793,13 @@ export function usePlatformProfileCenterController({ } }) .catch(() => { - confirmingWechatRechargeOrderIdRef.current = null; + if ( + !isRechargeOrderCurrent(order) || + !isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) + ) { + return; + } + confirmingWechatRechargeOrderRef.current = null; setWechatRechargeOrderConfirmationState(null); setRechargePaymentResult({ kind: 'pending', @@ -685,7 +808,13 @@ export function usePlatformProfileCenterController({ }); }); return true; - }, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]); + }, [ + applyRechargeCenter, + isRechargeOrderCurrent, + isSameRechargeOrder, + nativeWechatPayment, + onRechargeSuccess, + ]); const openRechargeModal = useCallback(() => { if (!currentUser) { @@ -717,16 +846,30 @@ export function usePlatformProfileCenterController({ if (current?.isConfirming) { return current; } - pendingWechatRechargeOrderIdRef.current = null; + const pendingOrder = pendingWechatRechargeOrderRef.current; + if ( + current && + pendingOrder?.orderId === current.orderId && + isRechargeOrderCurrent(pendingOrder) + ) { + pendingWechatRechargeOrderRef.current = null; + } return null; }); - }, []); + }, [isRechargeOrderCurrent]); const buyRechargeProduct = useCallback( (product: ProfileRechargeProduct) => { - if (submittingRechargeProductId) { + const account = captureAccountLifecycle(); + if ( + submittingRechargeProductId || + !account.ownerUserId || + !isAccountLifecycleCurrent(account) + ) { return; } + const walletSnapshot = captureWalletBalanceSnapshot(account.ownerUserId); + let createdOrder: WechatRechargeOrderLifecycle | null = null; const paymentChannel = resolveProfileRechargeProductPaymentChannel( { kind: product.kind }, @@ -739,23 +882,50 @@ export function usePlatformProfileCenterController({ setNativeWechatPayment(null); void createPlatformProfileRechargeOrder(product.productId, paymentChannel) .then(async (response) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } + const order: WechatRechargeOrderLifecycle = { + orderId: response.order.orderId, + account, + }; + createdOrder = order; + pendingWechatRechargeOrderRef.current = order; + if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) { - pendingWechatRechargeOrderIdRef.current = response.order.orderId; - if (!applyRechargeCenter(response.center, true)) { + if ( + !applyRechargeCenter( + response.center, + account, + true, + walletSnapshot, + ) || + !isRechargeOrderCurrent(order) + ) { return; } const paymentHandled = await requestHostPayment({ payload: response.wechatMiniProgramPayParams, orderId: response.order.orderId, }); + if (!isRechargeOrderCurrent(order)) { + return; + } if (!paymentHandled) { throw new Error('请在微信小程序内完成支付'); } return; } if (paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL) { - pendingWechatRechargeOrderIdRef.current = response.order.orderId; - if (!applyRechargeCenter(response.center, true)) { + if ( + !applyRechargeCenter( + response.center, + account, + true, + walletSnapshot, + ) || + !isRechargeOrderCurrent(order) + ) { return; } setRechargePaymentResult({ @@ -766,6 +936,9 @@ export function usePlatformProfileCenterController({ await requestWechatJsapiPayment( response.wechatMiniProgramPayParams, ); + if (!isRechargeOrderCurrent(order)) { + return; + } setRechargePaymentResult({ kind: 'pending', title: '支付处理中', @@ -773,22 +946,37 @@ export function usePlatformProfileCenterController({ }); void confirmWechatRechargeOrderUntilSettled(response.order.orderId) .then((confirmResponse) => { + if (!isRechargeOrderCurrent(order)) { + return; + } const result = buildRechargePaymentResultForOrder( confirmResponse.order, ); const isPaid = result.kind === 'success'; - if (!applyRechargeCenter(confirmResponse.center, true)) { + if ( + !applyRechargeCenter(confirmResponse.center, account, true) + ) { return; } setRechargePaymentResult(result); if (result.kind !== 'pending') { - pendingWechatRechargeOrderIdRef.current = null; + if ( + isSameRechargeOrder( + pendingWechatRechargeOrderRef.current, + order, + ) + ) { + pendingWechatRechargeOrderRef.current = null; + } } if (isPaid) { void onRechargeSuccess?.(); } }) .catch(() => { + if (!isRechargeOrderCurrent(order)) { + return; + } setRechargePaymentResult({ kind: 'pending', title: '等待微信确认', @@ -802,8 +990,15 @@ export function usePlatformProfileCenterController({ if (!h5Url) { throw new Error('微信 H5 支付链接生成失败'); } - pendingWechatRechargeOrderIdRef.current = response.order.orderId; - if (!applyRechargeCenter(response.center, true)) { + if ( + !applyRechargeCenter( + response.center, + account, + true, + walletSnapshot, + ) || + !isRechargeOrderCurrent(order) + ) { return; } setRechargePaymentResult({ @@ -811,6 +1006,9 @@ export function usePlatformProfileCenterController({ title: '正在打开微信支付', message: '完成支付后返回页面确认到账状态。', }); + if (!isRechargeOrderCurrent(order)) { + return; + } await redirectToPaymentUrl(h5Url); return; } @@ -821,8 +1019,15 @@ export function usePlatformProfileCenterController({ if (!wechatNativePayment || !codeUrl || !expiresAt) { throw new Error('微信 Native 支付链接生成失败'); } - pendingWechatRechargeOrderIdRef.current = response.order.orderId; - if (!applyRechargeCenter(response.center, true)) { + if ( + !applyRechargeCenter( + response.center, + account, + true, + walletSnapshot, + ) || + !isRechargeOrderCurrent(order) + ) { return; } setNativeWechatPayment({ @@ -841,7 +1046,18 @@ export function usePlatformProfileCenterController({ throw new Error('充值支付渠道无效'); }) .catch((error: unknown) => { - pendingWechatRechargeOrderIdRef.current = null; + if (!isAccountLifecycleCurrent(account)) { + return; + } + if ( + createdOrder && + isSameRechargeOrder( + pendingWechatRechargeOrderRef.current, + createdOrder, + ) + ) { + pendingWechatRechargeOrderRef.current = null; + } setNativeWechatPayment(null); if ( paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL && @@ -857,6 +1073,9 @@ export function usePlatformProfileCenterController({ message: '正在打开微信小程序登录,请重新登录后再支付。', }); void requestHostLogin().catch((miniProgramLoginError: unknown) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } setRechargePaymentResult(null); setRechargeError( miniProgramLoginError instanceof Error @@ -877,6 +1096,9 @@ export function usePlatformProfileCenterController({ message: '正在跳转微信授权,授权后请重新发起支付。', }); void startWechatBind().catch((wechatLoginError: unknown) => { + if (!isAccountLifecycleCurrent(account)) { + return; + } setRechargePaymentResult(null); setRechargeError( wechatLoginError instanceof Error @@ -890,13 +1112,29 @@ export function usePlatformProfileCenterController({ setSubmittingRechargeProductId(null); }); }, - [applyRechargeCenter, onRechargeSuccess, submittingRechargeProductId], + [ + applyRechargeCenter, + captureAccountLifecycle, + captureWalletBalanceSnapshot, + isAccountLifecycleCurrent, + isRechargeOrderCurrent, + isSameRechargeOrder, + onRechargeSuccess, + submittingRechargeProductId, + ], ); const confirmNativeWechatPayment = useCallback(() => { if (!nativeWechatPayment || nativeWechatPayment.isConfirming) { return; } + const order = pendingWechatRechargeOrderRef.current; + if ( + !isRechargeOrderCurrent(order) || + order.orderId !== nativeWechatPayment.orderId + ) { + return; + } setNativeWechatPayment((current) => current && current.orderId === nativeWechatPayment.orderId @@ -905,20 +1143,21 @@ export function usePlatformProfileCenterController({ ); void confirmWechatRechargeOrderQuickly(nativeWechatPayment.orderId) .then((response) => { - if ( - pendingWechatRechargeOrderIdRef.current !== - nativeWechatPayment.orderId - ) { + if (!isRechargeOrderCurrent(order)) { return; } const result = buildRechargePaymentResultForOrder(response.order); const isPaid = result.kind === 'success'; - if (!applyRechargeCenter(response.center, true)) { + if (!applyRechargeCenter(response.center, order.account, true)) { return; } if (result.kind !== 'pending') { setNativeWechatPayment(null); - pendingWechatRechargeOrderIdRef.current = null; + if ( + isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order) + ) { + pendingWechatRechargeOrderRef.current = null; + } setRechargePaymentResult(result); if (isPaid) { void onRechargeSuccess?.(); @@ -936,6 +1175,9 @@ export function usePlatformProfileCenterController({ } }) .catch(() => { + if (!isRechargeOrderCurrent(order)) { + return; + } setNativeWechatPayment((current) => current && current.orderId === nativeWechatPayment.orderId ? { @@ -946,20 +1188,40 @@ export function usePlatformProfileCenterController({ : current, ); }) - .finally(() => setSubmittingRechargeProductId(null)); - }, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]); + .finally(() => { + if (isRechargeOrderCurrent(order)) { + setSubmittingRechargeProductId(null); + } + }); + }, [ + applyRechargeCenter, + isRechargeOrderCurrent, + isSameRechargeOrder, + nativeWechatPayment, + onRechargeSuccess, + ]); useEffect(() => { const orderId = nativeWechatPayment?.orderId; const expiresAtMs = Date.parse(nativeWechatPayment?.expiresAt ?? ''); - if (!orderId || !Number.isFinite(expiresAtMs)) { + const order = pendingWechatRechargeOrderRef.current; + if ( + !orderId || + !Number.isFinite(expiresAtMs) || + !isRechargeOrderCurrent(order) || + order.orderId !== orderId + ) { return undefined; } let cancelled = false; const abortController = new AbortController(); const watchUntilSettled = async () => { - while (!cancelled && Date.now() < expiresAtMs) { + while ( + !cancelled && + Date.now() < expiresAtMs && + isRechargeOrderCurrent(order) + ) { try { const response = await watchWechatPlatformProfileRechargeOrder( orderId, @@ -970,13 +1232,13 @@ export function usePlatformProfileCenterController({ if ( cancelled || !response || - pendingWechatRechargeOrderIdRef.current !== orderId + !isSameRechargeOrder(pendingWechatRechargeOrderRef.current, order) ) { return; } const result = buildRechargePaymentResultForOrder(response.order); - if (!applyRechargeCenter(response.center, true)) { + if (!applyRechargeCenter(response.center, order.account, true)) { return; } if (result.kind === 'pending') { @@ -984,9 +1246,11 @@ export function usePlatformProfileCenterController({ continue; } - pendingWechatRechargeOrderIdRef.current = null; - if (confirmingWechatRechargeOrderIdRef.current === orderId) { - confirmingWechatRechargeOrderIdRef.current = null; + pendingWechatRechargeOrderRef.current = null; + if ( + isSameRechargeOrder(confirmingWechatRechargeOrderRef.current, order) + ) { + confirmingWechatRechargeOrderRef.current = null; } setNativeWechatPayment((current) => current?.orderId === orderId ? null : current, @@ -998,7 +1262,11 @@ export function usePlatformProfileCenterController({ } return; } catch { - if (cancelled || abortController.signal.aborted) { + if ( + cancelled || + abortController.signal.aborted || + !isRechargeOrderCurrent(order) + ) { return; } } @@ -1016,6 +1284,8 @@ export function usePlatformProfileCenterController({ nativeWechatPayment?.expiresAt, nativeWechatPayment?.orderId, applyRechargeCenter, + isRechargeOrderCurrent, + isSameRechargeOrder, onRechargeSuccess, ]);