diff --git a/__pycache__/repair_test_bug_50.cpython-313-pytest-9.0.2.pyc b/__pycache__/repair_test_bug_50.cpython-313-pytest-9.0.2.pyc index 2a08ea5..3cd5e94 100644 Binary files a/__pycache__/repair_test_bug_50.cpython-313-pytest-9.0.2.pyc and b/__pycache__/repair_test_bug_50.cpython-313-pytest-9.0.2.pyc differ diff --git a/src/api/request.ts b/src/api/request.ts index 59aca11..1fde7e4 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import type { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; +import type { AxiosInstance, AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; // API 基础配置 // 开发环境使用代理,生产环境使用环境变量 @@ -62,6 +62,65 @@ function reportToLogCenter(error: Error, context?: Record) { } } +// Token 自动刷新状态 +let isRefreshing = false; +let failedQueue: Array<{ + resolve: (value: AxiosResponse) => void; + reject: (reason?: unknown) => void; + config: InternalAxiosRequestConfig; +}> = []; + +function getRefreshToken(): string | null { + try { + const authStorage = localStorage.getItem('auth-storage'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + return parsed?.state?.refreshToken || null; + } + } catch { + // ignore + } + return null; +} + +function processQueue(error: Error | null, token: string | null = null) { + failedQueue.forEach(({ resolve, reject, config }) => { + if (error) { + reject(error); + } else if (token) { + config.headers.Authorization = `Bearer ${token}`; + resolve(request(config)); + } + }); + failedQueue = []; +} + +function updateStoredTokens(access: string, refresh?: string) { + localStorage.setItem('admin_token', access); + try { + const authStorage = localStorage.getItem('auth-storage'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + if (parsed?.state) { + parsed.state.token = access; + if (refresh) { + parsed.state.refreshToken = refresh; + } + localStorage.setItem('auth-storage', JSON.stringify(parsed)); + } + } + } catch { + // ignore + } +} + +function clearAuthAndRedirect() { + localStorage.removeItem('admin_token'); + localStorage.removeItem('admin_info'); + localStorage.removeItem('auth-storage'); + window.location.href = '/login'; +} + // 创建 Axios 实例 const request: AxiosInstance = axios.create({ baseURL: BASE_URL, @@ -95,12 +154,9 @@ request.interceptors.response.use( const data = response.data; // 业务错误处理 if (data.code !== 0) { - // Token 过期 + // Token 过期(业务错误码) if (data.code === 401 || data.code === 1001) { - localStorage.removeItem('admin_token'); - localStorage.removeItem('admin_info'); - localStorage.removeItem('auth-storage'); - window.location.href = '/login'; + clearAuthAndRedirect(); return Promise.reject(new Error(data.message || '登录已过期')); } return Promise.reject(new Error(data.message || '请求失败')); @@ -108,23 +164,64 @@ request.interceptors.response.use( return data; }, (error: AxiosError<{ code: number; message: string }>) => { - // 上报到 Log Center - const apiError = new Error(error.message); - reportToLogCenter(apiError, { - url: error.config?.url, - method: error.config?.method, - status: error.response?.status, - responseData: error.response?.data, - }); - if (error.response) { const { status, data } = error.response; - if (status === 401) { - localStorage.removeItem('admin_token'); - localStorage.removeItem('admin_info'); - localStorage.removeItem('auth-storage'); - window.location.href = '/login'; + const originalConfig = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + // 401: 尝试用 refresh token 刷新 + if (status === 401 && originalConfig && !originalConfig._retry) { + const refreshTokenValue = getRefreshToken(); + + if (!refreshTokenValue) { + clearAuthAndRedirect(); + return Promise.reject(new Error(data?.message || '登录已过期')); + } + + if (isRefreshing) { + // 已在刷新中,排队等待 + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject, config: originalConfig }); + }); + } + + originalConfig._retry = true; + isRefreshing = true; + + return axios + .post( + `${request.defaults.baseURL || ''}/api/admin/auth/refresh/`, + { refresh: refreshTokenValue }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then((res) => { + const tokenData = res.data.data || res.data; + const newAccess: string = tokenData.access; + const newRefresh: string | undefined = tokenData.refresh; + + updateStoredTokens(newAccess, newRefresh); + isRefreshing = false; + processQueue(null, newAccess); + + originalConfig.headers.Authorization = `Bearer ${newAccess}`; + return request(originalConfig); + }) + .catch((refreshError) => { + isRefreshing = false; + processQueue(refreshError instanceof Error ? refreshError : new Error('刷新令牌失败')); + clearAuthAndRedirect(); + return Promise.reject(new Error('登录已过期,请重新登录')); + }); } + + // 非 401 错误上报到 Log Center + const apiError = new Error(error.message); + reportToLogCenter(apiError, { + url: error.config?.url, + method: error.config?.method, + status: error.response?.status, + responseData: error.response?.data, + }); + return Promise.reject(new Error(data?.message || `请求失败 (${status})`)); } return Promise.reject(new Error('网络错误,请检查网络连接'));