Files
Genarrative/packages/shared/src/stores/createProfileWalletStore.ts
T
k88936 3b9d645042 重构(钱包): 共享组件与钱包 store 改用三池余额
- createProfileWalletStore 的余额状态改为三池视图,去掉「响应只有 walletBalance 时兜底」的旧分支,缺少 mudPointBalance 一律算读取失败
- 泥点明细面板并列展示永久泥点 / 月度泥点 / 每日免费泥点,月度行带北京时间刷新时刻;「不限时泥点」标签改为「永久泥点」
- 充值弹层当前余额改用三池合计,不再读 walletBalance
- 三池与首充赠送相关的 store、明细面板、充值弹层测试同步更新

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-02 19:00:23 +08:00

241 lines
7.3 KiB
TypeScript

import { create, type StoreApi, type UseBoundStore } from 'zustand';
import type {
ProfileMudPointBalance,
ProfileRechargeCenterResponse,
} from '../contracts/runtime';
import {
type ProfileMudPointBalanceView,
resolveProfileMudPointBalanceView,
} from '../utils/mudPoints';
export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error';
export type ProfileWalletApi = {
getRechargeCenter(
signal?: AbortSignal,
): Promise<ProfileRechargeCenterResponse>;
};
export type ProfileWalletBalanceSnapshot = Readonly<{
ownerUserId: string;
ownerVersion: number;
invalidationVersion: number;
operationSequence: number;
}>;
export type ProfileWalletStore = {
ownerUserId: string | null;
mudPointBalance: ProfileMudPointBalanceView | null;
legacyWalletBalance: number | null;
mudPointBalanceStatus: MudPointBalanceStatus;
mudPointBalanceError: string;
setWalletOwner: (userId: string | null) => void;
captureWalletBalanceSnapshot: (
ownerUserId: string,
) => ProfileWalletBalanceSnapshot | null;
applyWalletBalanceSnapshot: (
snapshot: ProfileWalletBalanceSnapshot,
balance: ProfileMudPointBalance | ProfileMudPointBalanceView,
) => boolean;
applyLegacyWalletBalanceSnapshot: (
snapshot: ProfileWalletBalanceSnapshot,
balance: number,
) => boolean;
onWalletBalanceMayHaveChanged: () => Promise<void>;
resetWalletBalance: () => void;
};
const EMPTY_WALLET_STATE = {
mudPointBalance: null,
legacyWalletBalance: null,
mudPointBalanceStatus: 'idle',
mudPointBalanceError: '',
} as const;
export function createProfileWalletStore(
api: ProfileWalletApi,
): UseBoundStore<StoreApi<ProfileWalletStore>> {
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;
const invalidateActiveRefresh = () => {
requestGeneration += 1;
activeAbortController?.abort();
activeAbortController = null;
activeRefresh = null;
};
return create<ProfileWalletStore>((set, get) => ({
ownerUserId: null,
...EMPTY_WALLET_STATE,
setWalletOwner: (userId) => {
const normalizedUserId = userId?.trim() || null;
if (get().ownerUserId === normalizedUserId) {
return;
}
ownerVersion += 1;
latestAppliedOperationSequence = 0;
invalidateActiveRefresh();
settledRefreshVersion = refreshVersion;
set({
ownerUserId: normalizedUserId,
...EMPTY_WALLET_STATE,
});
},
captureWalletBalanceSnapshot: (ownerUserId) => {
const normalizedOwnerUserId = ownerUserId.trim();
if (
!normalizedOwnerUserId ||
get().ownerUserId !== normalizedOwnerUserId
) {
return null;
}
return {
ownerUserId: normalizedOwnerUserId,
ownerVersion,
invalidationVersion: refreshVersion,
operationSequence: ++operationSequence,
};
},
applyWalletBalanceSnapshot: (snapshot, balance) => {
if (
get().ownerUserId !== snapshot.ownerUserId ||
ownerVersion !== snapshot.ownerVersion ||
snapshot.invalidationVersion < refreshVersion ||
snapshot.invalidationVersion < settledRefreshVersion ||
snapshot.operationSequence < latestAppliedOperationSequence
) {
return false;
}
invalidateActiveRefresh();
latestAppliedOperationSequence = Math.max(
latestAppliedOperationSequence,
snapshot.operationSequence,
);
settledRefreshVersion = snapshot.invalidationVersion;
const mudPointBalance = resolveProfileMudPointBalanceView(balance);
set({
mudPointBalance,
legacyWalletBalance: mudPointBalance.totalPoints,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
return true;
},
applyLegacyWalletBalanceSnapshot: (snapshot, balance) => {
if (
!Number.isFinite(balance) ||
get().ownerUserId !== snapshot.ownerUserId ||
ownerVersion !== snapshot.ownerVersion ||
snapshot.invalidationVersion < refreshVersion ||
snapshot.invalidationVersion < settledRefreshVersion ||
snapshot.operationSequence < latestAppliedOperationSequence
) {
return false;
}
invalidateActiveRefresh();
latestAppliedOperationSequence = Math.max(
latestAppliedOperationSequence,
snapshot.operationSequence,
);
settledRefreshVersion = snapshot.invalidationVersion;
set({
mudPointBalance: null,
legacyWalletBalance: balance,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
return true;
},
onWalletBalanceMayHaveChanged: () => {
refreshVersion += 1;
if (activeRefresh) {
return activeRefresh;
}
const abortController = new AbortController();
const refresh = (async () => {
while (settledRefreshVersion < refreshVersion) {
const ownerUserId = get().ownerUserId;
const requestedVersion = refreshVersion;
const generation = requestGeneration;
if (!ownerUserId) {
settledRefreshVersion = requestedVersion;
return;
}
set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' });
try {
const center = await api.getRechargeCenter(abortController.signal);
if (generation !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
settledRefreshVersion = requestedVersion;
continue;
}
if (!center.mudPointBalance) {
throw new Error('充值中心响应缺少泥点余额');
}
settledRefreshVersion = requestedVersion;
const mudPointBalance = resolveProfileMudPointBalanceView(
center.mudPointBalance,
);
set({
mudPointBalance,
legacyWalletBalance: mudPointBalance.totalPoints,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
} catch {
if (generation !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
settledRefreshVersion = requestedVersion;
continue;
}
settledRefreshVersion = requestedVersion;
set({
mudPointBalanceStatus: 'error',
mudPointBalanceError: '泥点明细读取失败',
});
}
}
})();
activeRefresh = refresh;
activeAbortController = abortController;
const clearActiveRefresh = () => {
if (activeRefresh === refresh) {
activeRefresh = null;
}
if (activeAbortController === abortController) {
activeAbortController = null;
}
};
void refresh.then(clearActiveRefresh, clearActiveRefresh);
return refresh;
},
resetWalletBalance: () => {
ownerVersion += 1;
latestAppliedOperationSequence = 0;
invalidateActiveRefresh();
settledRefreshVersion = refreshVersion;
set({ ownerUserId: null, ...EMPTY_WALLET_STATE });
},
}));
}