This commit is contained in:
2026-01-18 15:27:43 +08:00
parent a0378b5cbd
commit 567e77cd87
14 changed files with 358 additions and 620 deletions

View File

@@ -1,15 +1,7 @@
/**
* Mono 级别的 C 端 Axios 实例
* 供 monorepo 中所有应用使用的统一 HTTP 客户端
*/
import axios from 'axios'
// 直接使用实例(最简单、最可靠)
import tokenManager from '@gold/utils/token-manager'
/**
* 不需要 token 的接口白名单
*/
// Token白名单
const WHITE_LIST = [
'/auth/login',
'/auth/send-sms-code',
@@ -20,89 +12,38 @@ const WHITE_LIST = [
'/auth/refresh-token',
]
/**
* 检查 URL 是否在白名单中
*/
function isInWhiteList(url) {
return !!(url && WHITE_LIST.some(path => url.includes(path)))
}
/**
* 自动刷新 token 的锁机制和队列
*/
// Token刷新状态管理
let isRefreshing = false
let refreshSubscribers = []
/**
* 订阅 token 刷新完成
* @param {Function} callback - 回调函数
*/
function subscribeTokenRefresh(callback) {
if (isRefreshing) {
refreshSubscribers.push(callback)
} else {
callback()
}
isRefreshing ? refreshSubscribers.push(callback) : callback()
}
/**
* 执行所有订阅回调
*/
function onRefreshed() {
refreshSubscribers.forEach(callback => callback())
refreshSubscribers = []
}
/**
* 处理 401 未授权错误
* 注意:只做清理工作,不处理重定向(重定向由上层回调处理)
*/
let isHandling401 = false
let handling401Timeout = null
function handle401Error(error) {
// 防止重复处理
if (isHandling401 || error?._handled) {
return
}
isHandling401 = true
error && (error._handled = true)
try {
tokenManager.clearTokens()
const message = error?.needRelogin
? '[HTTP] Token已清空refreshToken无效需要重新登录'
: '[HTTP] Token已清空因401错误'
console.warn(message)
} catch (e) {
console.error('[HTTP] 清空 token 失败:', e)
}
if (handling401Timeout) {
clearTimeout(handling401Timeout)
}
handling401Timeout = setTimeout(() => {
isHandling401 = false
}, 3000)
}
/**
* 创建 C 端 Axios 实例
* 创建Axios实例
* @param {Object} options - 配置选项
* @param {string} options.baseURL - 基础 URL
* @param {number} options.timeout - 超时时间(毫秒)
* @param {Function} options.on401 - 401 错误处理函数
* @param {Function} options.on403 - 403 错误处理函数
* @param {Function} options.refreshTokenFn - Token 刷新函数(可选)
* @returns {AxiosInstance} Axios 实例
* @param {string} options.baseURL - 基础URL
* @param {number} options.timeout - 超时时间
* @param {Function} options.on401 - 401错误处理函数
* @param {Function} options.on403 - 403错误处理函数
* @param {Function} options.refreshTokenFn - Token刷新函数
* @returns {AxiosInstance} Axios实例
*/
export function createClientAxios(options = {}) {
export default function createClientAxios(options = {}) {
const {
baseURL = '/',
timeout = 180000,
on401 = handle401Error,
on401 = null,
on403 = null,
refreshTokenFn = null,
} = options
@@ -114,7 +55,16 @@ export function createClientAxios(options = {}) {
// 请求拦截器
client.interceptors.request.use((config) => {
// 添加 tenant-id
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
if (!needToken) {
return config
}
const currentToken = tokenManager.getAccessToken()
if (!currentToken) {
return config
}
const tenantId =
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) ||
(typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) ||
@@ -123,27 +73,9 @@ export function createClientAxios(options = {}) {
if (tenantId) {
config.headers['tenant-id'] = tenantId
}
// 检查是否需要认证
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
if (!needToken) {
return config
}
// 检查 token 是否即将过期30秒缓冲
const BUFFER_TIME = 30 * 1000
const currentToken = tokenManager.getAccessToken()
if (!currentToken) {
console.warn('[Token] 没有可用的 accessToken')
return config
}
const isTokenExpired = tokenManager.isExpired(BUFFER_TIME)
const isTokenExpired = tokenManager.isExpired(30 * 1000)
if (!isTokenExpired) {
// Token 未过期,直接添加 Authorization 头
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
@@ -151,44 +83,32 @@ export function createClientAxios(options = {}) {
return config
}
// Token 即将过期,需要刷新
console.info('[Token] Token刷新')
// 如果不在刷新过程中,启动刷新
if (!isRefreshing) {
isRefreshing = true
if (!refreshTokenFn || typeof refreshTokenFn !== 'function') {
console.warn('[Token] 未提供刷新函数,跳过刷新')
isRefreshing = false
onRefreshed()
return config
}
refreshTokenFn()
.then(() => {
console.info('[Token] 刷新成功')
isRefreshing = false
onRefreshed()
})
.catch((error) => {
isRefreshing = false
onRefreshed()
console.error('[Token] 刷新失败:', error.message)
if (error.code === 401 && error.needRelogin) {
console.info('[Token] refreshToken无效需要重新登录')
}
})
refreshTokenFn().finally(() => {
isRefreshing = false
onRefreshed()
})
}
// 等待刷新完成
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
subscribeTokenRefresh(() => {
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
const token = tokenManager.getAccessToken()
if (!token) {
return reject({
message: 'Token已过期需要重新登录',
code: 401,
})
}
config.headers.Authorization = tokenManager.getAuthHeader()
resolve(config)
})
})
@@ -199,24 +119,21 @@ export function createClientAxios(options = {}) {
(response) => {
const data = response.data
// 检查业务状态码
if (data && typeof data.code === 'number') {
if (data.code === 0 || data.code === 200) {
return data
}
// 创建业务错误对象
const error = new Error(data?.message || data?.msg || '请求失败')
error.code = data?.code
error.data = data
// 业务码 403 只在响应拦截器处理,避免重复处理
if (data.code === 403 && typeof on403 === 'function') {
const forbiddenError = new Error('权限不足,无法访问该资源')
forbiddenError.code = 403
on403(forbiddenError)
on403(error)
}
if (data.code === 401 && typeof on401 === 'function') {
refreshTokenFn && refreshTokenFn()
}
return Promise.reject(error)
}
@@ -224,13 +141,13 @@ export function createClientAxios(options = {}) {
},
(error) => {
const status = error.response?.status
const code = error.code
if (status === 401 && typeof on401 === 'function') {
if ((status === 401 || code === 401) && typeof on401 === 'function') {
on401(error)
} else if (status === 403 && typeof on403 === 'function') {
const forbiddenError = new Error('权限不足,无法访问该资源')
forbiddenError.code = 403
on403(forbiddenError)
}
else if ((status === 403 || code === 403) && typeof on403 === 'function') {
on403(error)
}
return Promise.reject(error)
@@ -238,13 +155,4 @@ export function createClientAxios(options = {}) {
)
return client
}
/**
* 默认导出的 C 端 Axios 实例
* 可在应用层覆盖配置
*/
export const clientAxios = createClientAxios()
export default clientAxios
}