/** * Mono 级别的 C 端 Axios 实例 * 供 monorepo 中所有应用使用的统一 HTTP 客户端 */ import axios from 'axios' // 直接使用实例(最简单、最可靠) import tokenManager from '@gold/utils/token-manager' /** * 不需要 token 的接口白名单 */ const WHITE_LIST = [ '/auth/login', '/auth/send-sms-code', '/auth/sms-login', '/auth/validate-sms-code', '/auth/register', '/auth/reset-password', '/auth/refresh-token', ] /** * 检查 URL 是否在白名单中 */ function isInWhiteList(url) { return !!(url && WHITE_LIST.some(path => url.includes(path))) } /** * 自动刷新 token 的锁机制和队列 */ let isRefreshing = false let refreshSubscribers = [] /** * 订阅 token 刷新完成 * @param {Function} callback - 回调函数 */ function subscribeTokenRefresh(callback) { if (isRefreshing) { refreshSubscribers.push(callback) } else { callback() } } /** * 执行所有订阅回调 */ function onRefreshed() { refreshSubscribers.forEach(callback => callback()) refreshSubscribers = [] } /** * 处理 401 未授权错误 * 注意:只做清理工作,不处理重定向(重定向由上层回调处理) */ let isHandling401 = false let handling401Timeout = null function handle401Error(error) { // 防止重复处理 if (isHandling401 || error?._handled) { return } isHandling401 = true error && (error._handled = true) try { tokenManager.clearTokens() const message = error?.needRelogin ? '[HTTP] Token已清空,refreshToken无效需要重新登录' : '[HTTP] Token已清空,因401错误' console.warn(message) } catch (e) { console.error('[HTTP] 清空 token 失败:', e) } if (handling401Timeout) { clearTimeout(handling401Timeout) } handling401Timeout = setTimeout(() => { isHandling401 = false }, 3000) } /** * 创建 C 端 Axios 实例 * @param {Object} options - 配置选项 * @param {string} options.baseURL - 基础 URL * @param {number} options.timeout - 超时时间(毫秒) * @param {Function} options.on401 - 401 错误处理函数 * @param {Function} options.on403 - 403 错误处理函数 * @param {Function} options.refreshTokenFn - Token 刷新函数(可选) * @returns {AxiosInstance} Axios 实例 */ export function createClientAxios(options = {}) { const { baseURL = '/', timeout = 180000, on401 = handle401Error, on403 = null, refreshTokenFn = null, } = options const client = axios.create({ baseURL, timeout, }) // 请求拦截器 client.interceptors.request.use((config) => { // 添加 tenant-id const tenantId = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) || (typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) || '1' if (tenantId) { config.headers['tenant-id'] = tenantId } // 检查是否需要认证 const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '') if (!needToken) { return config } // 检查 token 是否即将过期(30秒缓冲) const BUFFER_TIME = 30 * 1000 const currentToken = tokenManager.getAccessToken() if (!currentToken) { console.warn('[Token] 没有可用的 accessToken') return config } const isTokenExpired = tokenManager.isExpired(BUFFER_TIME) if (!isTokenExpired) { // Token 未过期,直接添加 Authorization 头 const authHeader = tokenManager.getAuthHeader() if (authHeader) { config.headers.Authorization = authHeader } return config } // Token 即将过期,需要刷新 console.info('[Token] Token刷新') // 如果不在刷新过程中,启动刷新 if (!isRefreshing) { isRefreshing = true if (!refreshTokenFn || typeof refreshTokenFn !== 'function') { console.warn('[Token] 未提供刷新函数,跳过刷新') isRefreshing = false onRefreshed() return config } refreshTokenFn() .then(() => { console.info('[Token] 刷新成功') isRefreshing = false onRefreshed() }) .catch((error) => { isRefreshing = false onRefreshed() console.error('[Token] 刷新失败:', error.message) if (error.code === 401 && error.needRelogin) { console.info('[Token] refreshToken无效,需要重新登录') } }) } // 等待刷新完成 return new Promise((resolve) => { subscribeTokenRefresh(() => { const authHeader = tokenManager.getAuthHeader() if (authHeader) { config.headers.Authorization = authHeader } resolve(config) }) }) }) // 响应拦截器 client.interceptors.response.use( (response) => { const data = response.data // 检查业务状态码 if (data && typeof data.code === 'number') { if (data.code === 0 || data.code === 200) { return data } // 创建业务错误对象 const error = new Error(data?.message || data?.msg || '请求失败') error.code = data?.code error.data = data // 业务码 403 只在响应拦截器处理,避免重复处理 if (data.code === 403 && typeof on403 === 'function') { const forbiddenError = new Error('权限不足,无法访问该资源') forbiddenError.code = 403 on403(forbiddenError) } return Promise.reject(error) } return data }, (error) => { const status = error.response?.status if (status === 401 && typeof on401 === 'function') { on401(error) } else if (status === 403 && typeof on403 === 'function') { const forbiddenError = new Error('权限不足,无法访问该资源') forbiddenError.code = 403 on403(forbiddenError) } return Promise.reject(error) } ) return client } /** * 默认导出的 C 端 Axios 实例 * 可在应用层覆盖配置 */ export const clientAxios = createClientAxios() export default clientAxios