import { useCallback, useEffect, useRef } from 'react'; import apiClient from '../api/client'; import { getHealth } from '../api/health'; const HEALTH_POLL_INTERVAL_MS = 5 * 60 * 1000; const STARTUP_RETRY_DELAYS_MS = [750, 1500, 3000]; const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); const getErrorStatus = (error) => Number(error?.response?.status || 0); const isTransientApiError = (error) => { const status = getErrorStatus(error); return !status || status === 408 || status === 429 || status >= 500; }; const requestWithTransientRetry = async ( requestFn, { delays = STARTUP_RETRY_DELAYS_MS, shouldRetry = isTransientApiError, } = {}, ) => { let lastError = null; const retryDelays = Array.isArray(delays) ? delays : []; for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) { try { return await requestFn(); } catch (error) { lastError = error; if (attempt >= retryDelays.length || !shouldRetry(error)) { throw error; } await sleep(retryDelays[attempt]); } } throw lastError; }; export default function useAppAuthLifecycle({ clearRadarSearchResults, radarSearchRequestSeqRef, aoeLayerRef, activeLayersRef, radarPreviewLayersRef, setHasRadarSearched, setCurrentUser, setAuthChecked, setPendingTaskIds, setHealthLoading, setHealthError, setHealthStatus, setAoiLayer, setAllData, setRadarPagination, }) { const authGenerationRef = useRef(0); const fetchCurrentUser = useCallback(async (options = {}) => { const { clearOnFailure = true } = options; const requestGeneration = authGenerationRef.current; try { const response = await requestWithTransientRetry(() => ( apiClient.get('/auth/me', { skipAuthReset: !clearOnFailure, authGeneration: requestGeneration, }) )); if (requestGeneration === authGenerationRef.current) { setCurrentUser(response.data || null); } } catch (error) { if ( clearOnFailure && getErrorStatus(error) === 401 && requestGeneration === authGenerationRef.current ) { setCurrentUser(null); } } finally { if (requestGeneration === authGenerationRef.current || !clearOnFailure) { setAuthChecked(true); } } }, [setCurrentUser, setAuthChecked]); const handleLoginSuccess = useCallback(async (userFromLogin = null) => { authGenerationRef.current += 1; if (userFromLogin) { setCurrentUser(userFromLogin); setAuthChecked(true); void fetchCurrentUser({ clearOnFailure: false }); return; } await fetchCurrentUser({ clearOnFailure: !userFromLogin }); }, [fetchCurrentUser, setCurrentUser, setAuthChecked]); const handleLogout = useCallback(async () => { authGenerationRef.current += 1; try { await apiClient.post('/auth/logout'); } catch (error) { console.error('Logout failed:', error); } finally { radarSearchRequestSeqRef.current += 1; setHasRadarSearched(false); clearRadarSearchResults(); setCurrentUser(null); setPendingTaskIds([]); setAuthChecked(true); } }, [ clearRadarSearchResults, radarSearchRequestSeqRef, setHasRadarSearched, setCurrentUser, setPendingTaskIds, setAuthChecked, ]); const fetchHealthStatus = useCallback(async (options = {}) => { const { refresh = false, silent = false } = options; try { if (!silent) { setHealthLoading(true); } setHealthError(''); const data = await requestWithTransientRetry( () => getHealth(refresh ? { refresh: true } : {}), { delays: silent ? [] : STARTUP_RETRY_DELAYS_MS.slice(0, 2) }, ); setHealthStatus(data || null); } catch (error) { setHealthError(error.response?.data?.detail || '运维自检失败'); setHealthStatus(null); } finally { if (!silent) { setHealthLoading(false); } } }, [setHealthLoading, setHealthError, setHealthStatus]); useEffect(() => { const interceptorId = apiClient.interceptors.response.use( (response) => response, (error) => { const requestGeneration = error?.config?.authGeneration; const staleAuthFailure = typeof requestGeneration === 'number' && requestGeneration < authGenerationRef.current; if (error?.response?.status === 401 && !error?.config?.skipAuthReset && !staleAuthFailure) { radarSearchRequestSeqRef.current += 1; setHasRadarSearched(false); if (aoeLayerRef.current) { aoeLayerRef.current.remove(); aoeLayerRef.current = null; } setAoiLayer(null); Object.values(activeLayersRef.current).forEach((layer) => layer.remove()); activeLayersRef.current = {}; Object.values(radarPreviewLayersRef.current).forEach((layer) => layer.remove()); radarPreviewLayersRef.current = {}; setAllData([]); setRadarPagination((prev) => ({ ...prev, total: 0, offset: 0, hasMore: false, })); setCurrentUser(null); setAuthChecked(true); } return Promise.reject(error); } ); return () => { apiClient.interceptors.response.eject(interceptorId); }; }, [ radarSearchRequestSeqRef, setHasRadarSearched, aoeLayerRef, setAoiLayer, activeLayersRef, radarPreviewLayersRef, setAllData, setRadarPagination, setCurrentUser, setAuthChecked, ]); useEffect(() => { fetchCurrentUser(); }, [fetchCurrentUser]); useEffect(() => { const startupTimer = setTimeout(() => { void fetchHealthStatus({ silent: true }); }, 1500); const interval = setInterval(() => { void fetchHealthStatus({ silent: true }); }, HEALTH_POLL_INTERVAL_MS); return () => { clearTimeout(startupTimer); clearInterval(interval); }; }, [fetchHealthStatus]); return { handleLoginSuccess, handleLogout, fetchHealthStatus, }; }