This commit is contained in:
zlwu committed 2026-05-21 11:09:29 +08:00
1 parent ee73f69759
commit 74f751d0d1
70 files changed
+9340 -6930

No files matched your search

+93 -16
View File
@@ -5,7 +5,7 @@
* ## 主要功能
*
* - 请求/响应拦截器(自动添加 Token、统一错误处理)
* - 401 未授权自动登出(带防抖机制)
* - 401 未授权自动刷新 Token(并发安全,刷新失败后登出)
* - 请求失败自动重试(可配置)
* - 统一的成功/错误消息提示
* - 支持 GET/POST/PUT/DELETE 等常用方法
@@ -21,6 +21,7 @@ import { HttpError, handleError, showError, showSuccess } from './error'
import { $t } from '@/locales'
import { BaseResponse } from '@/types'
import { decryptJson, deriveKey, encryptJson, randomNonce } from './crypto'
import { fetchRefreshToken } from '@/api/auth'
/** 请求配置常量 */
const REQUEST_TIMEOUT = 60000
@@ -34,10 +35,32 @@ const UNAUTHORIZED_DEBOUNCE_TIME = 3000
let isUnauthorizedErrorShown = false
let unauthorizedTimer: NodeJS.Timeout | null = null
/** Token 刷新状态(并发安全) */
let isRefreshing = false
let pendingRequests: Array<{
resolve: (token: string) => void
reject: (error: unknown) => void
}> = []
function subscribeTokenRefresh(resolve: (token: string) => void, reject: (error: unknown) => void) {
pendingRequests.push({ resolve, reject })
}
function onRefreshed(newToken: string) {
pendingRequests.forEach(({ resolve }) => resolve(newToken))
pendingRequests = []
}
function onRefreshFailed(error: unknown) {
pendingRequests.forEach(({ reject }) => reject(error))
pendingRequests = []
}
/** 扩展 AxiosRequestConfig */
interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
showErrorMessage?: boolean
showSuccessMessage?: boolean
_retry?: boolean
}
const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env
@@ -148,11 +171,15 @@ axiosInstance.interceptors.response.use(
const code = resData?.code
const msg = resData?.msg || resData?.message
if (code === ApiStatus.success) return response
if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
if (code === ApiStatus.unauthorized) {
return handleUnauthorizedResponse(response.config, msg)
}
throw createHttpError(msg || $t('httpMsg.requestFailed'), code)
},
(error) => {
if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError()
async (error) => {
if (error.response?.status === ApiStatus.unauthorized) {
return handleUnauthorizedResponse(error.response.config, undefined)
}
return Promise.reject(handleError(error))
}
)
@@ -162,21 +189,71 @@ function createHttpError(message: string, code: number) {
return new HttpError(message, code)
}
/** 处理401错误(带防抖) */
function handleUnauthorizedError(message?: string): never {
const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
/**
* 处理 401 响应:尝试刷新 token,成功则重试原请求,失败则登出
*/
async function handleUnauthorizedResponse(
config: InternalAxiosRequestConfig,
message?: string
): Promise<AxiosResponse> {
const userStore = useUserStore()
const storedRefreshToken = userStore.refreshToken
if (!isUnauthorizedErrorShown) {
isUnauthorizedErrorShown = true
logOut()
unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
showError(error, true)
throw error
if (!storedRefreshToken) {
triggerLogoutWithDebounce(message)
throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
}
throw error
if ((config as ExtendedAxiosRequestConfig)._retry) {
triggerLogoutWithDebounce(message)
throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
}
if (isRefreshing) {
return new Promise<AxiosResponse>((resolve, reject) => {
subscribeTokenRefresh(
(newToken: string) => {
config.headers.set('Authorization', `Bearer ${newToken}`)
;(config as ExtendedAxiosRequestConfig)._retry = true
resolve(axiosInstance.request(config))
},
(error: unknown) => {
reject(createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized))
}
)
})
}
isRefreshing = true
;(config as ExtendedAxiosRequestConfig)._retry = true
try {
const result = await fetchRefreshToken(storedRefreshToken)
const newAccessToken = result.token
const newRefreshToken = result.refreshToken
userStore.setToken(newAccessToken, newRefreshToken)
onRefreshed(newAccessToken)
config.headers.set('Authorization', `Bearer ${newAccessToken}`)
return await axiosInstance.request(config)
} catch (refreshError) {
onRefreshFailed(refreshError)
triggerLogoutWithDebounce(message)
throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
} finally {
isRefreshing = false
}
}
/** 带 3 秒防抖的登出提示 */
function triggerLogoutWithDebounce(message?: string) {
if (isUnauthorizedErrorShown) return
isUnauthorizedErrorShown = true
const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
logOut()
unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
showError(error, true)
}
/** 重置401防抖状态 */