提示词保存
This commit is contained in:
@@ -1,126 +1,168 @@
|
||||
import axios from 'axios'
|
||||
/**
|
||||
* 应用层 HTTP 客户端
|
||||
* 使用 mono 级别的 axios 实例,添加应用特定的 401 处理
|
||||
*/
|
||||
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getAuthHeader, clearAllTokens } from '@gold/utils/token-manager'
|
||||
import { clearAllTokens, getRefreshToken } from '@gold/utils/token-manager'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createClientAxios } from '@gold/api/axios/client'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
|
||||
// 刷新 token 的状态管理
|
||||
let isRefreshing = false
|
||||
let refreshPromise = null
|
||||
|
||||
/**
|
||||
* 不需要 token 的接口白名单
|
||||
* 支持完整路径匹配或路径包含匹配
|
||||
* 处理 403 禁止访问错误(应用层特定逻辑)
|
||||
* 先尝试刷新 token,如果失败或没有 refresh token 才提示用户
|
||||
*/
|
||||
const WHITE_LIST = [
|
||||
'/auth/login', // 密码登录
|
||||
'/auth/send-sms-code', // 发送验证码
|
||||
'/auth/sms-login', // 短信登录
|
||||
'/auth/validate-sms-code', // 验证验证码
|
||||
'/auth/register', // 注册(如果有)
|
||||
'/auth/reset-password', // 重置密码
|
||||
'/auth/refresh-token', // 刷新token(可选,根据后端要求)
|
||||
]
|
||||
|
||||
/**
|
||||
* 检查 URL 是否在白名单中
|
||||
* @param {string} url - 请求 URL
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInWhiteList(url) {
|
||||
if (!url) return false
|
||||
return WHITE_LIST.some((path) => url.includes(path))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 创建 axios 实例
|
||||
const http = axios.create({
|
||||
baseURL: '/',
|
||||
timeout: 180000, // 3分钟
|
||||
})
|
||||
|
||||
// 请求拦截:自动注入 Token
|
||||
http.interceptors.request.use((config) => {
|
||||
// 检查是否需要 token(不在白名单中且未显式设置 isToken = false)
|
||||
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
|
||||
config.headers['tenant-id'] = import.meta.env.VITE_TENANT_ID
|
||||
if (needToken) {
|
||||
// 使用统一的 token 管理器获取 header
|
||||
const authHeader = getAuthHeader()
|
||||
if (authHeader) {
|
||||
config.headers.Authorization = authHeader
|
||||
}
|
||||
}
|
||||
|
||||
// 允许跨域第三方域名,使用绝对地址时保持原样
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.response.use(
|
||||
(resp) => {
|
||||
const data = resp.data
|
||||
// 检查响应数据中的 code 字段
|
||||
if (data && typeof data.code === 'number' && (data.code === 0 || data.code === 200)) {
|
||||
return data
|
||||
} else {
|
||||
// code 不为 0 时,检查是否为401
|
||||
if (data && typeof data.code === 'number' && data.code === 401) {
|
||||
handle401Error()
|
||||
}
|
||||
// code 不为 0 时,抛出错误
|
||||
const error = new Error(data?.message || data?.msg || '请求失败')
|
||||
error.code = data?.code
|
||||
error.data = data
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 处理 HTTP 状态码 401
|
||||
if (error.response && error.response.status === 401) {
|
||||
handle401Error()
|
||||
}
|
||||
// 统一错误处理:输出关键信息,便于排查 403 等问题
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 处理 401 未授权错误
|
||||
* 清空 token 并退出登录
|
||||
*
|
||||
* 注意:使用防抖机制避免多个请求同时401时重复处理
|
||||
*/
|
||||
function handle401Error() {
|
||||
// 避免重复处理(防止多个请求同时401导致多次调用)
|
||||
if (handle401Error.processed) {
|
||||
async function handleApp403Error() {
|
||||
// 避免重复处理
|
||||
if (handleApp403Error.processed) {
|
||||
return
|
||||
}
|
||||
|
||||
handle401Error.processed = true
|
||||
handleApp403Error.processed = true
|
||||
|
||||
// 1. 清空所有 token
|
||||
try {
|
||||
clearAllTokens() // 统一使用 token-manager 的清空函数
|
||||
// 检查是否有 refresh token
|
||||
const refreshTokenValue = getRefreshToken()
|
||||
|
||||
if (refreshTokenValue) {
|
||||
// 如果有 refresh token,尝试刷新
|
||||
try {
|
||||
// 如果正在刷新,等待刷新完成
|
||||
if (isRefreshing && refreshPromise) {
|
||||
await refreshPromise
|
||||
handleApp403Error.processed = false
|
||||
return
|
||||
}
|
||||
|
||||
// 开始刷新 token
|
||||
isRefreshing = true
|
||||
refreshPromise = refreshToken()
|
||||
|
||||
await refreshPromise
|
||||
|
||||
// 刷新成功,重置状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
handleApp403Error.processed = false
|
||||
|
||||
// 刷新成功,不提示用户(静默刷新)
|
||||
return
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
console.error('刷新 token 失败:', refreshError)
|
||||
|
||||
// 刷新失败才提示用户
|
||||
message.warning('登录状态已过期,请重新登录', 3)
|
||||
handleApp403Error.processed = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 没有 refresh token,提示用户
|
||||
message.warning('登录状态已过期,请重新登录', 3)
|
||||
handleApp403Error.processed = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('清空 token 失败:', e)
|
||||
console.error('处理 403 错误失败:', e)
|
||||
handleApp403Error.processed = false
|
||||
}
|
||||
|
||||
// 2. 退出登录状态(清空用户信息)
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
// logout() 会清空用户信息和本地存储
|
||||
userStore.logout()
|
||||
} catch (e) {
|
||||
console.error('退出登录失败:', e)
|
||||
}
|
||||
|
||||
// 3. 提示用户(延迟显示,避免在清空过程中显示)
|
||||
setTimeout(() => {
|
||||
message.warning('登录已过期,请重新登录', 3)
|
||||
}, 100)
|
||||
|
||||
// 4. 延迟重置标志,避免短时间内重复处理
|
||||
setTimeout(() => {
|
||||
handle401Error.processed = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 401 未授权错误(应用层特定逻辑)
|
||||
* 先尝试刷新 token,如果失败或没有 refresh token 才退出登录
|
||||
*/
|
||||
async function handleApp401Error() {
|
||||
// 避免重复处理
|
||||
if (handleApp401Error.processed) {
|
||||
return
|
||||
}
|
||||
|
||||
handleApp401Error.processed = true
|
||||
|
||||
try {
|
||||
// 检查是否有 refresh token
|
||||
const refreshTokenValue = getRefreshToken()
|
||||
|
||||
if (refreshTokenValue) {
|
||||
// 如果有 refresh token,尝试刷新
|
||||
try {
|
||||
// 如果正在刷新,等待刷新完成
|
||||
if (isRefreshing && refreshPromise) {
|
||||
await refreshPromise
|
||||
handleApp401Error.processed = false
|
||||
return
|
||||
}
|
||||
|
||||
// 开始刷新 token
|
||||
isRefreshing = true
|
||||
refreshPromise = refreshToken()
|
||||
|
||||
await refreshPromise
|
||||
|
||||
// 刷新成功,重置状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
handleApp401Error.processed = false
|
||||
|
||||
// 刷新成功,不提示用户(静默刷新)
|
||||
return
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
console.error('刷新 token 失败:', refreshError)
|
||||
|
||||
// 继续执行退出登录逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 没有 refresh token 或刷新失败,执行退出登录
|
||||
try {
|
||||
clearAllTokens()
|
||||
} catch (e) {
|
||||
console.error('清空 token 失败:', e)
|
||||
}
|
||||
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
} catch (e) {
|
||||
console.error('退出登录失败:', e)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
message.warning('登录已过期,请重新登录', 3)
|
||||
}, 100)
|
||||
|
||||
} catch (e) {
|
||||
console.error('处理 401 错误失败:', e)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
handleApp401Error.processed = false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用层 HTTP 客户端
|
||||
* 基于 mono 级别的 axios 实例,添加应用特定的错误处理
|
||||
*/
|
||||
const http = createClientAxios({
|
||||
baseURL: '/',
|
||||
timeout: 180000,
|
||||
on401: handleApp401Error,
|
||||
on403: handleApp403Error,
|
||||
})
|
||||
|
||||
// 注意:403 处理已在 createClientAxios 的响应拦截器中通过 on403 回调处理
|
||||
|
||||
export default http
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user