/** * Axios API 配置 * 统一管理 HTTP 请求 * * 功能特性: * - 自动添加 Token * - Token 自动刷新 * - 请求重试机制 * - 请求取消功能 * - 统一错误处理 * - 请求/响应日志 * - Loading 状态管理 */ import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, CancelTokenSource, AxiosRequestHeaders, } from 'axios'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { router } from 'expo-router'; import { config } from '../config'; import { transformRequest, parseResponse } from './helper'; import { cloneDeep, pick, includes } from 'lodash-es'; import md5 from 'md5'; /** * API 响应数据结构 */ export interface ApiResponse { code: number; message: string; data: T; success?: boolean; } /** * API 错误响应 */ export interface ApiError { code: number; message: string; errors?: Record; } /** * 请求配置扩展 */ export interface RequestConfig extends AxiosRequestConfig { /** 是否显示 loading */ showLoading?: boolean; /** 是否显示错误提示 */ showError?: boolean; /** 是否重试 */ retry?: boolean; /** 重试次数 */ retryCount?: number; /** 是否需要 token */ requiresAuth?: boolean; /** 自定义错误处理 */ customErrorHandler?: (error: AxiosError) => void; } // API 基础配置 const API_CONFIG = { baseURL: config.api.baseURL, timeout: config.api.timeout, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Accept: 'application/json, application/xml, text/play, text/html, *.*', }, }; // 创建 axios 实例 const api = axios.create(API_CONFIG); // 请求队列(用于取消请求) const pendingRequests = new Map(); // 是否正在刷新 token let isRefreshing = false; // 刷新 token 时的请求队列 let refreshSubscribers: Array<(token: string) => void> = []; /** * 生成请求唯一标识 */ function generateRequestKey(config: InternalAxiosRequestConfig): string { const cmdId = config.headers.cmdId || config.url; const data = cloneDeep(config.method === 'post' ? config.data : config.params); return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`; } /** * 添加请求到队列 */ function addPendingRequest(config: InternalAxiosRequestConfig): void { const requestKey = generateRequestKey(config); // 如果已存在相同请求,取消之前的请求 if (pendingRequests.has(requestKey)) { const source = pendingRequests.get(requestKey); source?.cancel('重复请求已取消'); } // 创建新的取消令牌 const source = axios.CancelToken.source(); config.cancelToken = source.token; pendingRequests.set(requestKey, source); } /** * 从队列中移除请求 */ function removePendingRequest(config: InternalAxiosRequestConfig | AxiosRequestConfig): void { const requestKey = generateRequestKey(config as InternalAxiosRequestConfig); pendingRequests.delete(requestKey); } /** * 订阅 token 刷新 */ function subscribeTokenRefresh(callback: (token: string) => void): void { refreshSubscribers.push(callback); } /** * 通知所有订阅者 token 已刷新 */ function onTokenRefreshed(token: string): void { refreshSubscribers.forEach((callback) => callback(token)); refreshSubscribers = []; } /** * 刷新 token */ async function refreshAccessToken(): Promise { try { const refreshToken = await AsyncStorage.getItem('refresh_token'); if (!refreshToken) { throw new Error('No refresh token'); } // 调用刷新 token 接口 const response = await axios.post>( `${config.api.baseURL}/auth/refresh-token`, { refreshToken } ); const { token, refreshToken: newRefreshToken } = response.data.data; // 保存新的 token await AsyncStorage.setItem('auth_token', token); await AsyncStorage.setItem('refresh_token', newRefreshToken); return token; } catch (error) { // 刷新失败,清除所有 token await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); return null; } } /** * 请求拦截器 * 在请求发送前添加 token 等信息 */ api.interceptors.request.use( async (config: InternalAxiosRequestConfig) => { try { // 添加到请求队列(防止重复请求) addPendingRequest(config); const { apiName } = config.headers; const { headers, data } = transformRequest(pick(config, ['headers', 'data'])); config.headers = { ...headers, ...(__DEV__ ? { apiName } : {}), } as AxiosRequestHeaders; config.data = data; if (Number(config.headers.cmdId) !== 381120) { config.url = '/v2/'; } // if (__DEV__ && apiName) { // config.url = `${config.url}?${apiName}`; // } // // 从本地存储获取 token // const token = await AsyncStorage.getItem('auth_token'); // // // 添加 token 到请求头 // if (token && config.headers) { // config.headers.Authorization = `Bearer ${token}`; // } // 添加请求时间戳(用于计算请求耗时) (config as any).metadata = { startTime: Date.now() }; // 打印请求信息(开发环境) if (__DEV__) { console.log('📤 API Request:', { method: config.method?.toUpperCase(), url: config.url, baseURL: config.baseURL, fullURL: `${config.baseURL}${config.url}`, params: config.params, headers: config.headers, }); } return config; } catch (error) { console.error('❌ Request interceptor error:', error); return Promise.reject(error); } }, (error) => { console.error('❌ Request error:', error); return Promise.reject(error); } ); /** * 响应拦截器 * 统一处理响应和错误 */ api.interceptors.response.use( async (response: AxiosResponse) => { // 从请求队列中移除 removePendingRequest(response.config); // 计算请求耗时 const duration = Date.now() - (response.config as any).metadata?.startTime; const resData: any = await parseResponse(response); // 打印响应信息(开发环境) // if (__DEV__) { // console.log('📥 API Response:', { // url: response.config.url, // status: response.status, // duration: `${duration}ms`, // data: response.data, // }); // } // 统一处理响应数据格式 // const apiResponse = response.data as ApiResponse; // 如果后端返回的数据结构包含 code 和 data // if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) { // // 检查业务状态码 // if (apiResponse.code !== 0 && apiResponse.code !== 200) { // // 业务错误 // const error = new Error(apiResponse.message || '请求失败') as any; // error.code = apiResponse.code; // error.response = response; // return Promise.reject(error); // } // // // 返回 data 字段 // return apiResponse.data; // } // 直接返回响应数据 // return response.data; return Promise.resolve(resData); }, async (error: AxiosError) => { // 从请求队列中移除 if (error.config) { removePendingRequest(error.config); } // 如果是取消的请求,直接返回 if (axios.isCancel(error)) { if (__DEV__) { console.log('🚫 Request cancelled:', error.message); } return Promise.reject(error); } const originalRequest = error.config as RequestConfig & { _retry?: boolean }; // 打印错误信息 if (__DEV__) { console.error('❌ API Error:', { method: error.config?.method, cmdId: error.config?.headers?.cmdId, status: error.response?.status, message: error.message, data: error.response?.data, }); } if (includes([500, 502, 503], error.response?.status) && includes(['371130'], `${error.config?.headers?.cmdId}`)) { router.replace('/maintenance' as any); } // 处理不同的错误状态码 if (error.response) { const { status, data } = error.response; switch (status) { case 401: { // Token 过期,尝试刷新 if (!originalRequest._retry) { if (isRefreshing) { // 如果正在刷新,将请求加入队列 return new Promise((resolve) => { subscribeTokenRefresh((token: string) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } resolve(api(originalRequest)); }); }); } originalRequest._retry = true; isRefreshing = true; try { const newToken = await refreshAccessToken(); if (newToken) { // Token 刷新成功 isRefreshing = false; onTokenRefreshed(newToken); // 重试原请求 if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${newToken}`; } return api(originalRequest); } else { // Token 刷新失败,跳转到登录页 isRefreshing = false; await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); // 跳转到登录页 if (router.canGoBack()) { router.replace('/(auth)/login' as any); } } } catch (refreshError) { isRefreshing = false; await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); // 跳转到登录页 if (router.canGoBack()) { router.replace('/(auth)/login' as any); } return Promise.reject(refreshError); } } break; } case 403: // 禁止访问 console.error('❌ 403: 没有权限访问该资源'); break; case 404: // 资源不存在 console.error('❌ 404: 请求的资源不存在'); break; case 422: // 表单验证错误 console.error('❌ 422: 表单验证失败', data); break; case 429: // 请求过于频繁 console.error('❌ 429: 请求过于频繁,请稍后再试'); break; case 500: // 服务器错误 console.error('❌ 500: 服务器内部错误'); break; case 502: // 网关错误 console.error('❌ 502: 网关错误'); break; case 503: // 服务不可用 console.error('❌ 503: 服务暂时不可用'); break; default: console.error(`❌ ${status}: 未知错误`); } } else if (error.request) { // 请求已发送但没有收到响应 console.error('❌ 网络错误: 请检查网络连接'); } else { // 请求配置出错 console.error('❌ 请求配置错误:', error.message); } // 自定义错误处理 if (originalRequest?.customErrorHandler) { originalRequest.customErrorHandler(error); } return Promise.reject(error); } ); /** * 取消所有待处理的请求 */ export function cancelAllRequests(message = '请求已取消'): void { pendingRequests.forEach((source) => { source.cancel(message); }); pendingRequests.clear(); } /** * 取消指定 URL 的请求 */ export function cancelRequest(url: string): void { pendingRequests.forEach((source, key) => { if (key.includes(url)) { source.cancel('请求已取消'); pendingRequests.delete(key); } }); } /** * 通用请求方法(增强版) */ export const request = { /** * GET 请求 */ get: (url: string, config?: RequestConfig) => api.get(url, config), /** * POST 请求 */ post: (url: string, data?: any, config?: RequestConfig) => api.post(url, data, config), /** * PUT 请求 */ put: (url: string, data?: any, config?: RequestConfig) => api.put(url, data, config), /** * DELETE 请求 */ delete: (url: string, config?: RequestConfig) => api.delete(url, config), /** * PATCH 请求 */ patch: (url: string, data?: any, config?: RequestConfig) => api.patch(url, data, config), /** * 上传文件 */ upload: ( url: string, file: File | Blob, onProgress?: (progress: number) => void, config?: RequestConfig ) => { const formData = new FormData(); formData.append('file', file); return api.post(url, formData, { ...config, headers: { 'Content-Type': 'multipart/form-data', ...config?.headers, }, onUploadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); onProgress(progress); } }, }); }, /** * 下载文件 */ download: async ( url: string, filename?: string, onProgress?: (progress: number) => void, config?: RequestConfig ) => { const response: any = await api.get(url, { ...config, responseType: 'blob', onDownloadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); onProgress(progress); } }, }); // 创建下载链接 const blob = new Blob([response]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename || 'download'; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl); return response; }, /** * 并发请求 */ all: (requests: Promise[]) => Promise.all(requests), /** * 串行请求 */ series: async (requests: (() => Promise)[]): Promise => { const results: T[] = []; for (const request of requests) { const result = await request(); results.push(result); } return results; }, }; /** * 创建带重试的请求 */ export function createRetryRequest( requestFn: () => Promise, maxRetries = 3, retryDelay = 1000 ): Promise { return new Promise((resolve, reject) => { let retries = 0; const attempt = async () => { try { const result = await requestFn(); resolve(result); } catch (error) { retries++; if (retries < maxRetries) { if (__DEV__) { console.log(`🔄 Retrying request (${retries}/${maxRetries})...`); } setTimeout(attempt, retryDelay * retries); } else { reject(error); } } }; attempt(); }); } export default api;