import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios'; import { mockData, type MockApiResponse } from './mock'; import { API_BASE_URL, DOCUMENT_URL } from '../config/api-config'; import { toastService } from '../components/ui/Toast'; /** * API响应类型 */ export type ApiResponse = { data?: T; error?: string; status: number; headers?: Record; }; export type QueryParams = Record; // 获取 API 基础 URL (从配置文件导入) // const API_BASE_URL = 'http://172.16.0.58:8008'; // const API_BASE_URL = 'http://nas.7bm.co:3000'; // const API_BASE_URL = 'http://172.18.0.100:3000'; // const API_BASE_URL = 'http://172.16.0.119:9000/admin'; // 调试:打印当前API_BASE_URL的值 console.log('🔍 axios-client.ts - API_BASE_URL:', API_BASE_URL); // 文档URL前缀 (从配置文件导入) // export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/'; export { DOCUMENT_URL }; // 是否使用模拟数据(开发环境使用) const USE_MOCK_DATA = false; // 设置为true使用模拟数据,避免API连接问题 // 设置超时时间(毫秒) const DEFAULT_TIMEOUT = 30000; // 增加到30秒 // 创建 axios 实例 const axiosInstance = axios.create({ baseURL: API_BASE_URL, timeout: DEFAULT_TIMEOUT, // 增加超时时间 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); // 请求白名单 - 这些接口不需要添加 Authorization 头 const AUTH_WHITELIST = [ '/auth/login', '/auth/refresh', '/auth/register', '/oauth/token', '/oauth/userinfo' ]; // 错误容忍白名单 - 这些接口即使返回 401/403 也不触发强制登出 const ERROR_TOLERANT_WHITELIST = [ '/admin/statistics/top-error-points', '/admin/statistics/top-risk-users' ]; /** * 检查请求URL是否在白名单中 */ function isInAuthWhitelist(url?: string): boolean { if (!url) return false; return AUTH_WHITELIST.some(path => url.includes(path)); } /** * 检查请求URL是否在错误容忍白名单中 */ function isInErrorTolerantWhitelist(url?: string): boolean { if (!url) return false; return ERROR_TOLERANT_WHITELIST.some(path => url.includes(path)); } /** * 请求拦截器 - 自动添加 Authorization 头 */ axiosInstance.interceptors.request.use( (config) => { // 检查是否在白名单中 if (isInAuthWhitelist(config.url)) { console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url); return config; } // 从 localStorage 获取 token (浏览器环境) if (typeof window !== 'undefined') { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; // console.log('🔑 [Request Interceptor] 添加Authorization头:', { // url: config.url, // method: config.method, // hasToken: !!token, // tokenPreview: token.substring(0, 20) + '...' // }); } else { console.warn('⚠️ [Request Interceptor] 没有找到access_token:', { url: config.url, localStorage: Object.keys(localStorage) }); } } return config; }, (error) => { console.error('❌ [Request Interceptor] 请求拦截器错误:', error); return Promise.reject(error); } ); /** * 自定义错误类:表示需要重新登录 */ export class AuthenticationError extends Error { constructor(message = 'Token 已过期或无效,请重新登录') { super(message); this.name = 'AuthenticationError'; } } /** * 响应拦截器 - 处理 401 错误(token 过期) */ axiosInstance.interceptors.response.use( (response) => { // console.log('✅ [Response Interceptor] 请求成功:', { // url: response.config.url, // status: response.status, // statusText: response.statusText // }); return response; }, (error) => { console.error('❌ [Response Interceptor] 请求失败:', { url: error.config?.url, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data }); if (isAxiosError(error) && error.response?.status === 401) { // 检查是否在错误容忍白名单中 const requestUrl = error.config?.url; if (isInErrorTolerantWhitelist(requestUrl)) { console.warn('⚠️ [容错白名单] 接口返回 401,但不触发强制登出:', requestUrl); // 直接返回错误,不触发登出 return Promise.reject(error); } // Token 过期或无效 console.warn('⚠️ Token 已过期或无效,请重新登录'); console.warn('⚠️ 401 错误详情:', { url: requestUrl, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, headers: error.response?.headers }); if (typeof window !== 'undefined') { // 🌐 客户端环境:清除 localStorage 并跳转 localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; } else { // 🖥️ 服务端环境:抛出特殊错误,由 loader/action 处理 console.warn('⚠️ [Server] 检测到 401 错误,抛出 AuthenticationError'); throw new AuthenticationError('Token 已过期或无效,请重新登录'); } } // 🔒 403 无权限错误处理 if (isAxiosError(error) && error.response?.status === 403) { console.warn('⚠️ [403 Forbidden] 无权限访问:', error.config?.url); // 只在客户端显示 toast 提示 if (typeof window !== 'undefined') { toastService.warning('无权限访问该资源'); } } return Promise.reject(error); } ); // 最大重试次数 const MAX_RETRIES = 2; /** * 带重试功能的请求方法 * @param config Axios请求配置 * @param retries 当前重试次数 * @returns Axios响应 */ async function axiosRetry(config: AxiosRequestConfig, retries = 0): Promise { try { return await axiosInstance(config); } catch (error) { if (isAxiosError(error) && error.code === 'ECONNABORTED' && retries < MAX_RETRIES) { console.log(`请求超时,第${retries + 1}次重试...`); // 递增重试次数并重新发送请求 return axiosRetry(config, retries + 1); } throw error; } } /** * 将编码后的URL解码为可读格式 * 当前已注释掉日志,暂未使用此函数 * @param url 编码后的URL * @returns 解码后的可读URL * @unused 保留供将来使用 */ // function decodeUrlForDisplay(url: string): string { // try { // // 首先解码整个URL // const decodedUrl = decodeURIComponent(url); // // // 如果URL中包含@符号作为前缀,则移除它 // if (decodedUrl.startsWith('@')) { // return decodedUrl.substring(1); // } // // return decodedUrl; // } catch (error) { // // 如果解码失败,返回原始URL // console.error('URL解码失败:', error); // return url; // } // } /** * 构建完整的 API URL */ function buildUrl(endpoint: string, params?: QueryParams): string { let fullUrl; // 检查endpoint是否已经是完整URL if (endpoint.startsWith('http')) { fullUrl = endpoint; } else { // 处理相对路径的情况 if (API_BASE_URL === '/api') { // 如果是相对路径,直接使用endpoint const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; fullUrl = path; } else { // 确保API_BASE_URL格式正确 const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; fullUrl = `${baseUrl}${path}`; } } try { // 创建URL对象 const url = new URL(fullUrl); // 添加查询参数 if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { // 处理数组参数:使用逗号分隔 if (Array.isArray(value)) { url.searchParams.append(key, value.join(',')); } else { url.searchParams.append(key, String(value)); } } }); } return url.toString(); } catch (error) { console.error('URL构建错误:', fullUrl, error); throw new Error(`无法构建URL: ${fullUrl}, 错误: ${error}`); } } /** * 获取模拟响应数据 */ function getMockResponse(endpoint: string): ApiResponse { // console.log(`[开发模式] 使用模拟数据: ${endpoint}`); // 移除开头的斜杠以便于匹配 const path = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 查找匹配的模拟数据 for (const mockPath in mockData) { const normalizedMockPath = mockPath.startsWith('/') ? mockPath.substring(1) : mockPath; if (path === normalizedMockPath || path.startsWith(normalizedMockPath + '/')) { // 如果是详情查询 (如 /evaluation_points/1) if (path.includes('/') && path !== normalizedMockPath) { const id = parseInt(path.split('/')[1]); const mockDataItem = mockData[mockPath as keyof typeof mockData] as MockApiResponse; if (Array.isArray(mockDataItem.data)) { const item = mockDataItem.data.find((item: {id: number}) => item.id === id); if (item) { return { data: { code: 0, msg: "成功", data: item } as unknown as T, status: 200 }; } } return { error: '未找到数据', status: 404 }; } // 返回列表数据 return { data: mockData[mockPath as keyof typeof mockData] as unknown as T, status: 200 }; } } return { error: '没有匹配的模拟数据', status: 404 }; } /** * 扩展AxiosRequestConfig类型,以支持body参数 */ interface ExtendedAxiosRequestConfig extends AxiosRequestConfig { body?: string; } /** * 通用 API 请求函数 */ export async function apiRequest( endpoint: string, options: ExtendedAxiosRequestConfig = {}, params?: QueryParams ): Promise> { // 如果使用模拟数据,直接返回模拟响应 if (USE_MOCK_DATA) { return getMockResponse(endpoint); } // console.log('api-base-url-----------',API_BASE_URL) try { // 构建 URL const url = buildUrl(endpoint, params); // 只有在 options.headers 存在时才处理,否则让拦截器处理 let headers = options.headers; if (headers) { // 设置默认请求头(仅当 headers 已存在时) if (!headers['Content-Type'] && options.method !== 'GET') { headers['Content-Type'] = 'application/json'; } if (!headers['Accept']) { headers['Accept'] = 'application/json'; } } // 针对 PostgREST 的额外处理 if (endpoint.includes('evaluation_point_groups') && (options.method === 'POST' || options.method === 'PATCH')) { // console.log('使用 PostgREST 特定配置处理请求'); // 确保请求体是有效的 JSON 对象 if (options.body && typeof options.body === 'string') { try { JSON.parse(options.body); // 验证 JSON 是否有效 } catch (e) { console.error('请求体不是有效的 JSON:', options.body); throw new Error('请求体必须是有效的 JSON'); } } } // console.log(`📦 axios-client.ts->请求URL: ${url}`); // console.log(`axios-client.ts->发送 ${options.method || 'GET'} 请求到: ${url}`); // 处理body参数,转换为data if (options.body) { // console.log(`axios-client.ts->请求体: \n${options.body}`); options.data = options.body; } else if (options.data) { // console.log(`axios-client.ts->请求体: \n${typeof options.data === 'string' ? options.data : JSON.stringify(options.data)}`); } // 构建请求配置 // 如果没有传入 headers,就不设置 headers,让拦截器自动添加 const config: AxiosRequestConfig = { ...options, url, // 确保使用默认超时时间 timeout: options.timeout || DEFAULT_TIMEOUT }; // 只有在 headers 存在时才设置 if (headers) { config.headers = headers; // 🔍 调试:打印 Authorization 头 if (headers['Authorization']) { // console.log('🔑 [apiRequest] 请求包含 Authorization 头:', headers['Authorization'].substring(0, 20) + '...'); } else { console.warn('⚠️ [apiRequest] 请求缺少 Authorization 头!headers:', Object.keys(headers)); } } // console.log(`📦 axios-client.ts->请求配置: \n${JSON.stringify(config)}`); // 删除body属性,避免axios警告 if ('body' in config) { delete (config as ExtendedAxiosRequestConfig).body; } // 使用带重试功能的请求方法 const response: AxiosResponse = await axiosRetry(config); // 收集响应头信息 const responseHeaders: Record = {}; Object.entries(response.headers).forEach(([key, value]) => { if (typeof value === 'string') { responseHeaders[key] = value; } }); // 打印响应信息 // console.log(`响应状态: ${response.status}`); // console.log(`响应头:`, responseHeaders); // console.log(`响应体:`, response.data); // 检查 PostgREST 特定错误 if (response.status >= 400) { if (response.status === 400) { console.error('PostgREST 错误 - 无效请求:', response.data); return { error: response.data?.message || response.data?.msg || '无效的请求格式,请检查数据格式是否正确', status: response.status, headers: responseHeaders }; } else if (response.status === 401) { console.error('PostgREST 错误 - 未授权:', response.data); return { error: response.data?.message || response.data?.msg || '未授权,请检查认证信息', status: response.status, headers: responseHeaders }; } else if (response.status === 403) { console.error('PostgREST 错误 - 禁止访问:', response.data); return { error: response.data?.message || response.data?.msg || '没有权限执行此操作', status: response.status, headers: responseHeaders }; } else if (response.status === 404) { console.error('PostgREST 错误 - 资源不存在:', response.data); return { error: response.data?.message || response.data?.msg || '请求的资源不存在', status: response.status, headers: responseHeaders }; } else { console.error(`HTTP请求失败: ${response.status} - ${url}`, response.data); return { error: response.data?.message || response.data?.msg || `请求失败: ${response.status}`, status: response.status, headers: responseHeaders }; } } // 检查API返回的状态码 const data = response.data; // 修复:支持code=0(PostgREST)和code=200(RBAC API)两种成功响应 // 🔑 只有当 code 是数字类型时才认为是 API 状态码,字符串 code 是业务编码 if (data && typeof data === 'object' && 'code' in data && typeof data.code === 'number' && data.code !== 0 && data.code !== 200) { const errorMessage = data.message || data.msg || '未知错误'; console.error(`API请求失败: ${errorMessage} - ${url}`); // 🔑 检测令牌过期错误 const isTokenExpired = errorMessage.includes('令牌已过期') || errorMessage.includes('令牌') || errorMessage.includes('token') || errorMessage.includes('expired') || errorMessage.includes('认证') || errorMessage.includes('未授权'); if (isTokenExpired) { console.error('🔑 [API Client] 检测到令牌过期,准备清除会话并重定向...'); // 只在客户端执行重定向 if (typeof window !== 'undefined') { console.error('🔑 [API Client] 客户端环境,清除 localStorage 并重定向到登录页'); // 清除所有认证相关数据 localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); sessionStorage.clear(); // 重定向到登录页 window.location.href = '/login?expired=true'; } } return { error: errorMessage, status: response.status, headers: responseHeaders }; } return { data, status: response.status, headers: responseHeaders }; } catch (error: unknown) { console.error('API请求失败:', error); if (isAxiosError(error)) { const status = error.response?.status || 500; const errorData = error.response?.data; // 如果超时或网络错误,使用模拟数据(仅开发环境) if (error.code === 'ECONNABORTED' && process.env.NODE_ENV !== 'production') { console.warn('自动使用模拟数据作为回退'); return getMockResponse(endpoint); } return { error: errorData?.message || errorData?.msg || error.message || '未知错误', status }; } // 如果超时或网络错误,使用模拟数据(仅开发环境) if (process.env.NODE_ENV !== 'production') { console.warn('自动使用模拟数据作为回退'); return getMockResponse(endpoint); } return { error: error instanceof Error ? error.message : '未知错误', status: 500 }; } } // GET请求简化方法 export async function get(endpoint: string, params?: QueryParams): Promise> { return apiRequest(endpoint, { method: 'GET' }, params); } // POST请求简化方法 export async function post(endpoint: string, data?: unknown, params?: QueryParams): Promise> { return apiRequest(endpoint, { method: 'POST', data }, params); } // PUT请求简化方法 export async function put(endpoint: string, data?: unknown, params?: QueryParams): Promise> { return apiRequest(endpoint, { method: 'PUT', data }, params); } // PATCH请求简化方法 export async function patch(endpoint: string, data?: unknown, params?: QueryParams): Promise> { return apiRequest(endpoint, { method: 'PATCH', data }, params); } // DELETE请求简化方法 export async function del(endpoint: string, params?: QueryParams): Promise> { return apiRequest(endpoint, { method: 'DELETE' }, params); } // 下载文件的方法 export async function downloadFile(path: string): Promise { // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`; try { // console.log(`📦 axios-client.ts->下载文件: ${downloadUrl}`); const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } return await response.blob(); } catch (error) { console.error('下载文件失败:', error); throw error; } }