2025-11-13 01:06:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Mono 级别的 C 端 Axios 实例
|
|
|
|
|
|
* 供 monorepo 中所有应用使用的统一 HTTP 客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import axios from 'axios'
|
2025-11-25 00:58:51 +08:00
|
|
|
|
// 直接使用实例(最简单、最可靠)
|
|
|
|
|
|
import tokenManager from '@gold/utils/token-manager'
|
2025-11-13 01:06:28 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 不需要 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) {
|
|
|
|
|
|
if (!url) return false
|
|
|
|
|
|
return WHITE_LIST.some((path) => url.includes(path))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-21 22:24:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 自动刷新 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 = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 401 未授权错误
|
2025-11-25 01:03:20 +08:00
|
|
|
|
* 注意:只做清理工作,不处理重定向(重定向由上层回调处理)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
*/
|
|
|
|
|
|
let isHandling401 = false
|
2025-11-25 01:03:20 +08:00
|
|
|
|
function handle401Error(error) {
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (isHandling401) return
|
2025-11-25 01:03:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
isHandling401 = true
|
2025-11-25 01:03:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
try {
|
2025-11-25 01:03:20 +08:00
|
|
|
|
// 清空token
|
2025-11-25 00:58:51 +08:00
|
|
|
|
tokenManager.clearTokens()
|
2025-11-25 01:03:20 +08:00
|
|
|
|
console.warn('Token已清空,因401错误')
|
2025-11-13 01:06:28 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('清空 token 失败:', e)
|
|
|
|
|
|
}
|
2025-11-25 01:03:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
// 延迟重置标志
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isHandling401 = false
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建 C 端 Axios 实例
|
|
|
|
|
|
* @param {Object} options - 配置选项
|
|
|
|
|
|
* @param {string} options.baseURL - 基础 URL
|
|
|
|
|
|
* @param {number} options.timeout - 超时时间(毫秒)
|
|
|
|
|
|
* @param {Function} options.on401 - 401 错误处理函数
|
|
|
|
|
|
* @param {Function} options.on403 - 403 错误处理函数
|
2025-12-21 22:24:16 +08:00
|
|
|
|
* @param {Function} options.refreshTokenFn - Token 刷新函数(可选)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
* @returns {AxiosInstance} Axios 实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function createClientAxios(options = {}) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
baseURL = '/',
|
|
|
|
|
|
timeout = 180000,
|
|
|
|
|
|
on401 = handle401Error,
|
|
|
|
|
|
on403 = null,
|
2025-12-21 22:24:16 +08:00
|
|
|
|
refreshTokenFn = null,
|
2025-11-13 01:06:28 +08:00
|
|
|
|
} = options
|
|
|
|
|
|
|
|
|
|
|
|
const client = axios.create({
|
|
|
|
|
|
baseURL,
|
|
|
|
|
|
timeout,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 请求拦截器
|
|
|
|
|
|
client.interceptors.request.use((config) => {
|
|
|
|
|
|
// 添加 tenant-id
|
2025-12-21 22:24:16 +08:00
|
|
|
|
const tenantId =
|
2025-11-13 01:06:28 +08:00
|
|
|
|
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_TENANT_ID) ||
|
|
|
|
|
|
(typeof process !== 'undefined' && process.env?.VITE_TENANT_ID) ||
|
|
|
|
|
|
'1'
|
2025-12-21 22:24:16 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (tenantId) {
|
|
|
|
|
|
config.headers['tenant-id'] = tenantId
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-21 22:24:16 +08:00
|
|
|
|
// 检查是否需要认证
|
2025-11-13 01:06:28 +08:00
|
|
|
|
const needToken = config.headers?.isToken !== false && !isInWhiteList(config.url || '')
|
2025-12-21 22:24:16 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (needToken) {
|
2025-12-21 22:24:16 +08:00
|
|
|
|
// 检查 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) {
|
|
|
|
|
|
console.info('[Token] Token刷新')
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不在刷新过程中,启动刷新
|
|
|
|
|
|
if (!isRefreshing) {
|
|
|
|
|
|
isRefreshing = true
|
|
|
|
|
|
|
|
|
|
|
|
// 执行刷新(使用上层传入的刷新函数)
|
|
|
|
|
|
if (refreshTokenFn && typeof refreshTokenFn === 'function') {
|
|
|
|
|
|
refreshTokenFn()
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
console.info('[Token] 刷新成功')
|
|
|
|
|
|
isRefreshing = false
|
|
|
|
|
|
onRefreshed()
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
isRefreshing = false
|
|
|
|
|
|
onRefreshed()
|
|
|
|
|
|
console.error('[Token] 刷新失败:', error.message)
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('[Token] 未提供刷新函数,跳过刷新')
|
|
|
|
|
|
isRefreshing = false
|
|
|
|
|
|
onRefreshed()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待刷新完成
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
subscribeTokenRefresh(() => {
|
|
|
|
|
|
// 刷新完成后,重新获取 token 并添加到请求头
|
|
|
|
|
|
const authHeader = tokenManager.getAuthHeader()
|
|
|
|
|
|
if (authHeader) {
|
|
|
|
|
|
config.headers.Authorization = authHeader
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve(config)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Token 未过期,直接添加 Authorization 头
|
|
|
|
|
|
const authHeader = tokenManager.getAuthHeader()
|
|
|
|
|
|
if (authHeader) {
|
|
|
|
|
|
config.headers.Authorization = authHeader
|
|
|
|
|
|
}
|
2025-11-13 01:06:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 响应拦截器
|
|
|
|
|
|
client.interceptors.response.use(
|
|
|
|
|
|
(response) => {
|
|
|
|
|
|
const data = response.data
|
2025-11-25 01:07:22 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
// 检查业务状态码
|
|
|
|
|
|
if (data && typeof data.code === 'number') {
|
|
|
|
|
|
if (data.code === 0 || data.code === 200) {
|
|
|
|
|
|
return data
|
|
|
|
|
|
}
|
2025-11-25 01:00:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建业务错误对象
|
|
|
|
|
|
const error = new Error(data?.message || data?.msg || '请求失败')
|
|
|
|
|
|
error.code = data?.code
|
|
|
|
|
|
error.data = data
|
|
|
|
|
|
|
2025-11-25 01:07:22 +08:00
|
|
|
|
// 业务码 401/403 只在响应拦截器处理,避免重复处理
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (data.code === 401 && typeof on401 === 'function') {
|
2025-11-25 01:00:20 +08:00
|
|
|
|
on401(error)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
}
|
2025-11-25 01:00:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (data.code === 403 && typeof on403 === 'function') {
|
2025-11-25 01:00:20 +08:00
|
|
|
|
on403(error)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
}
|
2025-11-25 01:00:20 +08:00
|
|
|
|
|
2025-11-25 01:07:22 +08:00
|
|
|
|
// 抛出错误,业务代码可以捕获
|
2025-11-13 01:06:28 +08:00
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
|
}
|
2025-11-25 01:07:22 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
return data
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
2025-11-25 01:07:22 +08:00
|
|
|
|
// HTTP 状态码 401/403 只在错误拦截器处理
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (error.response?.status === 401 && typeof on401 === 'function') {
|
2025-11-25 01:00:20 +08:00
|
|
|
|
on401(error)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
}
|
2025-11-25 01:00:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
if (error.response?.status === 403 && typeof on403 === 'function') {
|
2025-11-25 01:00:20 +08:00
|
|
|
|
on403(error)
|
2025-11-13 01:06:28 +08:00
|
|
|
|
}
|
2025-11-25 01:00:20 +08:00
|
|
|
|
|
2025-11-13 01:06:28 +08:00
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 默认导出的 C 端 Axios 实例
|
|
|
|
|
|
* 可在应用层覆盖配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const clientAxios = createClientAxios()
|
|
|
|
|
|
|
|
|
|
|
|
export default clientAxios
|
|
|
|
|
|
|