import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios' import { useAccountStore } from '@/stores' import { eventBus, AppEvents } from '@/utils' import type { ApiResponse, InternalRequestConfig } from './types' import { createRefreshTokenInterceptor } from './refreshInterceptor' /** * 替换 URL 中的路径参数占位符 * * @example * resolvePathParams('/user/:id/posts', { id: 1 }) // → '/user/1/posts' */ function resolvePathParams(url: string, pathParams?: Record): string { if (!pathParams) return url return url.replace(/:(\w+)/g, (_, key) => { const value = pathParams[key] if (value === undefined) { throw new Error(`路径参数 "${key}" 缺失`) } return String(value) }) } /** * 基于 axios 封装的网络请求客户端 * * 不依赖任何 UI 库,错误通过事件总线发布,由外部订阅处理。 * * @example * ```ts * // GET 请求 * const res = await requestClient.get('/users', { page: 1 }) * * // POST 请求 * const res = await requestClient.post('/auth/login', { username, password }) * ``` */ class RequestClient { /** axios 实例 */ public instance: AxiosInstance /** 是否正在刷新 token */ isRefreshing = false /** 刷新期间等待队列(401 请求在刷新完成前暂存,刷新后用新 token 重试) */ refreshTokenQueue: Array<(token: string) => void> = [] constructor(config?: CreateAxiosDefaults) { this.instance = axios.create({ baseURL: '/api', timeout: 15000, headers: { 'Content-Type': 'application/json' }, ...config }) this.setupInterceptors() } // @ts-ignore /** 注册请求和响应拦截器 */ private setupInterceptors(): void { // === 请求拦截器 === this.instance.interceptors.request.use( (config) => { const reqConfig = config as InternalRequestConfig // 替换路径参数 :param → 实际值 if (reqConfig.pathParams && config.url) { config.url = resolvePathParams(config.url, reqConfig.pathParams) } // 跳过鉴权时不注入 saToken if (!reqConfig?.skipAuth) { const accountStore = useAccountStore() if (accountStore.accessToken) { config.headers.saToken = accountStore.accessToken } } return config }, (error) => { return Promise.reject(error) } ) // === 通用响应拦截器 === // 职责:解包 ApiResponse + 按 HTTP 状态码分类处理错误。 // HTTP 401 全部透传,由 refreshInterceptor 统一处理。 this.instance.interceptors.response.use( // @ts-expect-error 拦截器解包 axiosResponse → ApiResponse (response) => { const data = response.data as ApiResponse if (data.code === 0) { return data } // code !== 0 但 HTTP 2xx — 未改造端点或特殊场景,按业务错误处理 eventBus.emit(AppEvents.REQUEST_ERROR, { type: 'business', code: data.code, httpStatus: response.status, message: data.message || '请求失败' }) return Promise.reject(data) }, (error) => { const status = error.response?.status // HTTP 401 透传给刷新拦截器 if (status === 401) { throw error } // HTTP 400/403/404/409/405/500 — 从 body 提取 ApiResponse const body = error.response?.data as ApiResponse | undefined if (body && status) { eventBus.emit(AppEvents.REQUEST_ERROR, { type: 'business', code: body.code, httpStatus: status, message: body.message || `请求错误 (${status})` }) return Promise.reject(body) } // 网络异常(无 response) eventBus.emit(AppEvents.REQUEST_ERROR, { type: error.response ? 'http' : 'network', code: status, httpStatus: status, message: error.response ? `请求错误 (${status})` : '网络异常,请检查网络连接' }) return Promise.reject(error) } ) // === Token 刷新拦截器 === // 通用拦截器透传 401 → 此处统一处理刷新/重试/重新认证 const { onFulfilled, onRejected } = createRefreshTokenInterceptor({ client: this.instance, doReAuthenticate: async () => { const accountStore = useAccountStore() accountStore.logout() eventBus.emit(AppEvents.UNAUTHORIZED) }, doRefreshToken: () => { const accountStore = useAccountStore() return accountStore.refreshAccessToken() }, formatToken: (token: string) => token, isRefreshing: { get value() { return requestClient.isRefreshing }, set value(v: boolean) { requestClient.isRefreshing = v } }, refreshTokenQueue: this.refreshTokenQueue }) // 拦截器解包 axiosResponse → ApiResponse this.instance.interceptors.response.use(onFulfilled, onRejected) } /** * 通用请求方法 * * 适用于路径参数、混合参数等复杂场景。 */ request(config: InternalRequestConfig): Promise> { return this.instance.request(config) } /** GET 请求 */ get( url: string, params?: Record, config?: InternalRequestConfig ): Promise> { return this.instance.get(url, { params, ...config }) } /** POST 请求 */ post( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.post(url, data, config) } /** PUT 请求 */ put( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.put(url, data, config) } /** DELETE 请求 */ delete(url: string, config?: InternalRequestConfig): Promise> { return this.instance.delete(url, config) } /** PATCH 请求 */ patch( url: string, data?: unknown, config?: InternalRequestConfig ): Promise> { return this.instance.patch(url, data, config) } } /** 请求客户端单例 */ export const requestClient = new RequestClient()