You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

597 lines
15 KiB

1 month ago
/**
* 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';
1 month ago
import md5 from 'md5';
/**
* API
*/
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
success?: boolean;
}
/**
* API
*/
export interface ApiError {
code: number;
message: string;
errors?: Record<string, string[]>;
}
/**
*
*/
export interface RequestConfig extends AxiosRequestConfig {
/** 是否显示 loading */
showLoading?: boolean;
/** 是否显示错误提示 */
showError?: boolean;
/** 是否重试 */
retry?: boolean;
/** 重试次数 */
retryCount?: number;
/** 是否需要 token */
requiresAuth?: boolean;
/** 自定义错误处理 */
customErrorHandler?: (error: AxiosError<ApiError>) => 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<string, CancelTokenSource>();
// 是否正在刷新 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<string | null> {
try {
const refreshToken = await AsyncStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token');
}
// 调用刷新 token 接口
const response = await axios.post<ApiResponse<{ token: string; refreshToken: string }>>(
`${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/';
}
1 month ago
// if (__DEV__ && apiName) {
// config.url = `${config.url}?${apiName}`;
// }
1 month ago
// // 从本地存储获取 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() };
// 打印请求信息(开发环境)
1 month ago
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,
});
}
1 month ago
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<ApiError>) => {
// 从请求队列中移除
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);
}
1 month ago
// 处理不同的错误状态码
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: <T = any>(url: string, config?: RequestConfig) => api.get<T, T>(url, config),
/**
* POST
*/
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.post<T, T>(url, data, config),
/**
* PUT
*/
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.put<T, T>(url, data, config),
/**
* DELETE
*/
delete: <T = any>(url: string, config?: RequestConfig) => api.delete<T, T>(url, config),
/**
* PATCH
*/
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
api.patch<T, T>(url, data, config),
/**
*
*/
upload: <T = any>(
url: string,
file: File | Blob,
onProgress?: (progress: number) => void,
config?: RequestConfig
) => {
const formData = new FormData();
formData.append('file', file);
return api.post<T, T>(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
) => {
1 month ago
const response: any = await api.get(url, {
1 month ago
...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: <T = any>(requests: Promise<T>[]) => Promise.all(requests),
/**
*
*/
series: async <T = any>(requests: (() => Promise<T>)[]): Promise<T[]> => {
const results: T[] = [];
for (const request of requests) {
const result = await request();
results.push(result);
}
return results;
},
};
/**
*
*/
export function createRetryRequest<T = any>(
requestFn: () => Promise<T>,
maxRetries = 3,
retryDelay = 1000
): Promise<T> {
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;