在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,159 @@
/**
* axios 实例 & JWT 拦截器。
*
* - 请求拦截器:自动从 localStorage 读取 access_token 并附加 Authorization header
* - 响应拦截器:遇到 401 时尝试用 refresh_token 刷新,刷新失败则清除令牌并跳转 /login
* - 并发刷新保护:多个请求同时 401 时只触发一次 refresh其余排队等待
*/
import axios, {
type AxiosError,
type AxiosRequestConfig,
type InternalAxiosRequestConfig,
} from "axios";
/* ------------------------------------------------------------------ */
/* 常量 */
/* ------------------------------------------------------------------ */
const ACCESS_TOKEN_KEY = "access_token";
const REFRESH_TOKEN_KEY = "refresh_token";
/* ------------------------------------------------------------------ */
/* axios 实例 */
/* ------------------------------------------------------------------ */
export const apiClient = axios.create({
baseURL: "/api",
});
/* ------------------------------------------------------------------ */
/* 请求拦截器 — 附加 JWT */
/* ------------------------------------------------------------------ */
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
/* ------------------------------------------------------------------ */
/* 响应拦截器 — 401 自动刷新 */
/* ------------------------------------------------------------------ */
/** 是否正在刷新中 */
let isRefreshing = false;
/** 等待刷新完成的请求队列 */
let pendingQueue: {
resolve: (token: string) => void;
reject: (err: unknown) => void;
}[] = [];
/** 刷新完成后,依次重放排队的请求 */
function processPendingQueue(token: string | null, error: unknown) {
pendingQueue.forEach(({ resolve, reject }) => {
if (token) {
resolve(token);
} else {
reject(error);
}
});
pendingQueue = [];
}
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & {
_retried?: boolean;
};
// 非 401、无原始请求、或已重试过 → 直接抛出
if (
error.response?.status !== 401 ||
!originalRequest ||
originalRequest._retried
) {
return Promise.reject(error);
}
// 刷新端点本身 401 → 不再递归刷新
if (originalRequest.url === "/auth/refresh") {
clearTokensAndRedirect();
return Promise.reject(error);
}
// 已有刷新请求在飞 → 排队等待
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
pendingQueue.push({ resolve, reject });
}).then((newToken) => {
originalRequest.headers = {
...originalRequest.headers,
Authorization: `Bearer ${newToken}`,
};
originalRequest._retried = true;
return apiClient(originalRequest);
});
}
// 发起刷新
isRefreshing = true;
originalRequest._retried = true;
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
if (!refreshToken) {
isRefreshing = false;
processPendingQueue(null, error);
clearTokensAndRedirect();
return Promise.reject(error);
}
try {
// 用独立 axios 调用避免被自身拦截器干扰
const { data } = await axios.post<{
access_token: string;
refresh_token: string;
}>("/api/auth/refresh", { refresh_token: refreshToken });
localStorage.setItem(ACCESS_TOKEN_KEY, data.access_token);
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token);
processPendingQueue(data.access_token, null);
// 重放原始请求
originalRequest.headers = {
...originalRequest.headers,
Authorization: `Bearer ${data.access_token}`,
};
return apiClient(originalRequest);
} catch (refreshError) {
processPendingQueue(null, refreshError);
clearTokensAndRedirect();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
/* ------------------------------------------------------------------ */
/* 辅助 */
/* ------------------------------------------------------------------ */
function clearTokensAndRedirect() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
// 派发自定义事件,让 authStore 监听并重置状态
// 避免直接 import authStore 导致循环依赖
window.dispatchEvent(new Event("auth:force-logout"));
// 避免在登录页反复跳转
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
}