fix(auth): 账号切换 - 切换用户后清空旧账号缓存并隔离积分响应
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
11
web/src/lib/query-client.ts
Normal file
11
web/src/lib/query-client.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user