/** * 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 // 排除 auth 端点:登录/注册/找回密码的 401 是正常业务响应(如密码错误), // 不应触发 token 刷新或登出跳转,走后面的错误提示逻辑即可 const requestUrl = originalRequest?.url || "" const isAuthEndpoint = requestUrl.includes("/auth/login") || requestUrl.includes("/auth/register") || requestUrl.includes("/auth/forgot-password") || requestUrl.includes("/auth/reset-password") if ( error.response?.status === 401 && originalRequest && !originalRequest._retry && !isAuthEndpoint ) { 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) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定 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