refactor: 优化请求拦截器逻辑

This commit is contained in:
Ray.Hao
2026-03-03 22:19:48 +08:00
parent 0fd1ff197e
commit 8f5ffcf521

View File

@@ -5,10 +5,10 @@ import { useUserStoreHook } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { AuthStorage, redirectToLogin } from "@/utils/auth";
// ============================================
// HTTP 请求实例
// ============================================
// 记录已重试的请求,防止无限循环
const retriedConfigs = new WeakSet<InternalAxiosRequestConfig>();
// HTTP 请求实例
const http = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
@@ -16,10 +16,7 @@ const http = axios.create({
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: "repeat" }),
});
// ============================================
// 请求拦截器
// ============================================
http.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = AuthStorage.getAccessToken();
@@ -35,14 +32,12 @@ http.interceptors.request.use(
(error) => Promise.reject(error)
);
// ============================================
// 响应拦截器
// ============================================
http.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// 二进制数据直接返回
const { responseType } = response.config;
// 二进制数据直接返回
if (responseType === "blob" || responseType === "arraybuffer") {
return response;
}
@@ -52,7 +47,9 @@ http.interceptors.response.use(
if (code === ApiCodeEnum.SUCCESS) {
return data;
}
return rejectWithMessage(msg, "系统出错");
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "系统出错"));
},
async (error) => {
@@ -67,7 +64,29 @@ http.interceptors.response.use(
// Token 过期:尝试刷新 token 后自动重试一次
if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) {
return retryWithRefresh(config);
// 已重试过,直接跳登录
if (retriedConfigs.has(config)) {
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error("Token Invalid"));
}
retriedConfigs.add(config);
try {
const userStore = useUserStoreHook();
await userStore.refreshTokenOnce();
const token = AuthStorage.getAccessToken();
if (token) {
config.headers = config.headers || ({} as any);
(config.headers as any).Authorization = `Bearer ${token}`;
}
return http(config);
} catch {
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error("Token refresh failed"));
}
}
// Refresh token 失效:无法续期,跳转登录
@@ -76,67 +95,17 @@ http.interceptors.response.use(
return Promise.reject(new Error(msg || "Token Invalid"));
}
// 权限不足:刷新权限快照(用户信息 + 动态路由)后提示
// 权限不足:刷新权限快照后提示
if (code === ApiCodeEnum.PERMISSION_DENIED) {
return handlePermissionDenied(msg);
const permissionStore = usePermissionStoreHook();
await permissionStore.reloadPermissionSnapshotOnce();
ElMessage.error(msg || "权限不足");
return Promise.reject(new Error(msg || "权限不足"));
}
return rejectWithMessage(msg, "请求失败");
ElMessage.error(msg || "请求失败");
return Promise.reject(new Error(msg || "请求失败"));
}
);
/**
* 权限不足处理:刷新权限快照(用户信息 + 动态路由),并给出提示
*
* 刷新完成后仍然按失败处理,交由调用方的错误流处理
*/
async function handlePermissionDenied(msg?: string): Promise<never> {
const permissionStore = usePermissionStoreHook();
await permissionStore.reloadPermissionSnapshotOnce();
return rejectWithMessage(msg, "权限不足");
}
/**
* access token 过期后的自动续期与重试
*
* - 刷新 token 走单飞userStore.refreshTokenOnce
* - 当前请求最多重试一次__isTokenRetry 标记)
*/
async function retryWithRefresh(config: InternalAxiosRequestConfig): Promise<unknown> {
const retryConfig = config as InternalAxiosRequestConfig & { __isTokenRetry?: boolean };
if (retryConfig.__isTokenRetry) {
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error("Token Invalid"));
}
retryConfig.__isTokenRetry = true;
try {
const userStore = useUserStoreHook();
await userStore.refreshTokenOnce();
const token = AuthStorage.getAccessToken();
if (token) {
retryConfig.headers = retryConfig.headers || ({} as any);
(retryConfig.headers as any).Authorization = `Bearer ${token}`;
}
return http(retryConfig);
} catch {
await redirectToLogin("登录已过期,请重新登录");
return Promise.reject(new Error("Token refresh failed"));
}
}
/**
* 统一处理业务错误提示并拒绝 Promise
*
* @param msg 错误消息内容
* @param fallback 默认兜底消息
*/
function rejectWithMessage(msg: string | undefined, fallback: string): Promise<never> {
const message = msg || fallback;
ElMessage.error(message);
return Promise.reject(new Error(message));
}
export default http;