import axios, { type AxiosRequestConfig } from "axios"; // ============================================================ // Token 存储 Key // ============================================================ const ACCESS_TOKEN_KEY = "track_admin_token"; const REFRESH_TOKEN_KEY = "track_admin_refresh_token"; const USER_KEY = "track_admin_user"; // ============================================================ // Token 读写工具 // ============================================================ export function getAccessToken(): string | null { return localStorage.getItem(ACCESS_TOKEN_KEY); } export function getRefreshToken(): string | null { return localStorage.getItem(REFRESH_TOKEN_KEY); } export function setTokens(accessToken: string, refreshToken: string) { localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); } export function clearAuth() { localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(USER_KEY); } // ============================================================ // Axios 实例 // ============================================================ const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, headers: { "Content-Type": "application/json", }, }); // ============================================================ // 请求拦截器 — 注入 Access Token // ============================================================ api.interceptors.request.use( (config) => { const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); // ============================================================ // 响应拦截器 — 双 Token 无感刷新 + 并发请求队列 // ============================================================ let isRefreshing = false; let retryQueue: Array<{ resolve: (token: string) => void; reject: (error: unknown) => void; }> = []; /** 处理队列中的所有挂起请求 */ function processQueue(error: unknown, token: string | null) { retryQueue.forEach((p) => { if (token) { p.resolve(token); } else { p.reject(error); } }); retryQueue = []; } api.interceptors.response.use( (response) => response, async (error) => { const originalRequest: AxiosRequestConfig & { _retry?: boolean } = error.config; const status = error.response?.status; // 仅处理 401 if (status !== 401) { return Promise.reject(error); } // 跳过登录和刷新接口自身(避免死循环) const url = originalRequest.url ?? ""; if (url.includes("/auth/login") || url.includes("/auth/refresh")) { return Promise.reject(error); } // 避免对同一请求重复刷新 if (originalRequest._retry) { clearAuth(); if (window.location.pathname.startsWith("/admin")) { window.location.href = "/admin/login"; } return Promise.reject(error); } originalRequest._retry = true; // ---- 并发请求队列机制 ---- if (isRefreshing) { // 已有刷新进行中,加入队列等待 return new Promise((resolve, reject) => { retryQueue.push({ resolve, reject }); }).then((newToken) => { originalRequest.headers = originalRequest.headers || {}; originalRequest.headers.Authorization = `Bearer ${newToken}`; return api(originalRequest); }); } isRefreshing = true; try { const refreshToken = getRefreshToken(); if (!refreshToken) { throw new Error("无 Refresh Token"); } // 调用刷新接口 const { data } = await axios.post( `${import.meta.env.VITE_API_BASE_URL}/auth/refresh`, { refresh_token: refreshToken }, { headers: { "Content-Type": "application/json" } }, ); const newAccessToken: string = data.access_token; setTokens(newAccessToken, refreshToken); // 更新 Access Token // 重放队列中的所有请求 processQueue(null, newAccessToken); // 重试当前请求 originalRequest.headers = originalRequest.headers || {}; originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return api(originalRequest); } catch (refreshError) { // Refresh Token 也过期 — 彻底登出 processQueue(refreshError, null); clearAuth(); if (window.location.pathname.startsWith("/admin")) { window.location.href = "/admin/login"; } return Promise.reject(refreshError); } finally { isRefreshing = false; } }, ); export default api;