import axios, { AxiosError, AxiosRequestConfig } from 'axios' import { Message } from '@arco-design/web-vue' import router from '@/router' import { OpenAPI, clearAuthToken, OmsLoginType } from './index' import { useRbacUserStore } from '@/store/modules/rbac-user' import { resolvePostLogoutLogin } from '@/utils/login-portal' // 创建自定义 axios 实例 // baseURL 为空;远端由 OpenAPI.BASE / omsUrl 按 env 拼接(本地走 Vite 代理到测试环境) export const axiosInstance = axios.create({ baseURL: '', // 始终为空,URL 由 OpenAPI.BASE + path 构建 timeout: 30000, headers: { 'Content-Type': 'application/json' } }) // 是否正在刷新 Token let isRefreshing = false // 等待刷新的请求队列 let refreshSubscribers: ((token: string) => void)[] = [] // 添加请求到等待队列 function subscribeTokenRefresh(callback: (token: string) => void) { refreshSubscribers.push(callback) } // 通知所有等待的请求 function onTokenRefreshed(token: string) { refreshSubscribers.forEach(callback => callback(token)) refreshSubscribers = [] } function redirectToLoginPage( userStore: ReturnType, loginType?: OmsLoginType | null ) { const current = router.currentRoute.value const target = resolvePostLogoutLogin({ loginType: loginType !== undefined ? loginType : userStore.resolveLoginTypeForRedirect(), path: current.fullPath, }) if (target.path) { router.replace({ path: target.path, query: target.query, }) return } router.replace({ name: target.name, query: target.query, }) } function logoutAndRedirectToLogin(userStore: ReturnType, message: string) { const loginType = userStore.resolveLoginTypeForRedirect() userStore.logout() Message.error(message) redirectToLoginPage(userStore, loginType) } // 请求拦截器 axiosInstance.interceptors.request.use( async (config: AxiosRequestConfig) => { const userStore = useRbacUserStore() // 确保 headers 存在 if (!config.headers) { config.headers = {} } const requestUrl = String(config.url || '') // COS 预签名只签 host;带 Bearer 会 CORS 预检失败 / 签名校验失败 const isCosAsset = /^https?:\/\/[^/]*cos\./i.test(requestUrl) || /myqcloud\.com/i.test(requestUrl) || requestUrl.startsWith('/__cos__/') // 优先使用 userStore 的 accessToken(最可靠) let token: string | undefined if (!isCosAsset) { if (userStore.accessToken) { token = userStore.accessToken } else if (OpenAPI.TOKEN) { // 兼容:从 OpenAPI 配置中获取 token token = typeof OpenAPI.TOKEN === 'function' ? await OpenAPI.TOKEN({} as any) : OpenAPI.TOKEN } } // 如果找到了 token,设置 Authorization header if (token) { // 如果token已经包含Bearer前缀,直接使用;否则添加Bearer前缀 if (typeof token === 'string' && token.startsWith('Bearer ')) { config.headers.Authorization = token } else if (typeof token === 'string' && token.startsWith('Bearer_')) { // 处理 Bearer_xxx 格式,转换为 Bearer xxx config.headers.Authorization = `Bearer ${token.replace('Bearer_', '')}` } else { config.headers.Authorization = `Bearer ${token}` } } else { // COS 资源或无 token:确保不带 Authorization if (isCosAsset && config.headers) { delete (config.headers as any).Authorization delete (config.headers as any).authorization } } // FormData 须由浏览器自动带 boundary,勿保留默认 application/json if (config.data instanceof FormData) { delete (config.headers as any)['Content-Type'] delete (config.headers as any)['content-type'] } // GET / 二进制下载勿带 application/json,避免多余 CORS 预检与中间件误判 const method = String(config.method || 'get').toLowerCase() if (method === 'get' || method === 'head' || isCosAsset) { delete (config.headers as any)['Content-Type'] delete (config.headers as any)['content-type'] } return config }, (error: AxiosError) => { return Promise.reject(error) } ) // 响应拦截器 axiosInstance.interceptors.response.use( (response) => { return response }, async (error: AxiosError) => { const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean silentError?: boolean } const userStore = useRbacUserStore() // 登录接口由页面内联 errorBox 展示,避免拦截器再弹 Message const requestUrl = String(originalRequest?.url || '') const isAuthLoginRequest = /\/rbac\/auth\/login(?:\?|$)/.test(requestUrl) const silentError = Boolean(originalRequest?.silentError) || isAuthLoginRequest if (error.response) { const status = error.response.status const data: any = error.response.data // 401:保活窗内(含 F5 从 lastActivity 恢复的 idle 窗)才允许续期 if (status === 401 && !originalRequest._retry) { if (!userStore.isKeepAliveActive()) { if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录') return Promise.reject(error) } // 如果没有 refreshToken,直接登出 if (!userStore.refreshToken) { if (!silentError) logoutAndRedirectToLogin(userStore, '未授权,请重新登录') return Promise.reject(error) } // 如果正在刷新,将请求加入队列 if (isRefreshing) { return new Promise(resolve => { subscribeTokenRefresh((token: string) => { if (!originalRequest.headers) { originalRequest.headers = {} } originalRequest.headers.Authorization = `Bearer ${token}` resolve(axiosInstance(originalRequest)) }) }) } originalRequest._retry = true isRefreshing = true try { const success = await userStore.refreshAccessToken() if (success) { // 刷新成功,重试原请求 onTokenRefreshed(userStore.accessToken) if (!originalRequest.headers) { originalRequest.headers = {} } originalRequest.headers.Authorization = `Bearer ${userStore.accessToken}` return axiosInstance(originalRequest) } else { // 刷新失败,登出 if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录') return Promise.reject(error) } } catch { if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录') return Promise.reject(error) } finally { isRefreshing = false } } if (silentError) { return Promise.reject(error) } switch (status) { case 401: // 如果走到这里,说明401没有被上面的逻辑处理(可能是没有retry标记的情况) logoutAndRedirectToLogin(userStore, '未授权,请重新登录') break case 403: Message.error('没有权限访问') break case 404: Message.error('请求的资源不存在') break case 500: Message.error(data?.message || '服务器错误') break default: Message.error(data?.message || '请求失败') } } else if (error.request) { if (!silentError) Message.error('网络错误,请检查网络连接') } else { if (!silentError) Message.error('请求配置错误') } return Promise.reject(error) } )