fix(auth): 账号切换 - 切换用户后清空旧账号缓存并隔离积分响应

This commit is contained in:
ddCat
2026-07-28 08:02:59 +08:00
parent cbcdcc2be2
commit b1d8a0a9ae
4 changed files with 61 additions and 28 deletions

View File

@@ -1,24 +1,15 @@
import type { ReactNode } from "react";
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import { App, ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import { AuthSessionHydrator } from "@/components/auth/auth-session-hydrator";
import { ClientRootInit } from "@/components/layout/client-root-init";
import { getAntThemeConfig } from "@/lib/app-theme";
import { appQueryClient } from "@/lib/query-client";
import { useThemeStore } from "@/stores/use-theme-store";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: false,
refetchOnWindowFocus: false,
},
},
});
export function AppProviders({ children }: { children: ReactNode }) {
const theme = useThemeStore((state) => state.theme);
const dark = theme === "dark";
@@ -31,7 +22,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
return (
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark)}>
<App message={{ duration: 3, maxCount: 3 }} notification={{ duration: 4.5, maxCount: 3, placement: "topRight" }}>
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={appQueryClient}>
<AuthSessionHydrator>
<ClientRootInit>{children}</ClientRootInit>
</AuthSessionHydrator>

View File

@@ -1,32 +1,52 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { getWallet } from "@/services/api/wallet";
const WALLET_REFRESH_INTERVAL_MS = 30_000;
type WalletBalanceSnapshot = {
userId: string;
availableMicrocredits: number | null;
refreshing: boolean;
};
export function useWalletBalance(userId?: string, enabled = true) {
const [availableMicrocredits, setAvailableMicrocredits] = useState<number | null>(null);
const [refreshing, setRefreshing] = useState(false);
const activeUserId = enabled ? userId || "" : "";
const requestSequence = useRef(0);
const [snapshot, setSnapshot] = useState<WalletBalanceSnapshot>({ userId: "", availableMicrocredits: null, refreshing: false });
const refresh = useCallback(async () => {
if (!enabled || !userId) {
setAvailableMicrocredits(null);
const requestedUserId = activeUserId;
const sequence = ++requestSequence.current;
if (!requestedUserId) {
setSnapshot({ userId: "", availableMicrocredits: null, refreshing: false });
return;
}
setRefreshing(true);
setSnapshot((current) => ({
userId: requestedUserId,
availableMicrocredits: current.userId === requestedUserId ? current.availableMicrocredits : null,
refreshing: true,
}));
try {
const wallet = await getWallet(1, 1);
setAvailableMicrocredits(wallet.account.availableMicrocredits);
} catch {
// 顶栏余额是只读辅助信息,读取失败时保留上次成功值,避免短暂网络错误造成闪烁。
} finally {
setRefreshing(false);
if (sequence !== requestSequence.current) return;
if (wallet.account.userId !== requestedUserId) throw new Error("积分账户与当前用户不一致");
setSnapshot({ userId: requestedUserId, availableMicrocredits: wallet.account.availableMicrocredits, refreshing: false });
} catch (error) {
if (sequence !== requestSequence.current) return;
console.warn("积分余额刷新失败", error);
setSnapshot((current) => ({
userId: requestedUserId,
availableMicrocredits: current.userId === requestedUserId ? current.availableMicrocredits : null,
refreshing: false,
}));
}
}, [enabled, userId]);
}, [activeUserId]);
useEffect(() => {
if (!enabled || !userId) {
setAvailableMicrocredits(null);
if (!activeUserId) {
requestSequence.current += 1;
setSnapshot({ userId: "", availableMicrocredits: null, refreshing: false });
return;
}
void refresh();
@@ -39,12 +59,18 @@ export function useWalletBalance(userId?: string, enabled = true) {
window.addEventListener("wallet:updated", handleFocus);
document.addEventListener("visibilitychange", handleVisibility);
return () => {
requestSequence.current += 1;
window.clearInterval(timer);
window.removeEventListener("focus", handleFocus);
window.removeEventListener("wallet:updated", handleFocus);
document.removeEventListener("visibilitychange", handleVisibility);
};
}, [enabled, refresh, userId]);
}, [activeUserId, refresh]);
return { availableMicrocredits, refreshing, refresh };
const snapshotMatchesUser = snapshot.userId === activeUserId;
return {
availableMicrocredits: snapshotMatchesUser ? snapshot.availableMicrocredits : null,
refreshing: snapshotMatchesUser ? snapshot.refreshing : Boolean(activeUserId),
refresh,
};
}

View File

@@ -0,0 +1,11 @@
import { QueryClient } from "@tanstack/react-query";
export const appQueryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: false,
refetchOnWindowFocus: false,
},
},
});

View File

@@ -1,5 +1,6 @@
import { getSystemChannels, type AuthSessionPayload } from "@/services/api/auth";
import { localForageStorage } from "@/lib/localforage-storage";
import { appQueryClient } from "@/lib/query-client";
import { scopedLocalStorage, setActiveUserScope } from "@/lib/user-scope";
import { CANVAS_STORE_KEY, flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { ASSET_STORE_KEY, useAssetStore } from "@/stores/use-asset-store";
@@ -8,8 +9,12 @@ import { useUserStore } from "@/stores/use-user-store";
import { installRemoteUserDataAutoSync, resetRemoteUserDataSync, syncRemoteUserData } from "@/services/user-data-sync";
export async function applyUserSession(payload: AuthSessionPayload) {
const previousUserId = useUserStore.getState().user?.id || "";
const nextUserId = payload.user?.id || "";
useUserStore.getState().setHydrated(false);
try {
// Query key 不携带用户 ID;身份变化时必须取消并清空旧账号请求,避免跨账号复用内存数据。
if (previousUserId !== nextUserId) appQueryClient.clear();
resetRemoteUserDataSync();
await flushCanvasStorePersistence();
setActiveUserScope(payload.user?.id);