提交1 bmad搭建与项目启动 - ok
This commit is contained in:
285
crop-x/apis/interceptor.js
Normal file
285
crop-x/apis/interceptor.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user