import axios from 'axios'; import { message as antMessage } from 'antd'; // 请求缓存:短时间内相同请求复用结果 interface CacheEntry { data: unknown; timestamp: number; } const requestCache = new Map(); const CACHE_TTL = 5000; // 5 秒缓存 function getCacheKey(config: { url?: string; params?: unknown; method?: string }): string { return `${config.method || 'get'}:${config.url || ''}:${JSON.stringify(config.params || {})}`; } const defaultAdapter = axios.getAdapter(axios.defaults.adapter); const client = axios.create({ baseURL: '/api/v1', timeout: 10000, headers: { 'Content-Type': 'application/json' }, adapter: (config) => { // GET 请求检查缓存 if (config.method === 'get' && config.url) { const key = getCacheKey(config); const entry = requestCache.get(key); if (entry && Date.now() - entry.timestamp < CACHE_TTL) { return Promise.resolve({ data: entry.data, status: 200, statusText: 'OK (cached)', headers: {} as any, config, }); } } return defaultAdapter(config); }, }); // Decode JWT payload without external library function decodeJwtPayload(token: string): { exp?: number } | null { try { const parts = token.split('.'); if (parts.length !== 3) return null; const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload; } catch { return null; } } // Check if token is expired or about to expire (within 30s buffer) function isTokenExpiringSoon(token: string): boolean { const payload = decodeJwtPayload(token); if (!payload?.exp) return true; return Date.now() / 1000 > payload.exp - 30; } // Request interceptor: attach access token + proactive refresh client.interceptors.request.use(async (config) => { const token = localStorage.getItem('access_token'); if (token) { // If token is about to expire, proactively refresh before sending the request if (isTokenExpiringSoon(token)) { const refreshToken = localStorage.getItem('refresh_token'); if (refreshToken && !isRefreshing) { isRefreshing = true; try { const { data } = await axios.post('/api/v1/auth/refresh', { refresh_token: refreshToken, }); const newAccess = data.data.access_token; const newRefresh = data.data.refresh_token; localStorage.setItem('access_token', newAccess); localStorage.setItem('refresh_token', newRefresh); processQueue(null, newAccess); config.headers.Authorization = `Bearer ${newAccess}`; return config; } catch { processQueue(new Error('refresh failed'), null); // Continue with old token, let 401 handler deal with it } finally { isRefreshing = false; } } } config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:缓存 GET 响应 + 自动刷新 token client.interceptors.response.use( (response) => { // 缓存 GET 响应 if (response.config.method === 'get' && response.config.url) { const key = getCacheKey(response.config); requestCache.set(key, { data: response.data, timestamp: Date.now() }); } return response; }, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/login')) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return client(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; try { const refreshToken = localStorage.getItem('refresh_token'); if (!refreshToken) throw new Error('No refresh token'); const { data } = await axios.post('/api/v1/auth/refresh', { refresh_token: refreshToken, }); const newAccessToken = data.data.access_token; const newRefreshToken = data.data.refresh_token; localStorage.setItem('access_token', newAccessToken); localStorage.setItem('refresh_token', newRefreshToken); processQueue(null, newAccessToken); originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return client(originalRequest); } catch (refreshError) { processQueue(refreshError, null); localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); window.location.hash = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); // 全局错误提示(仅对未被组件处理的错误显示) let globalErrorTimer: ReturnType | null = null; function showGlobalError(msg: string) { // 防止短时间内弹出大量相同提示 if (globalErrorTimer) return; antMessage.error(msg, 3); globalErrorTimer = setTimeout(() => { globalErrorTimer = null; }, 3000); } // 全局错误拦截 — 在响应拦截器之后、组件 catch 之前执行 client.interceptors.response.use( (response) => response, (error) => { if (!error.response) { showGlobalError('网络连接异常,请检查网络'); } else if (error.response.status === 403) { showGlobalError('权限不足,无法执行此操作'); } else if (error.response.status === 404) { // 404 通常由组件自行处理(如跳转),不全局提示 } else if (error.response.status >= 500) { showGlobalError('服务器异常,请稍后重试'); } return Promise.reject(error); } ); let isRefreshing = false; let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: unknown) => void; }> = []; function processQueue(error: unknown, token: string | null) { failedQueue.forEach(({ resolve, reject }) => { if (token) resolve(token); else reject(error); }); failedQueue = []; } // 清除缓存(登录/登出时调用) export function clearApiCache() { requestCache.clear(); } // 通用错误处理:提取后端错误消息并展示 export function handleApiError(err: unknown, fallback = '操作失败'): string { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || fallback; antMessage.error(msg); return msg; } export default client;