/** * API 客户端配置 * 封装 Axios 实例,配置拦截器和 Token 管理 */ import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"; import { message } from "antd"; import { useAuthStore } from "@/store/authStore"; import { refreshAccessToken } from "./auth"; // 创建 Axios 实例 const apiClient = axios.create({ baseURL: "/api/v1", timeout: 10000, headers: { "Content-Type": "application/json", }, }); // ── Token 刷新队列 ───────────────────────────────────────── let isRefreshing = false; let failedQueue: Array<{ resolve: (value?: unknown) => void; reject: (reason?: unknown) => void; }> = []; const processQueue = (error: unknown | null, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; // 请求拦截器:添加 Token apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = localStorage.getItem("access_token"); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error: AxiosError) => { return Promise.reject(error); }, ); // 响应拦截器:统一错误提示 + 401 自动刷新 Token apiClient.interceptors.response.use( (response) => response, async ( error: AxiosError<{ detail?: string; message?: string; msg?: string }>, ) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean; }; // 401 → 尝试刷新 Token if ( error.response?.status === 401 && originalRequest && !originalRequest._retry ) { const refreshToken = useAuthStore.getState().refreshToken; // 无 refresh_token → 直接登出 if (!refreshToken) { useAuthStore.getState().clearAuth(); window.location.href = "/"; return Promise.reject(error); } // 已在刷新中 → 排队等待 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then((token) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } return apiClient(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; try { const data = await refreshAccessToken(refreshToken); const newAccessToken = data.access_token; const newRefreshToken = data.refresh_token ?? refreshToken; // 更新 Zustand + localStorage useAuthStore .getState() .setAuth( useAuthStore.getState().user!, newAccessToken, newRefreshToken, ); // 处理排队的请求 processQueue(null, newAccessToken); // 重试原始请求 if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; } return apiClient(originalRequest); } catch (refreshError) { // 刷新失败 → 登出 processQueue(refreshError, null); useAuthStore.getState().clearAuth(); window.location.href = "/"; return Promise.reject(refreshError); } finally { isRefreshing = false; } } // 提取后端返回的错误信息(detail / message / msg) // 注意:后端返回的字段可能是对象 {code, message} 而非字符串,需要安全提取 const data = error.response?.data; const rawServerMsg = data?.detail || data?.message || data?.msg; // 安全提取字符串:递归处理嵌套对象(后端可能返回 {code, message: {code, message}} 等) const safeExtractString = (val: unknown): string => { if (typeof val === "string") return val; if (typeof val === "object" && val !== null) { const obj = val as Record; if (typeof obj.message === "string") return obj.message; if (typeof obj.msg === "string") return obj.msg; if (typeof obj.detail === "string") return obj.detail; // 嵌套对象:递归提取 if (typeof obj.message === "object" && obj.message !== null) return safeExtractString(obj.message); if (typeof obj.msg === "object" && obj.msg !== null) return safeExtractString(obj.msg); return JSON.stringify(val); } return ""; }; const serverMsg = safeExtractString(rawServerMsg); let handled = false; if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) { message.error("请求超时,请检查网络后重试"); handled = true; } else if (!error.response) { message.error("网络连接异常,请检查网络设置"); handled = true; } else if (serverMsg) { message.error(serverMsg); handled = true; } else { const status = error.response?.status; if (status === 413) { message.error("文件过大,请缩小后重试"); handled = true; } else if (status === 415) { message.error("不支持的文件格式"); handled = true; } else if (status === 503) { message.error("服务暂不可用,请稍后再试"); handled = true; } else if (status && status >= 500) { message.error("服务器繁忙,请稍后再试"); handled = true; } // 其他 4xx 且无具体信息时不弹通用提示,由各组件自行处理 } // 标记已展示过提示,组件 onError 可据此跳过重复 toast if (handled) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (error as any).__msgShown = true; } return Promise.reject(error); }, ); export default apiClient;