Files
smart-crop-ui/crop-x/apis/interceptor.js

285 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;