Revert "extract recharge logic"

This reverts commit 8c899b8a
This commit is contained in:
2026-07-20 10:29:11 +08:00
parent 36ae38c9a6
commit cc4a1f8297
8 changed files with 405 additions and 891 deletions
@@ -1,218 +0,0 @@
/* @vitest-environment jsdom */
import { act, renderHook, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type {
ConfirmWechatProfileRechargeOrderResponse,
CreateProfileRechargeOrderResponse,
ProfileRechargeOrder,
} from '../../contracts/runtime';
import {
buildRechargePaymentResult,
isWechatRechargeOrderTerminal,
useWechatNativeRechargeController,
} from './useWechatNativeRechargeController';
function buildOrder(
overrides: Partial<ProfileRechargeOrder> = {},
): ProfileRechargeOrder {
return {
orderId: 'order-native-1',
productId: 'points_60',
productTitle: '60泥点',
kind: 'points',
amountCents: 600,
status: 'pending',
paymentChannel: 'wechat_native',
paidAt: null,
providerTransactionId: null,
createdAt: '2026-07-17T00:00:00Z',
pointsDelta: 0,
membershipExpiresAt: null,
...overrides,
};
}
function buildCenter(walletBalance = 120) {
return {
walletBalance,
mudPointBalance: {
totalPoints: walletBalance,
permanentPoints: walletBalance,
limitedPoints: 0,
limitedExpiresAt: null,
dailyFreePoints: 0,
dailyFreeResetPoints: 20,
dailyFreeResetsAt: '2026-07-18T00:00:00Z',
},
membership: {
status: 'inactive' as const,
tier: 'normal' as const,
startedAt: null,
expiresAt: null,
updatedAt: null,
cycleStartedAt: null,
cycleResetsAt: null,
cycleGrantedPoints: 0,
cycleRemainingPoints: 0,
cyclePeriodDays: 0,
},
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
};
}
function buildCreateResponse(): CreateProfileRechargeOrderResponse {
return {
order: buildOrder(),
center: buildCenter(),
wechatNativePayment: {
codeUrl: 'weixin://pay.weixin.qq.com/native-test',
expiresAt: '2099-01-01T00:05:00Z',
},
};
}
describe('useWechatNativeRechargeController', () => {
it('classifies pending expiration separately from terminal expiration', () => {
expect(
isWechatRechargeOrderTerminal({
status: 'expired',
expirationCheckedAt: null,
}),
).toBe(false);
expect(
buildRechargePaymentResult({
status: 'expired',
expirationCheckedAt: '2026-07-17T00:05:00Z',
}),
).toMatchObject({ kind: 'expired', title: '支付已过期' });
});
it('rejects an incomplete native payment response', () => {
const { result } = renderHook(() =>
useWechatNativeRechargeController({
confirmOrder: vi.fn(),
onCenterReceived: vi.fn(),
}),
);
const response = buildCreateResponse();
response.wechatNativePayment = { codeUrl: ' ', expiresAt: '2099-01-01' };
expect(() => {
act(() => result.current.beginNativePayment(response));
}).toThrow('微信 Native 支付链接生成失败');
});
it('settles a watched order once and reports the authoritative center', async () => {
const paidResponse: ConfirmWechatProfileRechargeOrderResponse = {
order: buildOrder({
status: 'paid',
paidAt: '2026-07-17T00:01:00Z',
pointsDelta: 60,
}),
center: buildCenter(180),
};
const watchOrder = vi.fn().mockResolvedValue(paidResponse);
const onCenterReceived = vi.fn();
const onPaid = vi.fn();
const onTerminal = vi.fn();
const { result } = renderHook(() =>
useWechatNativeRechargeController({
confirmOrder: vi.fn(),
watchOrder,
onCenterReceived,
onPaid,
onTerminal,
}),
);
act(() => result.current.beginNativePayment(buildCreateResponse()));
await waitFor(() => expect(result.current.nativePayment).toBeNull());
expect(result.current.paymentResult?.kind).toBe('success');
expect(onCenterReceived).toHaveBeenCalledWith(paidResponse.center);
expect(onPaid).toHaveBeenCalledTimes(1);
expect(onTerminal).toHaveBeenCalledTimes(1);
});
it('retries an explicit confirmation before settling a paid order', async () => {
vi.useFakeTimers();
const confirmOrder = vi
.fn()
.mockResolvedValueOnce({
order: buildOrder(),
center: buildCenter(),
})
.mockResolvedValueOnce({
order: buildOrder({ status: 'paid', pointsDelta: 60 }),
center: buildCenter(180),
});
const onPaid = vi.fn();
const { result } = renderHook(() =>
useWechatNativeRechargeController({
confirmOrder,
onCenterReceived: vi.fn(),
onPaid,
}),
);
act(() => result.current.beginNativePayment(buildCreateResponse()));
let confirmation: Promise<void> | undefined;
act(() => {
confirmation = result.current.confirmNativePayment();
});
await act(async () => {
await Promise.resolve();
await vi.advanceTimersByTimeAsync(800);
await confirmation;
});
expect(confirmOrder).toHaveBeenCalledTimes(2);
expect(result.current.paymentResult?.kind).toBe('success');
expect(onPaid).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
it('ignores a watched response after the native lifecycle is reset', async () => {
let resolveWatch:
| ((response: ConfirmWechatProfileRechargeOrderResponse) => void)
| undefined;
const watchOrder = vi.fn(
() =>
new Promise<ConfirmWechatProfileRechargeOrderResponse>((resolve) => {
resolveWatch = resolve;
}),
);
const onCenterReceived = vi.fn();
const onPaid = vi.fn();
const { result } = renderHook(() =>
useWechatNativeRechargeController({
confirmOrder: vi.fn(),
watchOrder,
onCenterReceived,
onPaid,
}),
);
act(() => result.current.beginNativePayment(buildCreateResponse()));
await waitFor(() => expect(watchOrder).toHaveBeenCalledTimes(1));
act(() => result.current.resetNativePayment());
await act(async () => {
resolveWatch?.({
order: buildOrder({ status: 'paid' }),
center: buildCenter(180),
});
await Promise.resolve();
});
expect(result.current.nativePayment).toBeNull();
expect(result.current.paymentResult).toBeNull();
expect(onCenterReceived).not.toHaveBeenCalled();
expect(onPaid).not.toHaveBeenCalled();
});
});
@@ -1,307 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type {
ConfirmWechatProfileRechargeOrderResponse,
CreateProfileRechargeOrderResponse,
ProfileRechargeOrder,
} from '../../contracts/runtime';
import type { PlatformProfileRechargeNativePaymentState } from './index';
const WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS = [800, 1600] as const;
const WECHAT_NATIVE_WATCH_RETRY_DELAY_MS = 1000;
export type RechargePaymentResult = {
kind: 'success' | 'pending' | 'cancel' | 'failed' | 'expired';
title: string;
message: string;
};
export type UseWechatNativeRechargeControllerArgs = {
confirmOrder: (
orderId: string,
) => Promise<ConfirmWechatProfileRechargeOrderResponse>;
watchOrder?: (
orderId: string,
options: { signal: AbortSignal },
) => Promise<ConfirmWechatProfileRechargeOrderResponse>;
onCenterReceived: (
response: ConfirmWechatProfileRechargeOrderResponse['center'],
) => void;
onPaid?: () => void | Promise<void>;
onTerminal?: (response: ConfirmWechatProfileRechargeOrderResponse) => void;
};
export function isWechatRechargeOrderTerminal(
order: Pick<ProfileRechargeOrder, 'status' | 'expirationCheckedAt'>,
) {
return !(
order.status === 'pending' ||
(order.status === 'expired' && !order.expirationCheckedAt)
);
}
export function buildRechargePaymentResult(
order: Pick<ProfileRechargeOrder, 'status' | 'expirationCheckedAt'>,
): RechargePaymentResult {
switch (order.status) {
case 'paid':
return {
kind: 'success',
title: '支付成功',
message: '已到账,泥点余额已刷新。',
};
case 'expired':
return order.expirationCheckedAt
? {
kind: 'expired',
title: '支付已过期',
message: '订单已超过支付时限,本次没有入账。',
}
: {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看泥点余额。',
};
case 'closed':
return {
kind: 'cancel',
title: '支付未完成',
message: '本次没有扣款,泥点余额未发生变化。',
};
case 'failed':
case 'refunded':
return {
kind: 'failed',
title: '支付未完成',
message: '微信支付没有完成,本次不会入账。',
};
case 'pending':
default:
return {
kind: 'pending',
title: '支付处理中',
message: '正在等待到账状态确认,请稍后查看泥点余额。',
};
}
}
function waitForConfirmation(delayMs: number) {
return new Promise<void>((resolve) => {
window.setTimeout(resolve, delayMs);
});
}
export function useWechatNativeRechargeController({
confirmOrder,
watchOrder,
onCenterReceived,
onPaid,
onTerminal,
}: UseWechatNativeRechargeControllerArgs) {
const [nativePayment, setNativePayment] =
useState<PlatformProfileRechargeNativePaymentState | null>(null);
const [paymentResult, setPaymentResult] =
useState<RechargePaymentResult | null>(null);
const activeOrderIdRef = useRef<string | null>(null);
const lifecycleRef = useRef(0);
const settledOrderIdsRef = useRef(new Set<string>());
const applyTerminalResponse = useCallback(
(response: ConfirmWechatProfileRechargeOrderResponse) => {
const orderId = response.order.orderId;
if (
activeOrderIdRef.current !== orderId ||
settledOrderIdsRef.current.has(orderId)
) {
return false;
}
onCenterReceived(response.center);
if (!isWechatRechargeOrderTerminal(response.order)) {
return false;
}
settledOrderIdsRef.current.add(orderId);
activeOrderIdRef.current = null;
setNativePayment((current) =>
current?.orderId === orderId ? null : current,
);
const result = buildRechargePaymentResult(response.order);
setPaymentResult(result);
onTerminal?.(response);
if (result.kind === 'success') {
void onPaid?.();
}
return true;
},
[onCenterReceived, onPaid, onTerminal],
);
const beginNativePayment = useCallback(
(response: CreateProfileRechargeOrderResponse) => {
const payment = response.wechatNativePayment;
const codeUrl = payment?.codeUrl?.trim();
const expiresAt = payment?.expiresAt?.trim();
if (!payment || !codeUrl || !expiresAt) {
throw new Error('微信 Native 支付链接生成失败');
}
lifecycleRef.current += 1;
settledOrderIdsRef.current.delete(response.order.orderId);
activeOrderIdRef.current = response.order.orderId;
setPaymentResult(null);
setNativePayment({
orderId: response.order.orderId,
productTitle: response.order.productTitle,
amountCents: response.order.amountCents,
codeUrl,
expiresAt,
isConfirming: false,
});
},
[],
);
const closeNativePayment = useCallback(() => {
setNativePayment((current) => {
if (current?.isConfirming) {
return current;
}
lifecycleRef.current += 1;
activeOrderIdRef.current = null;
return null;
});
}, []);
const discardNativePayment = useCallback(() => {
lifecycleRef.current += 1;
activeOrderIdRef.current = null;
setNativePayment(null);
}, []);
const resetNativePayment = useCallback(() => {
discardNativePayment();
setPaymentResult(null);
}, [discardNativePayment]);
const confirmNativePayment = useCallback(async () => {
const payment = nativePayment;
if (!payment || payment.isConfirming) {
return;
}
const orderId = payment.orderId;
const lifecycle = lifecycleRef.current;
setNativePayment((current) =>
current?.orderId === orderId
? { ...current, isConfirming: true, confirmMessage: undefined }
: current,
);
try {
let response = await confirmOrder(orderId);
for (const delayMs of WECHAT_NATIVE_CONFIRM_RETRY_DELAYS_MS) {
if (
lifecycleRef.current !== lifecycle ||
activeOrderIdRef.current !== orderId ||
isWechatRechargeOrderTerminal(response.order)
) {
break;
}
await waitForConfirmation(delayMs);
response = await confirmOrder(orderId);
}
if (
lifecycleRef.current !== lifecycle ||
activeOrderIdRef.current !== orderId
) {
return;
}
if (!applyTerminalResponse(response)) {
setNativePayment((current) =>
current?.orderId === orderId
? {
...current,
isConfirming: false,
confirmMessage: '暂未确认到账,请确认付款完成后再点一次。',
}
: current,
);
}
} catch {
if (
lifecycleRef.current === lifecycle &&
activeOrderIdRef.current === orderId
) {
setNativePayment((current) =>
current?.orderId === orderId
? {
...current,
isConfirming: false,
confirmMessage: '暂时没能确认到账状态,请稍后再试。',
}
: current,
);
}
}
}, [applyTerminalResponse, confirmOrder, nativePayment]);
useEffect(() => {
const orderId = nativePayment?.orderId;
const expiresAtMs = Date.parse(nativePayment?.expiresAt ?? '');
if (!watchOrder || !orderId || !Number.isFinite(expiresAtMs)) {
return undefined;
}
const lifecycle = lifecycleRef.current;
const abortController = new AbortController();
let cancelled = false;
const watchUntilSettled = async () => {
while (!cancelled && Date.now() < expiresAtMs) {
try {
const response = await watchOrder(orderId, {
signal: abortController.signal,
});
if (
cancelled ||
lifecycleRef.current !== lifecycle ||
activeOrderIdRef.current !== orderId
) {
return;
}
if (applyTerminalResponse(response)) {
return;
}
} catch {
if (cancelled || abortController.signal.aborted) {
return;
}
}
await waitForConfirmation(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS);
}
};
void watchUntilSettled();
return () => {
cancelled = true;
abortController.abort();
};
}, [
applyTerminalResponse,
nativePayment?.expiresAt,
nativePayment?.orderId,
watchOrder,
]);
return {
beginNativePayment,
closeNativePayment,
confirmNativePayment,
discardNativePayment,
nativePayment,
paymentResult,
resetNativePayment,
setPaymentResult,
};
}