285 lines
7.6 KiB
JavaScript
285 lines
7.6 KiB
JavaScript
/**
|
||
* API 请求拦截器
|
||
* 功能:
|
||
* 1. 为除登录接口外的所有请求添加身份信息
|
||
* 2. 统一错误处理
|
||
* 3. 请求/响应日志记录
|
||
* 4. 自动刷新token
|
||
*/
|
||
|
||
import axios from 'axios';
|
||
import { toast } from 'sonner';
|
||
|
||
/**
|
||
* 创建API实例并配置拦截器
|
||
*/
|
||
export const createAPI = ({ baseURL, timeout = 10000, enableLogging = false }) => {
|
||
// 创建axios实例
|
||
const api = axios.create({
|
||
baseURL,
|
||
timeout,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
// 获取存储的token和用户信息
|
||
const getAuthData = () => {
|
||
try {
|
||
const authData = localStorage.getItem('authData');
|
||
if (authData) {
|
||
const { token, user } = JSON.parse(authData);
|
||
return { token, user_id: user?.id };
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to parse auth data from localStorage:', error);
|
||
}
|
||
return { token: null, user_id: null };
|
||
};
|
||
|
||
// 请求拦截器
|
||
api.interceptors.request.use(
|
||
(config) => {
|
||
// 添加请求日志
|
||
if (enableLogging) {
|
||
console.log(`🚀 API Request: ${config.method?.toUpperCase()} ${config.url}`, {
|
||
data: config.data,
|
||
params: config.params,
|
||
});
|
||
}
|
||
|
||
// 为除登录接口外的所有请求添加身份信息
|
||
const isLoginRequest = config.url?.includes('/auth/login');
|
||
const isRegisterRequest = config.url?.includes('/auth/register');
|
||
|
||
if (!isLoginRequest && !isRegisterRequest) {
|
||
const { token, user_id } = getAuthData();
|
||
|
||
if (token) {
|
||
config.headers['auth'] = token; // JWT token
|
||
}
|
||
|
||
if (user_id) {
|
||
config.headers['user_id'] = user_id; // 用户ID
|
||
}
|
||
}
|
||
|
||
// 添加请求时间戳
|
||
config.metadata = { startTime: new Date() };
|
||
|
||
return config;
|
||
},
|
||
(error) => {
|
||
console.error('❌ Request Error:', error);
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
// 响应拦截器
|
||
api.interceptors.response.use(
|
||
(response) => {
|
||
// 添加响应日志
|
||
if (enableLogging) {
|
||
const duration = new Date() - response.config.metadata.startTime;
|
||
console.log(`✅ API Response: ${response.config.method?.toUpperCase()} ${response.config.url}`, {
|
||
status: response.status,
|
||
duration: `${duration}ms`,
|
||
data: response.data,
|
||
});
|
||
}
|
||
|
||
// 统一处理成功响应格式
|
||
if (response.data && typeof response.data === 'object') {
|
||
// 如果后端返回统一格式 { code, message, data }
|
||
if ('code' in response.data) {
|
||
const { code, message, data } = response.data;
|
||
|
||
if (code === 200 || code === 0) {
|
||
return { ...response, data }; // 返回实际数据
|
||
} else {
|
||
// 业务错误处理
|
||
toast.error(message || '请求失败');
|
||
return Promise.reject(new Error(message || '请求失败'));
|
||
}
|
||
}
|
||
}
|
||
|
||
return response;
|
||
},
|
||
async (error) => {
|
||
const originalRequest = error.config;
|
||
|
||
// 添加错误日志
|
||
if (enableLogging) {
|
||
const duration = originalRequest?.metadata?.startTime
|
||
? `${new Date() - originalRequest.metadata.startTime}ms`
|
||
: 'N/A';
|
||
|
||
console.error(`❌ API Error: ${originalRequest?.method?.toUpperCase()} ${originalRequest?.url}`, {
|
||
status: error.response?.status,
|
||
duration,
|
||
message: error.message,
|
||
data: error.response?.data,
|
||
});
|
||
}
|
||
|
||
// 处理不同类型的错误
|
||
if (error.response) {
|
||
const { status, data } = error.response;
|
||
|
||
switch (status) {
|
||
case 401:
|
||
// 未授权 - token过期或无效
|
||
return handleUnauthorizedError(originalRequest);
|
||
|
||
case 403:
|
||
// 禁止访问 - 权限不足
|
||
toast.error('您没有权限访问此资源');
|
||
break;
|
||
|
||
case 404:
|
||
// 资源不存在
|
||
toast.error('请求的资源不存在');
|
||
break;
|
||
|
||
case 422:
|
||
// 表单验证错误
|
||
if (data?.errors) {
|
||
Object.values(data.errors).flat().forEach(errorMessage => {
|
||
toast.error(errorMessage);
|
||
});
|
||
} else {
|
||
toast.error(data?.message || '请求参数错误');
|
||
}
|
||
break;
|
||
|
||
case 429:
|
||
// 请求过于频繁
|
||
toast.error('请求过于频繁,请稍后再试');
|
||
break;
|
||
|
||
case 500:
|
||
// 服务器内部错误
|
||
toast.error('服务器内部错误,请稍后再试');
|
||
break;
|
||
|
||
default:
|
||
// 其他错误
|
||
toast.error(data?.message || `请求失败 (${status})`);
|
||
}
|
||
} else if (error.request) {
|
||
// 网络错误
|
||
if (error.code === 'ECONNABORTED') {
|
||
toast.error('请求超时,请检查网络连接');
|
||
} else {
|
||
toast.error('网络连接失败,请检查网络');
|
||
}
|
||
} else {
|
||
// 其他错误
|
||
toast.error('请求配置错误');
|
||
}
|
||
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
return api;
|
||
};
|
||
|
||
/**
|
||
* 处理401未授权错误
|
||
* 尝试刷新token或跳转到登录页
|
||
*/
|
||
const handleUnauthorizedError = async (originalRequest) => {
|
||
// 避免重复刷新token
|
||
if (originalRequest._retry) {
|
||
// 刷新失败,清除本地存储并跳转到登录页
|
||
localStorage.removeItem('authData');
|
||
window.location.href = '/login';
|
||
return Promise.reject(new Error('登录已过期,请重新登录'));
|
||
}
|
||
|
||
originalRequest._retry = true;
|
||
|
||
try {
|
||
// 尝试刷新token
|
||
const authData = JSON.parse(localStorage.getItem('authData') || '{}');
|
||
const refreshToken = authData.refreshToken;
|
||
|
||
if (refreshToken) {
|
||
const response = await axios.post(`${originalRequest.baseURL}/auth/refresh-token`, {
|
||
refreshToken
|
||
});
|
||
|
||
const { token, user } = response.data.data;
|
||
|
||
// 更新本地存储
|
||
localStorage.setItem('authData', JSON.stringify({
|
||
token,
|
||
refreshToken,
|
||
user
|
||
}));
|
||
|
||
// 重新发送原始请求
|
||
originalRequest.headers['auth'] = token;
|
||
originalRequest.headers['user_id'] = user.id;
|
||
|
||
return axios(originalRequest);
|
||
} else {
|
||
// 没有刷新token,跳转到登录页
|
||
localStorage.removeItem('authData');
|
||
window.location.href = '/login';
|
||
return Promise.reject(new Error('登录已过期,请重新登录'));
|
||
}
|
||
} catch (refreshError) {
|
||
// 刷新token失败
|
||
localStorage.removeItem('authData');
|
||
window.location.href = '/login';
|
||
return Promise.reject(new Error('登录已过期,请重新登录'));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 设置认证信息
|
||
* @param {string} token JWT token
|
||
* @param {object} user 用户信息
|
||
* @param {string} refreshToken 刷新token
|
||
*/
|
||
export const setAuthData = (token, user, refreshToken) => {
|
||
const authData = {
|
||
token,
|
||
refreshToken: refreshToken || '',
|
||
user
|
||
};
|
||
localStorage.setItem('authData', JSON.stringify(authData));
|
||
};
|
||
|
||
/**
|
||
* 清除认证信息
|
||
*/
|
||
export const clearAuthData = () => {
|
||
localStorage.removeItem('authData');
|
||
};
|
||
|
||
/**
|
||
* 获取当前认证信息
|
||
*/
|
||
export const getAuthData = () => {
|
||
try {
|
||
const authData = localStorage.getItem('authData');
|
||
return authData ? JSON.parse(authData) : null;
|
||
} catch (error) {
|
||
console.warn('Failed to get auth data:', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 检查是否已登录
|
||
*/
|
||
export const isAuthenticated = () => {
|
||
const authData = getAuthData();
|
||
return !!(authData?.token && authData?.user);
|
||
};
|
||
|
||
export default createAPI; |