修复钱包异步边界与任务陈旧状态

隔离账号切换后的充值响应并中止失效支付监听

防止乱序钱包快照回滚并保留旧版总额兜底

清理任务重试耗尽后的陈旧状态并补充回归测试

稳定测试夹具、用户错误提示与首帧加载状态

同步单一钱包 Store 的长期决策记录
This commit is contained in:
2026-08-07 11:00:04 +08:00
parent 58bf56ec56
commit 098009a701
12 changed files with 273 additions and 65 deletions
@@ -156,7 +156,7 @@ describe('createProfileWalletStore', () => {
expect(store.getState()).toMatchObject({
mudPointBalance: balance(55),
mudPointBalanceStatus: 'error',
mudPointBalanceError: 'network down',
mudPointBalanceError: '泥点明细读取失败',
});
});
@@ -173,7 +173,7 @@ describe('createProfileWalletStore', () => {
expect(store.getState()).toMatchObject({
mudPointBalance: null,
mudPointBalanceStatus: 'error',
mudPointBalanceError: '充值中心响应缺少泥点余额',
mudPointBalanceError: '泥点明细读取失败',
});
});
@@ -235,6 +235,43 @@ describe('createProfileWalletStore', () => {
expect(store.getState().mudPointBalance?.totalPoints).toBe(100);
});
test('rejects an older direct snapshot after a newer operation has applied', () => {
const store = createProfileWalletStore({ getRechargeCenter: vi.fn() });
store.getState().setWalletOwner('user-a');
const olderSnapshot = store
.getState()
.captureWalletBalanceSnapshot('user-a');
const newerSnapshot = store
.getState()
.captureWalletBalanceSnapshot('user-a');
expect(
store.getState().applyWalletBalanceSnapshot(newerSnapshot!, balance(100)),
).toBe(true);
expect(
store.getState().applyWalletBalanceSnapshot(olderSnapshot!, balance(20)),
).toBe(false);
expect(store.getState().mudPointBalance?.totalPoints).toBe(100);
});
test('retains a legacy total without inventing a balance breakdown', async () => {
const store = createProfileWalletStore({
getRechargeCenter: vi.fn().mockResolvedValue({
walletBalance: 37,
} as ProfileRechargeCenterResponse),
});
store.getState().setWalletOwner('user-a');
await store.getState().onWalletBalanceMayHaveChanged();
expect(store.getState()).toMatchObject({
legacyWalletBalance: 37,
mudPointBalance: null,
mudPointBalanceStatus: 'error',
mudPointBalanceError: '泥点明细读取失败',
});
});
test('rejects a snapshot after switching away from and back to the same owner', () => {
const store = createProfileWalletStore({ getRechargeCenter: vi.fn() });
store.getState().setWalletOwner('user-a');
@@ -17,11 +17,13 @@ export type ProfileWalletBalanceSnapshot = Readonly<{
ownerUserId: string;
ownerVersion: number;
invalidationVersion: number;
operationSequence: number;
}>;
export type ProfileWalletStore = {
ownerUserId: string | null;
mudPointBalance: ProfileMudPointBalance | null;
legacyWalletBalance: number | null;
mudPointBalanceStatus: MudPointBalanceStatus;
mudPointBalanceError: string;
setWalletOwner: (userId: string | null) => void;
@@ -38,6 +40,7 @@ export type ProfileWalletStore = {
const EMPTY_WALLET_STATE = {
mudPointBalance: null,
legacyWalletBalance: null,
mudPointBalanceStatus: 'idle',
mudPointBalanceError: '',
} as const;
@@ -48,6 +51,8 @@ export function createProfileWalletStore(
let refreshVersion = 0;
let settledRefreshVersion = 0;
let ownerVersion = 0;
let operationSequence = 0;
let latestAppliedOperationSequence = 0;
let requestGeneration = 0;
let activeRefresh: Promise<void> | null = null;
let activeAbortController: AbortController | null = null;
@@ -69,6 +74,7 @@ export function createProfileWalletStore(
}
ownerVersion += 1;
latestAppliedOperationSequence = 0;
invalidateActiveRefresh();
settledRefreshVersion = refreshVersion;
set({
@@ -89,6 +95,7 @@ export function createProfileWalletStore(
ownerUserId: normalizedOwnerUserId,
ownerVersion,
invalidationVersion: refreshVersion,
operationSequence: ++operationSequence,
};
},
applyWalletBalanceSnapshot: (snapshot, balance) => {
@@ -96,15 +103,21 @@ export function createProfileWalletStore(
get().ownerUserId !== snapshot.ownerUserId ||
ownerVersion !== snapshot.ownerVersion ||
snapshot.invalidationVersion < refreshVersion ||
snapshot.invalidationVersion < settledRefreshVersion
snapshot.invalidationVersion < settledRefreshVersion ||
snapshot.operationSequence < latestAppliedOperationSequence
) {
return false;
}
invalidateActiveRefresh();
latestAppliedOperationSequence = Math.max(
latestAppliedOperationSequence,
snapshot.operationSequence,
);
settledRefreshVersion = snapshot.invalidationVersion;
set({
mudPointBalance: balance,
legacyWalletBalance: balance.totalPoints,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
@@ -138,16 +151,20 @@ export function createProfileWalletStore(
continue;
}
if (!center.mudPointBalance) {
if (Number.isFinite(center.walletBalance)) {
set({ legacyWalletBalance: center.walletBalance });
}
throw new Error('充值中心响应缺少泥点余额');
}
settledRefreshVersion = requestedVersion;
set({
mudPointBalance: center.mudPointBalance,
legacyWalletBalance: center.mudPointBalance.totalPoints,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
} catch (error) {
} catch {
if (generation !== requestGeneration) {
return;
}
@@ -159,8 +176,7 @@ export function createProfileWalletStore(
settledRefreshVersion = requestedVersion;
set({
mudPointBalanceStatus: 'error',
mudPointBalanceError:
error instanceof Error ? error.message : '泥点明细读取失败',
mudPointBalanceError: '泥点明细读取失败',
});
}
}
@@ -180,6 +196,7 @@ export function createProfileWalletStore(
},
resetWalletBalance: () => {
ownerVersion += 1;
latestAppliedOperationSequence = 0;
invalidateActiveRefresh();
settledRefreshVersion = refreshVersion;
set({ ownerUserId: null, ...EMPTY_WALLET_STATE });