/** * API 客户端 * 使用 fetch API 进行 HTTP 请求 * 支持请求/响应拦截器 */ import AsyncStorage from '@react-native-async-storage/async-storage'; import { CommonActions } from '@react-navigation/native'; import Constants from 'expo-constants'; // 生产地址 https://bbs.littlelan.cn const getBaseUrl = () => { const configuredBaseUrl = Constants.expoConfig?.extra?.apiBaseUrl; if (typeof configuredBaseUrl === 'string' && configuredBaseUrl.trim().length > 0) { return configuredBaseUrl; } return 'https://bbs.littlelan.cn/api/v1'; }; const BASE_URL = getBaseUrl(); const SSE_URL = `${BASE_URL.replace(/\/+$/, '')}/realtime/sse`; // Token 存储键 const TOKEN_KEY = 'auth_token'; const REFRESH_TOKEN_KEY = 'refresh_token'; // API 响应格式 export interface ApiResponse { code: number; message: string; data: T; } // 分页响应格式 export interface PaginatedData { list: T[]; total: number; page: number; page_size: number; total_pages: number; } // 错误类型 export class ApiError extends Error { code: number; message: string; constructor(code: number, message: string) { super(message); this.code = code; this.message = message; this.name = 'ApiError'; } } // API 客户端类 class ApiClient { private baseUrl: string; private navigation: any = null; constructor(baseUrl: string) { this.baseUrl = baseUrl; } // 设置导航对象(用于处理401跳转) setNavigation(navigation: any) { this.navigation = navigation; } // 获取 token async getToken(): Promise { try { return await AsyncStorage.getItem(TOKEN_KEY); } catch (error) { console.error('获取token失败:', error); return null; } } // 保存 token async setToken(token: string): Promise { try { await AsyncStorage.setItem(TOKEN_KEY, token); } catch (error) { console.error('保存token失败:', error); } } // 获取刷新 token async getRefreshToken(): Promise { try { return await AsyncStorage.getItem(REFRESH_TOKEN_KEY); } catch (error) { console.error('获取刷新token失败:', error); return null; } } // 保存刷新 token async setRefreshToken(token: string): Promise { try { await AsyncStorage.setItem(REFRESH_TOKEN_KEY, token); } catch (error) { console.error('保存刷新token失败:', error); } } // 清除 token async clearToken(): Promise { try { await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]); } catch (error) { console.error('清除token失败:', error); } } // 统一请求方法 private async request( method: string, path: string, params?: Record, body?: any ): Promise> { const url = new URL(`${this.baseUrl}${path}`); // 添加查询参数 if (params) { Object.keys(params).forEach(key => { if (params[key] !== undefined && params[key] !== null) { url.searchParams.append(key, String(params[key])); } }); } // 构建请求头 const headers: HeadersInit = { 'Content-Type': 'application/json', }; // 添加认证 token const token = await this.getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } // 构建请求配置 const config: RequestInit = { method, headers, }; // 添加请求体 if (body && method !== 'GET') { config.body = JSON.stringify(body); } try { const response = await fetch(url.toString(), config); // 处理 401 未授权 if (response.status === 401) { // 尝试刷新 token const refreshed = await this.refreshToken(); if (!refreshed) { // 刷新失败,清除 token 并跳转登录 await this.clearToken(); if (this.navigation) { this.navigation.dispatch( CommonActions.reset({ index: 0, routes: [{ name: 'Login' }], }) ); } throw new ApiError(401, '登录已过期,请重新登录'); } // 刷新成功后重试请求 return this.request(method, path, params, body); } // 解析响应(兼容非 JSON 返回,避免 SyntaxError 被误判为网络错误) const contentType = response.headers.get('content-type') || ''; let parsedBody: any = null; let rawText = ''; if (contentType.includes('application/json')) { parsedBody = await response.json(); } else { rawText = await response.text(); if (rawText) { try { parsedBody = JSON.parse(rawText); } catch { parsedBody = null; } } } // 优先处理标准 API 结构 if (parsedBody && typeof parsedBody === 'object' && 'code' in parsedBody) { const data = parsedBody as ApiResponse; if (data.code !== 0) { throw new ApiError(data.code, data.message || '请求失败'); } return data; } // 非标准结构:先按 HTTP 状态处理失败 if (!response.ok) { const fallbackMessage = rawText || response.statusText || `请求失败(${response.status})`; throw new ApiError(response.status, fallbackMessage); } // 非标准结构但 HTTP 成功:兜底为成功响应(兼容部分纯文本成功接口) return { code: 0, message: 'success', data: (parsedBody as T) ?? (undefined as T), }; } catch (error) { // 如果是 ApiError,直接抛出 if (error instanceof ApiError) { throw error; } // 网络错误 console.error('API请求失败:', error); throw new ApiError(500, '网络请求失败,请检查网络连接'); } } // 刷新 token private async refreshToken(): Promise { try { const refreshToken = await this.getRefreshToken(); if (!refreshToken) { return false; } const response = await fetch(`${this.baseUrl}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${refreshToken}`, }, }); if (!response.ok) { return false; } const data = await response.json(); if (data.code === 0 && data.data?.token) { await this.setToken(data.data.token); // 后端返回refresh_token (snake_case),兼容两种命名 if (data.data.refresh_token || data.data.refreshToken) { await this.setRefreshToken(data.data.refresh_token || data.data.refreshToken); } return true; } return false; } catch (error) { console.error('刷新token失败:', error); return false; } } // GET 请求 async get(path: string, params?: Record): Promise> { return this.request('GET', path, params); } // POST 请求 async post(path: string, body?: any): Promise> { return this.request('POST', path, undefined, body); } // PUT 请求 async put(path: string, body?: any): Promise> { return this.request('PUT', path, undefined, body); } // DELETE 请求 async delete(path: string, body?: any): Promise> { return this.request('DELETE', path, undefined, body); } // 上传文件 async upload( path: string, file: { uri: string; name: string; type: string }, additionalData?: Record ): Promise> { const formData = new FormData(); // 添加文件(使用image字段名) formData.append('image', { uri: file.uri, name: file.name, type: file.type, } as any); // 添加额外数据 if (additionalData) { Object.keys(additionalData).forEach(key => { formData.append(key, additionalData[key]); }); } const token = await this.getToken(); const headers: HeadersInit = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers, body: formData, }); const data: ApiResponse = await response.json(); if (data.code !== 0) { throw new ApiError(data.code, data.message); } return data; } } // 导出 API 客户端实例 export const api = new ApiClient(BASE_URL); export { SSE_URL, TOKEN_KEY, REFRESH_TOKEN_KEY };