/** * 地磅(db)模块 HTTP 客户端 * 协议:直接 JSON 响应(与原 simple-dibang-xiaoshou 一致,非门户 code/data 信封) */ import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/store/modules/user' import { fetchRefreshToken } from '@/api/auth' type RetryableConfig = InternalAxiosRequestConfig & { _retry?: boolean /** 为 false 时不弹出全局错误提示(由调用方自行处理) */ showErrorMessage?: boolean } /** 地磅集合根路径(后端 @router.get("/") 注册,须带尾斜杠,否则 307 重定向会丢 Authorization) */ const DB_COLLECTION_ROOT = /^\/(plans|inventory|weighing)$/ function normalizeDbUrl(url?: string): string | undefined { if (!url || url.endsWith('/')) return url return DB_COLLECTION_ROOT.test(url) ? `${url}/` : url } const dbAxios = axios.create({ baseURL: '/api', timeout: 60000, headers: { 'Content-Type': 'application/json' } }) 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 = [] } /** 从 localStorage 兜底读取 accessToken(兼容 Pinia persist 的版本化 key) */ function readTokenFallback(): string { if (typeof localStorage === 'undefined') return '' try { for (const k of Object.keys(localStorage)) { if (!k.startsWith('sys-') || !k.includes('-user')) continue const raw = localStorage.getItem(k) if (!raw) continue const parsed = JSON.parse(raw) as { accessToken?: unknown } const token = parsed?.accessToken if (typeof token === 'string' && token.trim()) return token.trim() } } catch { // ignore } return '' } dbAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { if (config.url) { config.url = normalizeDbUrl(config.url) } const store = useUserStore() let token = store.accessToken as unknown as string if (!token) { token = readTokenFallback() } if (token) { if (typeof config.headers.set === 'function') { config.headers.set('Authorization', `Bearer ${token}`) } else { config.headers.Authorization = `Bearer ${token}` } } return config }) dbAxios.interceptors.response.use( (res) => res.data, async (err) => { const status = err.response?.status const config = err.config as RetryableConfig | undefined if (status === 401 && config && !config._retry) { const userStore = useUserStore() const storedRefreshToken = userStore.refreshToken if (storedRefreshToken) { if (isRefreshing) { return new Promise((resolve, reject) => { subscribeTokenRefresh( (newToken: string) => { if (typeof config.headers.set === 'function') { config.headers.set('Authorization', `Bearer ${newToken}`) } else { config.headers.Authorization = `Bearer ${newToken}` } config._retry = true resolve(dbAxios.request(config)) }, (refreshError: unknown) => reject(refreshError) ) }) } isRefreshing = true config._retry = true try { const result = await fetchRefreshToken(storedRefreshToken) userStore.setToken(result.token, result.refreshToken) onRefreshed(result.token) if (typeof config.headers.set === 'function') { config.headers.set('Authorization', `Bearer ${result.token}`) } else { config.headers.Authorization = `Bearer ${result.token}` } return dbAxios.request(config) } catch (refreshError) { onRefreshFailed(refreshError) await userStore.logOut() return Promise.reject(refreshError) } finally { isRefreshing = false } } await userStore.logOut() return Promise.reject(err) } const showErrorMessage = config?.showErrorMessage !== false if (showErrorMessage) { const detail = err.response?.data?.detail const message = typeof detail === 'string' ? detail : Array.isArray(detail) ? detail.map((x: { msg?: string }) => x.msg).filter(Boolean).join(';') : err.response?.data?.msg ElMessage.error(message || err.message || '网络异常') } return Promise.reject(err) } ) function request(config: AxiosRequestConfig) { return dbAxios.request(config) } const dbRequest = { get(url: string, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'GET' }) }, post(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'POST', data }) }, put(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'PUT', data }) }, patch(url: string, data?: unknown, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'PATCH', data }) }, delete(url: string, config?: AxiosRequestConfig) { return request({ ...config, url, method: 'DELETE' }) } } export default dbRequest