import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import { ElMessage, ElMessageBox } from 'element-plus'; import { Session } from '/@/utils/storage'; import qs from 'qs'; import { getChangedFields } from '/@/utils/diffUtils'; import { handleModuleNotEnabled } from '/@/utils/assetSubscribe'; // 标记是否正在处理 token 过期,避免重复弹窗 let isHandlingTokenExpired = false; // 错误消息防抖:防止短时间内显示多个错误消息 let lastErrorTime = 0; const ERROR_MESSAGE_INTERVAL = 2000; // 2秒内只显示一个错误 const showErrorMessage = (message: string) => { const now = Date.now(); // 2秒内只显示一个错误消息(不管内容是否相同) if (now - lastErrorTime < ERROR_MESSAGE_INTERVAL) { return; // 跳过 } lastErrorTime = now; ElMessage.error(message); }; // ============================================================ // Axios 实例配置 // 地址配置见 .env.development 文件 // ============================================================ // 统一服务实例(端口8000)- 全部模块共用 const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_URL, timeout: 50000, headers: { 'Content-Type': 'application/json' }, paramsSerializer: { serialize(params) { return qs.stringify(params, { allowDots: true, arrayFormat: 'brackets' }); }, }, }); // token 过期处理函数 const handleTokenExpired = () => { if (isHandlingTokenExpired) return; isHandlingTokenExpired = true; ElMessageBox.alert('登录状态已过期,请重新登录', '提示', { confirmButtonText: '确定', showClose: false, closeOnClickModal: false, closeOnPressEscape: false, beforeClose: (action, _instance, done) => { if (action === 'confirm') { done(); performLogout(); } }, }) .then(() => { performLogout(); }) .catch(() => { performLogout(); }); }; // 执行退出登录操作 const performLogout = () => { Session.clear(); localStorage.clear(); isHandlingTokenExpired = false; // Hash 路由统一回登录页,避免跳到错误地址 setTimeout(() => { window.location.href = '/#/login'; }, 500); }; // 请求拦截器 const requestInterceptor = (config: InternalAxiosRequestConfig) => { // 检查 token 是否有效 const token = Session.get('token'); if (token) { // 可以在这里添加 token 有效性检查(如果需要) config.headers!['Authorization'] = `Bearer ${token}`; } // PUT 请求最小化传参处理 // 如果请求数据中包含 _originalData,则自动计算差异,只传递修改过的字段 if (config.method?.toLowerCase() === 'put' && config.data && typeof config.data === 'object') { const { _originalData, ...currentData } = config.data; if (_originalData && typeof _originalData === 'object') { // 获取 id 字段(必须保留) const idField = currentData.id || currentData.Id || currentData.ID; // 计算差异 const changedFields = getChangedFields(_originalData, currentData, { exclude: ['_originalData', 'id', 'Id', 'ID'], }); // 如果有变化,只传递 id + 变化的字段 if (Object.keys(changedFields).length > 0) { config.data = { id: idField, ...changedFields }; } else { // 没有变化,只传递 id config.data = { id: idField }; } console.log('[最小化传参] 原始字段数:', Object.keys(currentData).length, '-> 传递字段数:', Object.keys(config.data).length); } } return config; }; const requestErrorHandler = (error: any) => { return Promise.reject(error); }; // 响应拦截器 const responseInterceptor = (response: AxiosResponse) => { // 文件流响应直接返回 if ( response.config.responseType === 'blob' || response.headers['content-type']?.includes('application/zip') || response.headers['content-type']?.includes('application/octet-stream') ) { return response; } const res = response.data; const httpStatus = response.status; const code = res?.code; const message = res?.message; // 检查 token 相关错误 if ( httpStatus === 401 || code === 401 || message?.includes('token') || message === 'token is invalid' || message === 'token 解析失败' || message?.includes('decrypt error') ) { handleTokenExpired(); return Promise.reject(new Error('登录状态已过期')); } // 处理模块未开通错误 (403) // 跳过资产SKU查询接口,避免弹窗内部请求触发循环 const requestUrl = response.config.url || ''; if (code === 402 && !requestUrl.includes('/assets/asset/sku/')) { // 获取当前路由路径 const currentPath = window.location.hash.replace('#', '') || window.location.pathname; console.log('[request.ts] 检测到403错误,当前路径:', currentPath); handleModuleNotEnabled(currentPath); // 直接返回,不再显示错误消息 return Promise.reject(new Error('模块未开通')); } // 业务逻辑错误处理(排除403,因为上面已处理) if (code !== undefined && code !== 0 && code !== 200 && code !== 403) { const errorMsg = message || `请求失败(${code})`; showErrorMessage(errorMsg); return Promise.reject(new Error(errorMsg)); } return res; }; // 响应错误拦截器 const responseErrorHandler = (error: any) => { console.error('API请求错误:', error); if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) { showErrorMessage('请求超时,请检查网络连接'); return Promise.reject(new Error('请求超时')); } if (!error.response) { if (error.message === 'Network Error') { // ElMessage.error('网络连接错误,请检查网络设置'); } else { // ElMessage.error('网络异常,请检查连接'); } return Promise.reject(error); } const httpStatus = error.response.status; // 优先使用返回数据中的 message 字段 const responseMessage = error.response.data?.message; // 处理 HTTP 错误状态 const requestUrl = error.response.config?.url || ''; switch (httpStatus) { case 401: handleTokenExpired(); break; case 402: // 模块未开通处理,跳过SKU相关接口避免循环 if (!requestUrl.includes('/assets/asset/sku/') && !requestUrl.includes('getAssetAndSku')) { // 检查是否刚从开通页面返回(5秒内不再跳转) const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime'); const now = Date.now(); if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) { console.log('[responseErrorHandler] 刚完成开通,跳过402处理'); showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面'); return Promise.reject(new Error('模块开通中')); } const currentPath = window.location.hash.replace('#', '') || window.location.pathname; console.log('[responseErrorHandler] 检测到HTTP 402错误,当前路径:', currentPath); handleModuleNotEnabled(currentPath); return Promise.reject(new Error('模块未开通')); } showErrorMessage(responseMessage || '服务未开通'); break; case 403: showErrorMessage(responseMessage || '没有权限访问该资源'); break; case 404: showErrorMessage(responseMessage || '请求的资源不存在'); break; case 429: showErrorMessage(responseMessage || '请求过于频繁,请稍后再试'); handleTokenExpired(); break; case 500: showErrorMessage(responseMessage || '服务器内部错误'); break; case 502: showErrorMessage(responseMessage || '网关错误'); break; case 503: showErrorMessage(responseMessage || '服务不可用'); break; default: if (httpStatus >= 400) { showErrorMessage(responseMessage || `请求失败(${httpStatus})`); } } return Promise.reject(error); }; // 为实例添加拦截器 service.interceptors.request.use(requestInterceptor, requestErrorHandler); service.interceptors.response.use(responseInterceptor, responseErrorHandler); // 导出 export default service; export { showErrorMessage };