Files
admin-web/src/shared/api/http.ts
T

157 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-07-30 17:59:43 +08:00
import axios, { type InternalAxiosRequestConfig } from 'axios'
2026-07-30 20:34:08 +08:00
import { parseApiJsonResponse } from '@chongchong/shared'
2026-07-30 17:59:43 +08:00
export interface ApiEnvelope<T> {
code: string
message: string
data: T
requestId?: string | null
traceId?: string | null
timestamp?: string
}
export class ApiResponseError extends Error {
readonly code: string
readonly status?: number
readonly details?: unknown
readonly requestId?: string
readonly traceId?: string
constructor(message: string, code: string, status?: number, details?: unknown, requestId?: string | null, traceId?: string | null) {
super(message)
this.name = 'ApiResponseError'
this.code = code
this.status = status
this.details = details
this.requestId = requestId || undefined
this.traceId = traceId || undefined
}
}
export interface ApiErrorDetails {
message: string
code?: string
requestId?: string
traceId?: string
}
export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15_000,
2026-07-30 20:34:08 +08:00
transformResponse: [(data: unknown) => parseApiJsonResponse(data)],
2026-07-30 17:59:43 +08:00
})
type RetryableRequestConfig = InternalAxiosRequestConfig & { _authRetry?: boolean }
type AuthRefreshHandler = () => Promise<string>
2026-07-31 13:43:16 +08:00
type AuthSessionExpiredHandler = () => void | Promise<void>
2026-07-30 17:59:43 +08:00
let authRefreshHandler: AuthRefreshHandler | null = null
2026-07-31 13:43:16 +08:00
let authSessionExpiredHandler: AuthSessionExpiredHandler | null = null
2026-07-30 17:59:43 +08:00
let activeRefresh: Promise<string> | null = null
export function registerAuthRefreshHandler(handler: AuthRefreshHandler) {
authRefreshHandler = handler
}
2026-07-31 13:43:16 +08:00
export function registerAuthSessionExpiredHandler(handler: AuthSessionExpiredHandler) {
authSessionExpiredHandler = handler
}
2026-07-30 17:59:43 +08:00
http.interceptors.request.use((config) => {
const token = localStorage.getItem('cc-admin-token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
http.interceptors.response.use(
(response) => response.data,
async (error) => {
const config = error.config as RetryableRequestConfig | undefined
const isAuthEndpoint = typeof config?.url === 'string' && config.url.startsWith('/auth/')
if (error.response?.status !== 401 || !config || config._authRetry || isAuthEndpoint || !authRefreshHandler) {
return Promise.reject(error)
}
config._authRetry = true
try {
if (!activeRefresh) {
2026-07-31 13:43:16 +08:00
activeRefresh = authRefreshHandler()
.catch(async (refreshError) => {
try {
await authSessionExpiredHandler?.()
} catch {
// 会话已清理,跳转失败不应覆盖原始认证错误。
}
throw refreshError
})
.finally(() => { activeRefresh = null })
2026-07-30 17:59:43 +08:00
}
const nextToken = await activeRefresh
config.headers.Authorization = `Bearer ${nextToken}`
return http.request(config)
} catch {
return Promise.reject(error)
}
},
)
export function unwrapApiResponse<T>(response: ApiEnvelope<T>): T {
if (response?.code !== '200') {
2026-07-30 17:59:43 +08:00
throw new ApiResponseError(
response?.message || '接口请求失败',
response?.code || 'UNKNOWN',
undefined,
response?.data,
response?.requestId,
response?.traceId,
)
}
return response.data
}
export function getApiErrorDetails(error: unknown, fallback = '接口请求失败'): ApiErrorDetails {
if (error instanceof ApiResponseError) {
2026-08-12 14:08:33 +08:00
const message = error.code === 'A0401'
? '登录状态已失效,请重新登录'
: error.code === 'A0403'
? '当前账号无权访问该数据,请联系管理员确认权限'
: error.code.startsWith('S05')
? '服务暂时不可用,请稍后重试'
: error.message
return { message, code: error.code, requestId: error.requestId, traceId: error.traceId }
2026-07-30 17:59:43 +08:00
}
if (axios.isAxiosError(error)) {
const response = error.response?.data as {
code?: string
message?: string
error?: string
requestId?: string | null
traceId?: string | null
} | undefined
2026-08-04 14:15:32 +08:00
const status = error.response?.status
2026-08-12 14:08:33 +08:00
const statusMessage = status === 401
? '登录状态已失效,请重新登录'
: status === 403
? '当前账号无权访问该数据,请联系管理员确认权限'
: status === 413
? '上传文件的请求体过大,服务器已拒绝;请压缩后重试,如文件未超过 5MB,请联系管理员检查网关上传限制'
: status && status >= 500
? '服务暂时不可用,请稍后重试'
: undefined
2026-07-30 17:59:43 +08:00
return {
2026-08-12 14:08:33 +08:00
message: statusMessage || response?.message || response?.error || fallback,
2026-07-30 17:59:43 +08:00
code: response?.code || (error.response?.status ? `HTTP_${error.response.status}` : undefined),
requestId: response?.requestId || undefined,
traceId: response?.traceId || undefined,
}
}
2026-08-12 12:35:32 +08:00
// Local/runtime errors are not user-facing API messages and may contain stack details.
return { message: fallback }
2026-07-30 17:59:43 +08:00
}
export function getApiErrorMessage(error: unknown, fallback = '接口请求失败'): string {
return getApiErrorDetails(error, fallback).message
}