// ============================================================ // AppProvider — 全局应用状态 Context + Provider + Hook // // 导出: // AppContext, useAppContext() — Context + Consumer Hook // AppContextValue — 类型 // AppProvider — Provider 组件 // ============================================================ import { createContext, useContext, useState, useMemo, useCallback, useEffect, type ReactNode } from 'react'; import type { Platform, Edition } from '@shared/types'; import { getPlatform, isDev } from '@/utils/env'; import { safeIpcOn, safeIpcOff } from '@/utils/bus'; import { AuthAPI, type LoginResponseBody } from '@/services/modules/auth'; import { setToken, clearToken, initTokenStore } from '@/services/request'; import { emit, EVENTS } from '@/utils/bus'; import { logger } from '@/utils/bus'; import { getAutoLoginFlag, clearAutoLoginFlag } from '@/services/auth-persistence'; import { initAuthRefresh, getStoredRefreshToken, setStoredRefreshToken, clearStoredRefreshToken, initRefreshTokenStore, scheduleProactiveRefresh, clearProactiveRefresh, } from '@/services/auth-token'; import {initModelCache} from '@/services/cache/model-cache'; import {initStorage} from '@/infrastructure/storage'; // ---------- Context 类型 ---------- export interface AppContextValue { /** 当前操作系统平台 */ platform: Platform; /** 产品版本(个人版 / 企业版) */ edition: Edition; /** 用户是否已登录 */ isLoggedIn: boolean; /** 当前登录用户信息(未登录时为 null,结构与 LoginResponseBody 一致) */ user: LoginResponseBody | null; /** 登录:保存 Token 并更新登录态(auth 为登录接口返回值,异步加密存储) */ login: (auth: LoginResponseBody) => Promise; /** 退出登录:清除 Token 和用户状态 */ logout: () => void; /** 是否为开发环境 */ isDevMode: boolean; } export const AppContext = createContext(null); // ---------- Consumer Hook ---------- export function useAppContext(): AppContextValue { const ctx = useContext(AppContext); if (!ctx) { throw new Error('useAppContext() 必须在 内部调用'); } return ctx; } // ---------- Provider 组件 ---------- interface AppProviderProps { children: ReactNode; } export function AppProvider({ children }: AppProviderProps) { const [platform] = useState(getPlatform); const [isLoggedIn, setIsLoggedIn] = useState(false); const [user, setUser] = useState(null); /** * edition 从登录用户的 account_type 派生(运行时判断,无需构建变量) * * 映射关系: * "individual" → personal ← 个人用户 * "company" | "employee" → enterprise ← 企业用户 * null(未登录) → personal ← 默认最小功能集 * * 安全性:edition 是 useMemo 派生值,不可独立赋值; * 不写入 localStorage,重启/登出后回退默认值,必须重新登录才能确权。 */ const edition: Edition = useMemo(() => { if (!user) return 'personal'; return user.account_type === 'individual' ? 'personal' : 'enterprise'; }, [user]); // 登录/登出后同步窗口标题版控后缀到主进程 useEffect(() => { window.appRuntime?.setWindowEdition(isLoggedIn ? edition : null); }, [edition, isLoggedIn]); // ---------- 登录 / 退出 ---------- const login = useCallback(async (auth: LoginResponseBody) => { await setToken(auth.access_token); await setStoredRefreshToken(auth.refresh_token); // 启动主动刷新定时器(在 access_token 过期前自动续期) scheduleProactiveRefresh(auth.access_token); setUser(auth); setIsLoggedIn(true); logger.info('auth', 'User logged in', { userId: auth.user_id, username: auth.username, }); emit(EVENTS.LOGIN_SUCCESS, auth); }, []); const logout = useCallback(() => { logger.info('auth', 'User logged out'); clearToken(); clearStoredRefreshToken(); clearProactiveRefresh(); setUser(null); setIsLoggedIn(false); emit(EVENTS.LOGOUT); // 注意:退出登录不自动清理本地缓存 // —— 数据已按 user_id 隔离,同一用户重新登录后缓存仍可命中 // 用户主动清理请使用设置页面的「清理缓存」按钮 }, []); // ---------- 启动时初始化 token 存储 + 自动登录 ---------- // // 注意:React StrictMode(开发模式)会挂载→卸载→重新挂载以暴露副作用问题。 // aborted 标志确保卸载后不再执行异步回调,避免 refreshToken / getUserInfo 重复请求。 useEffect(() => { let aborted = false; // 从加密存储恢复 token + 模型缓存 到内存缓存 Promise.all([initTokenStore(), initRefreshTokenStore(), initModelCache(), initStorage()]).then(() => { if (aborted) return; // 初始化 401 刷新回调(token 缓存就绪后才注册) initAuthRefresh(); // 自动登录(refresh_token 续期) if (!getAutoLoginFlag()) return; const refreshToken = getStoredRefreshToken(); if (!refreshToken) return; AuthAPI.refreshToken({ refresh_token: refreshToken }) .then(async (res) => { if (aborted) return; await setToken(res.access_token); await setStoredRefreshToken(res.refresh_token); scheduleProactiveRefresh(res.access_token); setIsLoggedIn(true); logger.info('auth', 'Auto-login succeeded'); // 补全用户信息 AuthAPI.getUserInfo().then((info) => { if (aborted) return; const u: LoginResponseBody = { access_token: res.access_token, refresh_token: res.refresh_token, token_type: res.token_type, expires_in: res.expires_in, refresh_expires_in: res.refresh_expires_in, user_id: info.id, username: info.username, role: info.role, email: info.email ?? '', balance_cent: info.balance_cent, account_type: info.account_type, display_name: info.display_name ?? '', company_id: info.company_id ?? '', real_name: info.real_name ?? '', phone: info.phone, license_code: '', admin_permissions: info.admin_permissions ?? [], }; setUser(u); emit(EVENTS.LOGIN_SUCCESS, u); }); }) .catch(() => { if (aborted) return; logger.warn('auth', 'Auto-login failed, refresh_token expired'); clearStoredRefreshToken(); // 仅清除自动登录标记,保留用户名供下次表单回填 clearAutoLoginFlag(); }); }); return () => { aborted = true; }; }, []); // ---------- 监听主进程推送的平台/版本信息 ---------- useEffect(() => { const handler = (_event: unknown, info: unknown) => { const data = info as { platform?: string; edition?: string } | undefined; if (data) { logger.debug('app', 'Received platform info from main process', data as Record); } }; safeIpcOn('platform-info', handler); return () => { safeIpcOff('platform-info', handler); }; }, []); const value: AppContextValue = { platform, edition, isLoggedIn, user, login, logout, isDevMode: isDev(), }; return {children}; }