Files
sionrui/frontend/api/axios/client.js
2026-01-18 02:55:02 +08:00

251 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Mono 级别的 C 端 Axios 实例
* 供 monorepo 中所有应用使用的统一 HTTP 客户端
*/
import axios from 'axios'
// 直接使用实例(最简单、最可靠)
import tokenManager from '@gold/utils/token-manager'
/**
* 不需要 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',
]
/**
* 检查 URL 是否在白名单中
*/
function isInWhiteList(url) {
return !!(url && WHITE_LIST.some(path => url.includes(path)))
}
/**
* 自动刷新 token 的锁机制和队列
*/
let isRefreshing = false
let refreshSubscribers = []
/**
* 订阅 token 刷新完成
* @param {Function} callback - 回调函数
*/
function subscribeTokenRefresh(callback) {
if (isRefreshing) {
refreshSubscribers.push(callback)
} else {
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 实例
* @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 实例
*/
export function createClientAxios(options = {}) {
const {
baseURL = '/',
timeout = 180000,
on401 = handle401Error,
on403 = null,
refreshTokenFn = null,
} = options
const client = axios.create({
baseURL,
timeout,
})
// 请求拦截器
client.interceptors.request.use((config) => {
// 添加 tenant-id
const tenantId =
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) ||
(typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) ||
'1'
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)
if (!isTokenExpired) {
// Token 未过期,直接添加 Authorization 头
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
}
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无效需要重新登录')
}
})
}
// 等待刷新完成
return new Promise((resolve) => {
subscribeTokenRefresh(() => {
const authHeader = tokenManager.getAuthHeader()
if (authHeader) {
config.headers.Authorization = authHeader
}
resolve(config)
})
})
})
// 响应拦截器
client.interceptors.response.use(
(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)
}
return Promise.reject(error)
}
return data
},
(error) => {
const status = error.response?.status
if (status === 401 && typeof on401 === 'function') {
on401(error)
} else if (status === 403 && typeof on403 === 'function') {
const forbiddenError = new Error('权限不足,无法访问该资源')
forbiddenError.code = 403
on403(forbiddenError)
}
return Promise.reject(error)
}
)
return client
}
/**
* 默认导出的 C 端 Axios 实例
* 可在应用层覆盖配置
*/
export const clientAxios = createClientAxios()
export default clientAxios