import { useUserStore, checkAdminPermission, checkSuperAdminPermission } from '@/stores/user' import { message } from 'ant-design-vue' /** * 基础API请求封装 * 提供统一的请求方法,自动处理认证头和错误 */ // 默认请求超时时间(毫秒) const DEFAULT_TIMEOUT = 30000 /** * 提取并拼接 422 验证错误详情为用户可读的中文提示 * 后端返回格式可能为 { detail: [{ msg, loc }] } 或 { detail: { field: [errors] } } 或字符串 * @param {*} detail - 后端返回的验证错误详情 * @returns {string} 拼接后的错误信息 */ function formatValidationError(detail) { if (!detail) return '请求参数验证失败' if (typeof detail === 'string') return detail if (Array.isArray(detail)) { return detail .map((err) => { if (typeof err === 'string') return err const field = err.loc?.slice(-1)?.[0] ?? err.field ?? '' const msg = err.msg ?? err.message ?? '' return field ? `${field}: ${msg}` : msg }) .filter(Boolean) .join('; ') } if (typeof detail === 'object') { return Object.entries(detail) .map(([field, errors]) => { const msg = Array.isArray(errors) ? errors.join(', ') : String(errors) return `${field}: ${msg}` }) .join('; ') } return '请求参数验证失败' } /** * 发送API请求的基础函数 * @param {string} url - API端点 * @param {Object} options - 请求选项 * @param {boolean} requiresAuth - 是否需要认证头 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export async function apiRequest(url, options = {}, requiresAuth = true, responseType = 'json') { // 超时控制:默认 30s,可通过 options.timeout 自定义;options.signal 支持外部主动取消 const { timeout = DEFAULT_TIMEOUT, signal: userSignal, ...restOptions } = options const controller = new AbortController() let isTimeout = false const timeoutId = setTimeout(() => { isTimeout = true controller.abort() }, timeout) // 联动用户传入的 signal,支持外部主动取消 if (userSignal) { if (userSignal.aborted) controller.abort() else { const onAbort = () => controller.abort() userSignal.addEventListener('abort', onAbort, { once: true }) } } try { const isFormData = options?.body instanceof FormData // 默认请求配置 const requestOptions = { ...restOptions, signal: controller.signal, headers: { ...(!isFormData ? { 'Content-Type': 'application/json' } : {}), ...options.headers } } // 如果需要认证,添加认证头 if (requiresAuth) { const userStore = useUserStore() if (!userStore.isLoggedIn) { throw new Error('用户未登录') } Object.assign(requestOptions.headers, userStore.getAuthHeaders()) } // 发送请求 let response try { response = await fetch(url, requestOptions) } catch (fetchError) { // 超时:AbortError 且 isTimeout 标志为真 if (fetchError.name === 'AbortError') { if (isTimeout) { message.error('请求超时,请稍后重试') throw new Error('请求超时,请稍后重试', { cause: fetchError }) } // 用户主动取消,静默抛出 throw fetchError } // TypeError 通常是网络中断 / DNS 失败 / CORS if (fetchError instanceof TypeError) { message.error('网络连接失败,请检查网络') throw new Error('网络连接失败,请检查网络', { cause: fetchError }) } throw fetchError } // 处理API返回的错误 if (!response.ok) { // 尝试解析错误信息 let errorMessage = `请求失败: ${response.status}, ${response.statusText}` let errorData = null try { errorData = await response.json() errorMessage = errorData.detail || errorData.message || errorMessage // 422 验证错误:提取字段错误并提示用户 if (response.status === 422) { console.error('422验证错误:', { url, status: response.status, responseData: errorData }) message.error(formatValidationError(errorData.detail)) } } catch { // 如果无法解析JSON,使用默认错误信息 } // 特殊处理401和403错误 const error = new Error(errorMessage) error.response = { status: response.status, statusText: response.statusText, data: errorData } if (response.status === 401) { // 如果是认证失败,可能需要重新登录 const userStore = useUserStore() // 检查是否是token过期(detail 可能为字符串或对象) const detailStr = typeof errorData?.detail === 'string' ? errorData.detail : '' const isTokenExpired = detailStr.includes('令牌已过期') || detailStr.includes('token expired') || errorMessage?.includes('令牌已过期') || errorMessage?.includes('token expired') message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录') // 如果用户当前认为自己已登录,则登出 if (userStore.isLoggedIn) { userStore.logout() } // 使用setTimeout确保消息显示后再跳转 setTimeout(() => { window.location.href = '/login' }, 1500) throw error } else if (response.status === 403) { error.message = '没有权限执行此操作' throw error } else if (response.status === 500) { // 提取后端统一错误响应中的 message 和 trace_id(格式:{success, error: {code, message, trace_id, details}}) const serverError = errorData?.error const traceId = serverError?.trace_id error.message = serverError?.message || '服务器内部错误,请稍后重试' if (traceId) { error.traceId = traceId // 控制台记录 trace_id 便于开发者排查,不暴露给用户 console.error(`[500] trace_id=${traceId}`, { url, status: response.status }) } throw error } throw error } // 根据responseType处理响应 if (responseType === 'blob') { return response } else if (responseType === 'json') { // 检查Content-Type以确定如何处理响应 const contentType = response.headers.get('Content-Type') if (contentType && contentType.includes('application/json')) { return await response.json() } return await response.text() } else if (responseType === 'text') { return await response.text() } else { return response } } catch (error) { if (error.name !== 'AbortError') { console.error('API请求错误:', error) } throw error } finally { clearTimeout(timeoutId) } } /** * 发送GET请求 * @param {string} url - API端点 * @param {Object} options - 请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export function apiGet(url, options = {}, requiresAuth = true, responseType = 'json') { return apiRequest(url, { method: 'GET', ...options }, requiresAuth, responseType) } export function apiAdminGet(url, options = {}, responseType = 'json') { checkAdminPermission() return apiGet(url, options, true, responseType) } export function apiSuperAdminGet(url, options = {}, responseType = 'json') { checkSuperAdminPermission() return apiGet(url, options, true, responseType) } /** * 发送POST请求 * @param {string} url - API端点 * @param {Object} data - 请求体数据 * @param {Object} options - 其他请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export function apiPost(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') { return apiRequest( url, { method: 'POST', body: data instanceof FormData ? data : JSON.stringify(data), ...options }, requiresAuth, responseType ) } export function apiAdminPost(url, data = {}, options = {}, responseType = 'json') { checkAdminPermission() return apiPost(url, data, options, true, responseType) } export function apiSuperAdminPost(url, data = {}, options = {}, responseType = 'json') { checkSuperAdminPermission() return apiPost(url, data, options, true, responseType) } /** * 发送PUT请求 * @param {string} url - API端点 * @param {Object} data - 请求体数据 * @param {Object} options - 其他请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export function apiPut(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') { return apiRequest( url, { method: 'PUT', body: data instanceof FormData ? data : JSON.stringify(data), ...options }, requiresAuth, responseType ) } export function apiAdminPut(url, data = {}, options = {}, responseType = 'json') { checkAdminPermission() return apiPut(url, data, options, true, responseType) } export function apiSuperAdminPut(url, data = {}, options = {}, responseType = 'json') { checkSuperAdminPermission() return apiPut(url, data, options, true, responseType) } /** * 发送PATCH请求 * @param {string} url - API端点 * @param {Object} data - 请求体数据 * @param {Object} options - 其他请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export function apiPatch(url, data = {}, options = {}, requiresAuth = true, responseType = 'json') { return apiRequest( url, { method: 'PATCH', body: data instanceof FormData ? data : JSON.stringify(data), ...options }, requiresAuth, responseType ) } export function apiAdminPatch(url, data = {}, options = {}, responseType = 'json') { checkAdminPermission() return apiPatch(url, data, options, true, responseType) } export function apiSuperAdminPatch(url, data = {}, options = {}, responseType = 'json') { checkSuperAdminPermission() return apiPatch(url, data, options, true, responseType) } /** * 发送DELETE请求 * @param {string} url - API端点 * @param {Object} options - 请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @param {string} responseType - 响应类型: 'json' | 'text' | 'blob' * @returns {Promise} - 请求结果 */ export function apiDelete(url, options = {}, requiresAuth = true, responseType = 'json') { return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth, responseType) } export function apiAdminDelete(url, options = {}) { checkAdminPermission() return apiDelete(url, options, true) } export function apiSuperAdminDelete(url, options = {}) { checkSuperAdminPermission() return apiDelete(url, options, true) }