/** * 統一的API客戶端 * 提供一致的請求處理、錯誤處理和認證邏輯 */ import { createApiErrorFromResponse, createApiErrorFromException, createSuccessResponse, createErrorResponse, StandardApiResponse, type ApiError } from './errorHandler' export interface RequestConfig extends RequestInit { timeout?: number retries?: number context?: string } export class ApiClient { private baseURL: string private defaultTimeout: number = 10000 // 10秒 private defaultRetries: number = 3 constructor(baseURL: string) { this.baseURL = baseURL } /** * 統一的HTTP請求方法 */ async request( endpoint: string, config: RequestConfig = {} ): Promise> { const { timeout = this.defaultTimeout, retries = this.defaultRetries, context, ...fetchOptions } = config const url = `${this.baseURL}${endpoint}` let lastError: ApiError // 重試邏輯 for (let attempt = 0; attempt <= retries; attempt++) { try { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) const response = await fetch(url, { ...fetchOptions, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...this.getDefaultHeaders(), ...fetchOptions.headers, }, }) clearTimeout(timeoutId) if (!response.ok) { const apiError = await createApiErrorFromResponse(response, context) lastError = apiError // 處理認證錯誤 - 自動清除過期token if (response.status === 401) { localStorage.removeItem('auth_token') console.log('🔒 認證過期,已清除token') } // 如果不可重試,直接返回錯誤 if (attempt === retries || !this.isRetryableStatus(response.status)) { return createErrorResponse(apiError) } // 等待後重試 await this.delay(Math.pow(2, attempt) * 1000) // 指數退避 continue } // 成功情況 const data = await response.json() return createSuccessResponse(data) } catch (error) { const apiError = createApiErrorFromException(error, context) lastError = apiError // 超時或網路錯誤才重試 if (attempt === retries || !this.isRetryableError(apiError)) { return createErrorResponse(apiError) } await this.delay(Math.pow(2, attempt) * 1000) } } return createErrorResponse(lastError!) } /** * GET請求 */ async get(endpoint: string, config?: RequestConfig): Promise> { return this.request(endpoint, { ...config, method: 'GET' }) } /** * POST請求 */ async post(endpoint: string, data?: any, config?: RequestConfig): Promise> { return this.request(endpoint, { ...config, method: 'POST', body: data ? JSON.stringify(data) : undefined, }) } /** * PUT請求 */ async put(endpoint: string, data?: any, config?: RequestConfig): Promise> { return this.request(endpoint, { ...config, method: 'PUT', body: data ? JSON.stringify(data) : undefined, }) } /** * DELETE請求 */ async delete(endpoint: string, config?: RequestConfig): Promise> { return this.request(endpoint, { ...config, method: 'DELETE' }) } /** * 獲取預設Headers */ private getDefaultHeaders(): Record { const headers: Record = {} // 添加認證Token(如果存在) const token = localStorage.getItem('auth_token') if (token) { headers.Authorization = `Bearer ${token}` } return headers } /** * 判斷HTTP狀態碼是否可重試 */ private isRetryableStatus(status: number): boolean { return [ 408, // Request Timeout 429, // Too Many Requests 500, // Internal Server Error 502, // Bad Gateway 503, // Service Unavailable 504, // Gateway Timeout ].includes(status) } /** * 判斷錯誤是否可重試 */ private isRetryableError(error: ApiError): boolean { return [ 'NETWORK_ERROR', 'TIMEOUT_ERROR', 'SERVER_ERROR' ].includes(error.type) } /** * 延遲函數 */ private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } /** * 設置基礎URL */ setBaseURL(url: string): void { this.baseURL = url } /** * 設置預設超時時間 */ setTimeout(timeout: number): void { this.defaultTimeout = timeout } /** * 設置預設重試次數 */ setRetries(retries: number): void { this.defaultRetries = retries } } // 統一import import { BASE_URL, API_URLS } from '@/lib/config/api' // 建立預設的API客戶端實例 export const apiClient = new ApiClient(BASE_URL) // 建立特定服務的API客戶端 export const authApiClient = new ApiClient(API_URLS.auth()) export const flashcardsApiClient = new ApiClient(API_URLS.flashcards()) export const reviewApiClient = new ApiClient(API_URLS.review()) export const imageApiClient = new ApiClient(API_URLS.image())