import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'; import {toast} from '@/components/ui/use-toast'; import { API_BASE_URL, isDev } from '@/config/env'; import { maintenanceDetector } from '@/services/maintenanceDetector'; import store from '../store'; import { logout } from '../store/userSlice'; export interface Response { code: number; message: string; data: T; success: boolean; } export interface RequestOptions extends AxiosRequestConfig { retryCount?: number; retryDelay?: number; } // 默认请求配置 const defaultConfig: Partial = { retryCount: 3, retryDelay: 1000, timeout: 30000 }; // 创建请求实例 const request = axios.create({ baseURL: API_BASE_URL, timeout: 30000, withCredentials: true, headers: { 'Content-Type': 'application/json' } }); // 开发环境打印请求配置 if (isDev) { console.log('📡 API Base URL:', API_BASE_URL || '使用代理 (proxy)'); } const defaultErrorMessage = '操作失败'; // 创建请求配置 const createRequestConfig = (options?: Partial): RequestOptions => { return { ...defaultConfig, ...options }; }; const responseHandler = (response: AxiosResponse>) => { // 请求成功,记录到维护检测器 maintenanceDetector.recordSuccess(); const result = response.data; if (result.success && result.code === 200) { return result.data; } else { if (result.message != undefined) { toast({ title: '操作失败', description: result.message || defaultErrorMessage, variant: 'destructive' }); return Promise.reject(response); } return Promise.reject(response); } }; const errorHandler = (error: any) => { // 检测网络错误(后端完全不可达) if (!error.response) { // 记录失败到维护检测器 maintenanceDetector.recordFailure(); // 不显示toast,因为会跳转到维护页面 return Promise.reject(error); } if (axios.isCancel(error)) { return Promise.reject(error); } const status = error.response.status; // 检测严重的服务器错误(500+),可能是服务器故障或维护 if (status >= 500) { maintenanceDetector.recordFailure(); // 继续执行下面的错误处理,显示具体的错误信息 } let errorMessage = ''; switch (status) { case 401: // 登录已过期,清除所有本地存储并跳转到登录页 errorMessage = '登录已过期,请重新登录'; toast({ title: '登录已过期', description: errorMessage, variant: 'destructive' }); // 使用统一的 logout action 清除所有用户数据 store.dispatch(logout()); // 延迟跳转,确保提示能显示出来 setTimeout(() => { window.location.href = '/login'; }, 1000); break; case 403: errorMessage = '拒绝访问'; toast({ title: '访问被拒绝', description: errorMessage, variant: 'destructive' }); break; case 404: errorMessage = '请求错误,未找到该资源'; toast({ title: '请求错误', description: errorMessage, variant: 'destructive' }); break; case 500: errorMessage = '服务异常,请稍后再试'; toast({ title: '服务器错误', description: errorMessage, variant: 'destructive' }); break; default: errorMessage = '服务器异常,请稍后再试!'; toast({ title: '请求失败', description: errorMessage, variant: 'destructive' }); } return Promise.reject(error); }; request.interceptors.request.use( (config) => { // 检查是否处于维护模式 if (maintenanceDetector.isInMaintenanceMode()) { // 如果在维护模式且当前不在维护页面,跳转 if (window.location.pathname !== '/maintenance') { window.location.replace('/maintenance'); } // 取消请求,避免继续发送 return Promise.reject(new Error('System in maintenance mode')); } const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); request.interceptors.response.use(responseHandler, errorHandler); const http = { get: (url: string, config?: RequestOptions) => request.get(url, createRequestConfig(config)), post: (url: string, data?: any, config?: RequestOptions) => request.post(url, data, createRequestConfig(config)), put: (url: string, data?: any, config?: RequestOptions) => request.put(url, data, createRequestConfig(config)), delete: (url: string, config?: RequestOptions) => request.delete(url, createRequestConfig(config)), upload: (url: string, file: File, config?: RequestOptions) => { const formData = new FormData(); formData.append('file', file); return request.post(url, formData, createRequestConfig({ headers: {'Content-Type': 'multipart/form-data'}, ...config })); }, download: (url: string, filename?: string, config?: RequestOptions) => request.get(url, createRequestConfig({ responseType: 'blob', ...config })).then(response => { const blob = new Blob([response.data]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename || ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl); }) }; export default http;