// ============================================================ // Axios 请求封装 // // 功能: // - 统一 BaseURL(环境变量 VITE_API_BASE_URL) // - 请求拦截器:自动附加 Token // - 响应拦截器:统一错误处理、业务码校验 // - 类型安全的 GET / POST / PUT / DELETE / UPLOAD // - 请求超时、取消请求支持 // ============================================================ import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig, } from 'axios'; import type { ApiResponse } from './types'; import { RequestError, RequestErrorType, RefreshError, isRefreshError } from './types'; import { emit, EVENTS } from '../utils/event-bus'; import { logger } from '../utils/logger'; import { getSafeStore, setSafeStore, clearSafeStore, initSafeStore } from '../utils/safe-storage'; // ---------- 配置 ---------- /** API 基础地址(可通过 Vite 环境变量覆盖) */ const BASE_URL = import.meta.env.VITE_API_BASE_URL; /** 请求超时时间(毫秒) */ const TIMEOUT = 30_000; /** localStorage 中 Token 的键名(密文由 safeStorage 保护) */ const TOKEN_KEY = 'ele-heixiu-token'; /** refresh token 接口路径(该接口 401 时不应触发 handle401,否则死循环) */ const REFRESH_TOKEN_URL = '/api/v1/auth/refresh'; /** 判断请求是否为 refresh token 接口 */ function isRefreshRequest(config: InternalAxiosRequestConfig): boolean { return config.url?.includes(REFRESH_TOKEN_URL) ?? false; } // ---------- Token 管理 ---------- /** * 启动时初始化 Token:从加密存储解密到内存缓存。 * 应在 AppProvider 挂载时调用(login/logout 之前)。 */ export async function initTokenStore(): Promise { await initSafeStore(TOKEN_KEY); } /** 获取当前内存缓存中的 Token(同步,高频安全) */ export function getToken(): string | null { return getSafeStore(TOKEN_KEY); } /** 设置 Token(异步加密存储 + 更新内存缓存) */ export async function setToken(token: string): Promise { await setSafeStore(TOKEN_KEY, token); } /** 清除 Token(同步清除内存缓存 + localStorage) */ export function clearToken(): void { clearSafeStore(TOKEN_KEY); } // ---------- Token 刷新(由外部注册,request.ts 不依赖任何 auth 模块) ---------- let refreshHandler: (() => Promise) | null = null; let isRefreshing = false; let refreshPromise: Promise | null = null; /** 等待刷新完成的请求队列 */ let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: Error) => void; }> = []; /** 由 auth 模块调用,注册 token 刷新回调 */ export function setTokenRefreshHandler(handler: () => Promise): void { refreshHandler = handler; } function processQueue(error: Error | null, token: string | null): void { failedQueue.forEach(({ resolve, reject }) => { if (error) reject(error); else resolve(token!); }); failedQueue = []; } // ---------- Axios 实例 ---------- const instance: AxiosInstance = axios.create({ baseURL: BASE_URL, timeout: TIMEOUT, headers: { 'Content-Type': 'application/json', }, }); // ---------- 请求拦截器 ---------- instance.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 自动附加 Token const token = getToken(); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }, () => { return Promise.reject(new RequestError(RequestErrorType.NETWORK, '请求配置失败')); }, ); // ---------- 401 → 刷新 Token + 重试 ---------- function handle401(config: InternalAxiosRequestConfig): Promise> { // 已经在刷新中 → 排队等待 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token) => { config.headers.Authorization = `Bearer ${token}`; resolve(instance(config)); }, reject, }); }); } // 没有注册刷新回调 → 直接要求重新登录 if (!refreshHandler) { clearToken(); emit(EVENTS.AUTH_REQUIRED); return Promise.reject(new RefreshError()); } // 开始刷新 isRefreshing = true; refreshPromise = refreshHandler(); return new Promise((resolve, reject) => { refreshPromise! .then((newToken) => { if (newToken) { processQueue(null, newToken); config.headers.Authorization = `Bearer ${newToken}`; resolve(instance(config)); } else { const err = new RefreshError(); processQueue(err, null); clearToken(); emit(EVENTS.AUTH_REQUIRED); reject(err); } isRefreshing = false; refreshPromise = null; }) .catch((err) => { processQueue(err, null); isRefreshing = false; refreshPromise = null; clearToken(); emit(EVENTS.AUTH_REQUIRED); reject(new RefreshError()); }); }); } // ---------- 响应拦截器 ---------- instance.interceptors.response.use( (response: AxiosResponse) => { const { data } = response; // HTTP 200 但业务码非 0 → 业务错误 if (data && data.code !== 0) { // 401 → 尝试刷新 token 并重试(但 refresh 接口自身 401 必须直接拒绝,否则死循环) if (data.code === 401 && response.config) { if (isRefreshRequest(response.config)) { return Promise.reject(new RefreshError()); } return handle401(response.config); } logger.warn('request', `Business error: ${data.message || '请求失败'}`, undefined, { url: response.config.url, method: response.config.method?.toUpperCase(), code: data.code, traceId: data.traceId, }); return Promise.reject( new RequestError(RequestErrorType.BUSINESS, data.message || '请求失败', { code: data.code, traceId: data.traceId, }), ); } return response; }, (error) => { // 提取请求上下文(所有日志共用) const reqCtx = { url: error.config?.url, method: error.config?.method?.toUpperCase(), }; // 全局捕获 RefreshError → 自动触发登录 Modal if (isRefreshError(error)) { logger.debug('request', 'Auth expired (RefreshError caught)', { ...reqCtx, errorType: error.name, errorMessage: error.message, }); clearToken(); emit(EVENTS.AUTH_REQUIRED); return Promise.reject(error); } // 请求已被取消 if (axios.isCancel(error)) { const cancelErr = new RequestError(RequestErrorType.CANCELLED, '请求已取消'); logger.debug('request', 'Request cancelled', reqCtx); return Promise.reject(cancelErr); } // 超时 if (error.code === 'ECONNABORTED') { const timeoutErr = new RequestError(RequestErrorType.TIMEOUT, '请求超时,请检查网络'); logger.warn('request', 'Request timeout', timeoutErr, reqCtx); return Promise.reject(timeoutErr); } // 无网络 if (!error.response) { const netErr = new RequestError(RequestErrorType.NETWORK, '网络异常,请检查连接'); logger.warn('request', 'Network error', netErr, reqCtx); return Promise.reject(netErr); } // HTTP 状态码错误 const { status, data } = error.response; // 401 → 尝试刷新 token 并重试(refresh 接口自身 401 直接拒绝,防死循环) if (status === 401 && error.config) { if (isRefreshRequest(error.config)) { const refreshErr = new RefreshError(); logger.debug('request', 'Refresh endpoint returned 401', { ...reqCtx, errorType: refreshErr.name, }); return Promise.reject(refreshErr); } return handle401(error.config); } let errMsg = '请求失败'; switch (status) { case 400: errMsg = '请求参数有误'; break; case 403: errMsg = '没有访问权限'; break; case 404: errMsg = '请求的资源不存在'; break; case 500: errMsg = '服务器内部错误'; break; case 502: errMsg = '网关错误'; break; case 503: errMsg = '服务暂时不可用'; break; } const httpErr = new RequestError(RequestErrorType.HTTP, data?.message || errMsg, { httpStatus: status, code: data?.code, }); // HTTP 5xx → error 级别,4xx → warn 级别 if (status >= 500) { logger.error('request', `HTTP ${status}: ${errMsg}`, httpErr, reqCtx); } else { logger.warn('request', `HTTP ${status}: ${errMsg}`, httpErr, reqCtx); } return Promise.reject(httpErr); }, ); // ---------- 请求方法 ---------- /** * 发起 GET 请求 * @param url - 请求路径 * @param params - 查询参数 * @param config - Axios 配置覆盖 */ export async function get( url: string, params?: Record, config?: AxiosRequestConfig, ): Promise { const res = await instance.get>(url, { ...config, params }); return res.data.data; } /** * 发起 POST 请求(JSON) * @param url - 请求路径 * @param data - 请求体 * @param config - Axios 配置覆盖 */ export async function post( url: string, data?: unknown, config?: AxiosRequestConfig, ): Promise { const res = await instance.post>(url, data, config); return res.data.data; } /** * 发起 PUT 请求(JSON) */ export async function put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { const res = await instance.put>(url, data, config); return res.data.data; } /** * 发起 DELETE 请求 */ export async function del(url: string, config?: AxiosRequestConfig): Promise { const res = await instance.delete>(url, config); return res.data.data; } /** * 文件上传(multipart/form-data) * @param url - 上传接口路径 * @param formData - FormData 对象 * @param onProgress - 上传进度回调 (0~100) */ export async function upload( url: string, formData: FormData, onProgress?: (percent: number) => void, ): Promise { const res = await instance.post>(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 120000, // 上传超时 2 分钟 onUploadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); onProgress(percent); } }, }); return res.data.data; } /** * 文件下载 * @param url - 下载接口路径 * @param filename - 保存的文件名 */ export async function download(url: string, filename?: string): Promise { const res = await instance.get(url, { responseType: 'blob', }); const blob = new Blob([res.data]); const downloadUrl = window.URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = downloadUrl; anchor.download = filename || 'download'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); window.URL.revokeObjectURL(downloadUrl); } /** * 创建可取消的请求控制器 * 用法: * const { controller, request } = createCancellableRequest(); * request(get('/some/url')); // 发起请求 * controller.abort(); // 取消请求 */ export function createCancellableRequest() { const controller = new AbortController(); return { controller, signal: controller.signal, /** 包裹一个请求 Promise,使其可通过 controller.abort() 取消 */ request: (promise: Promise): Promise => { return new Promise((resolve, reject) => { promise.then(resolve).catch(reject); controller.signal.addEventListener('abort', () => { reject(new RequestError(RequestErrorType.CANCELLED, '请求已取消')); }); }); }, }; } // ---------- 导出 axios 实例(供高级场景直接使用) ---------- export { instance as axiosInstance }; export { RequestError, RequestErrorType };