# 🚀 後端API完整策略設計 ## 📊 現狀分析 ### ✅ 現有功能 - 基本詞卡CRUD操作 - 用戶收藏功能 - 基本資料結構完整 ### ❌ 缺失功能 - **分頁功能** - 不支援page/limit參數 - **篩選功能** - difficultyLevel、partOfSpeech等參數無效 - **排序功能** - 不支援sortBy/sortOrder參數 - **搜尋功能** - 基本搜尋可能不完整 - **效能索引** - 缺少資料庫索引優化 --- ## 🎯 完整API設計 ### 核心端點:GET /api/flashcards #### 請求參數 (Query Parameters) ```typescript interface FlashcardQueryParams { // 搜尋和篩選 search?: string; // 全文搜尋 (詞彙、翻譯、定義) difficultyLevel?: string; // CEFR等級 (A1, A2, B1, B2, C1, C2) partOfSpeech?: string; // 詞性 (noun, verb, adjective, etc.) masteryLevel?: string; // 掌握程度 (low: <60%, medium: 60-79%, high: 80%+) favoritesOnly?: boolean; // 僅收藏詞卡 // 時間範圍篩選 createdAfter?: string; // 創建時間起始 (ISO 8601) createdBefore?: string; // 創建時間結束 (ISO 8601) reviewedAfter?: string; // 最後複習時間起始 reviewedBefore?: string; // 最後複習時間結束 // 複習統計篩選 reviewCountMin?: number; // 最少複習次數 reviewCountMax?: number; // 最多複習次數 // 排序 sortBy?: string; // 排序字段 (createdAt, word, masteryLevel, difficultyLevel, timesReviewed) sortOrder?: 'asc' | 'desc'; // 排序方向 // 分頁 page?: number; // 頁碼 (從1開始) limit?: number; // 每頁數量 (預設20,最大100) // 其他 includeMeta?: boolean; // 是否包含元數據 (預設true) } ``` #### 標準回應格式 ```typescript interface FlashcardQueryResponse { success: boolean; data: { flashcards: Flashcard[]; pagination: { current_page: number; total_pages: number; total_count: number; page_size: number; has_next: boolean; has_prev: boolean; }; filters_applied: { search?: string; difficulty_level?: string; part_of_speech?: string; mastery_level?: string; favorites_only?: boolean; // ... 其他應用的篩選 }; meta?: { query_time_ms: number; cache_hit: boolean; }; }; error?: string; } ``` --- ## 🏗️ 後端實作策略 ### 階段一:基礎功能修復 (必需) #### 1.1 分頁功能實作 ```python def get_flashcards(): # 分頁參數 page = int(request.args.get('page', 1)) limit = min(int(request.args.get('limit', 20)), 100) # 最大100 offset = (page - 1) * limit # 構建基本查詢 query = Flashcard.query.filter_by(user_id=current_user.id) # 應用篩選 (後續實作) query = apply_filters(query, request.args) # 應用排序 (後續實作) query = apply_sorting(query, request.args) # 計算總數 (在分頁之前) total_count = query.count() # 執行分頁查詢 flashcards = query.offset(offset).limit(limit).all() # 計算分頁資訊 total_pages = math.ceil(total_count / limit) return jsonify({ 'success': True, 'data': { 'flashcards': [card.to_dict() for card in flashcards], 'pagination': { 'current_page': page, 'total_pages': total_pages, 'total_count': total_count, 'page_size': limit, 'has_next': page < total_pages, 'has_prev': page > 1 } } }) ``` #### 1.2 篩選功能實作 ```python def apply_filters(query, args): """應用所有篩選條件""" # 全文搜尋 search = args.get('search') if search: search_pattern = f"%{search}%" query = query.filter( db.or_( Flashcard.word.ilike(search_pattern), Flashcard.translation.ilike(search_pattern), Flashcard.definition.ilike(search_pattern), Flashcard.example.ilike(search_pattern) ) ) # CEFR等級篩選 difficulty_level = args.get('difficultyLevel') if difficulty_level: query = query.filter(Flashcard.difficulty_level == difficulty_level) # 詞性篩選 part_of_speech = args.get('partOfSpeech') if part_of_speech: query = query.filter(Flashcard.part_of_speech == part_of_speech) # 掌握程度篩選 mastery_level = args.get('masteryLevel') if mastery_level: if mastery_level == 'high': query = query.filter(Flashcard.mastery_level >= 80) elif mastery_level == 'medium': query = query.filter( Flashcard.mastery_level >= 60, Flashcard.mastery_level < 80 ) elif mastery_level == 'low': query = query.filter(Flashcard.mastery_level < 60) # 收藏篩選 favorites_only = args.get('favoritesOnly', 'false').lower() == 'true' if favorites_only: query = query.filter(Flashcard.is_favorite == True) # 時間範圍篩選 created_after = args.get('createdAfter') if created_after: query = query.filter(Flashcard.created_at >= created_after) created_before = args.get('createdBefore') if created_before: query = query.filter(Flashcard.created_at <= created_before) # 複習次數篩選 review_count_min = args.get('reviewCountMin') if review_count_min: query = query.filter(Flashcard.times_reviewed >= int(review_count_min)) review_count_max = args.get('reviewCountMax') if review_count_max: query = query.filter(Flashcard.times_reviewed <= int(review_count_max)) return query ``` #### 1.3 排序功能實作 ```python def apply_sorting(query, args): """應用排序邏輯""" sort_by = args.get('sortBy', 'createdAt') sort_order = args.get('sortOrder', 'desc') # 排序字段映射 sort_fields = { 'createdAt': Flashcard.created_at, 'word': Flashcard.word, 'masteryLevel': Flashcard.mastery_level, 'difficultyLevel': Flashcard.difficulty_level, 'timesReviewed': Flashcard.times_reviewed } if sort_by not in sort_fields: sort_by = 'createdAt' # 預設排序 sort_field = sort_fields[sort_by] if sort_order.lower() == 'desc': query = query.order_by(sort_field.desc()) else: query = query.order_by(sort_field.asc()) # CEFR等級特殊排序 if sort_by == 'difficultyLevel': # 需要自定義排序邏輯 A1 < A2 < B1 < B2 < C1 < C2 level_order = case( (Flashcard.difficulty_level == 'A1', 1), (Flashcard.difficulty_level == 'A2', 2), (Flashcard.difficulty_level == 'B1', 3), (Flashcard.difficulty_level == 'B2', 4), (Flashcard.difficulty_level == 'C1', 5), (Flashcard.difficulty_level == 'C2', 6), else_=7 ) if sort_order.lower() == 'desc': query = query.order_by(level_order.desc()) else: query = query.order_by(level_order.asc()) return query ``` ### 階段二:資料庫優化 #### 2.1 索引建立 ```sql -- 基本篩選索引 CREATE INDEX idx_flashcards_user_difficulty ON flashcards(user_id, difficulty_level); CREATE INDEX idx_flashcards_user_part_of_speech ON flashcards(user_id, part_of_speech); CREATE INDEX idx_flashcards_user_mastery ON flashcards(user_id, mastery_level); CREATE INDEX idx_flashcards_user_favorite ON flashcards(user_id, is_favorite); -- 時間範圍索引 CREATE INDEX idx_flashcards_user_created_at ON flashcards(user_id, created_at); CREATE INDEX idx_flashcards_user_times_reviewed ON flashcards(user_id, times_reviewed); -- 複合索引 (重要查詢組合) CREATE INDEX idx_flashcards_user_difficulty_mastery ON flashcards(user_id, difficulty_level, mastery_level); CREATE INDEX idx_flashcards_user_favorite_created ON flashcards(user_id, is_favorite, created_at); -- 全文搜尋索引 (PostgreSQL) CREATE INDEX idx_flashcards_fulltext ON flashcards USING gin(to_tsvector('english', word || ' ' || translation || ' ' || definition || ' ' || example)); ``` #### 2.2 查詢優化 ```python # 使用 SQLAlchemy 查詢優化 def get_optimized_flashcards(): # 使用子查詢優化計數 subquery = apply_filters( Flashcard.query.filter_by(user_id=current_user.id), request.args ).subquery() # 總數查詢 total_count = db.session.query(subquery).count() # 分頁查詢 query = db.session.query(Flashcard).select_from(subquery) query = apply_sorting(query, request.args) flashcards = query.offset(offset).limit(limit).all() return flashcards, total_count ``` ### 階段三:快取策略 #### 3.1 Redis快取 ```python import redis import json import hashlib redis_client = redis.Redis(host='localhost', port=6379, db=0) def get_cache_key(user_id, params): """生成快取鍵""" cache_data = { 'user_id': user_id, 'params': dict(sorted(params.items())) } cache_string = json.dumps(cache_data, sort_keys=True) return f"flashcards:{hashlib.md5(cache_string.encode()).hexdigest()}" def get_cached_flashcards(user_id, params): """從快取獲取結果""" cache_key = get_cache_key(user_id, params) cached_data = redis_client.get(cache_key) if cached_data: return json.loads(cached_data) return None def cache_flashcards(user_id, params, data, ttl=300): """快取結果 (5分鐘TTL)""" cache_key = get_cache_key(user_id, params) redis_client.setex(cache_key, ttl, json.dumps(data)) ``` #### 3.2 快取失效策略 ```python def invalidate_user_cache(user_id): """當用戶資料變更時清除快取""" pattern = f"flashcards:*user_id*{user_id}*" for key in redis_client.scan_iter(match=pattern): redis_client.delete(key) # 在 CRUD 操作後調用 @app.after_request def invalidate_cache_on_mutation(response): if request.method in ['POST', 'PUT', 'DELETE']: invalidate_user_cache(current_user.id) return response ``` --- ## 🧪 API測試策略 ### 測試案例設計 #### 基本功能測試 ```bash # 1. 分頁測試 curl "http://localhost:5008/api/flashcards?page=1&limit=2" curl "http://localhost:5008/api/flashcards?page=2&limit=2" # 2. 篩選測試 curl "http://localhost:5008/api/flashcards?difficultyLevel=A2" curl "http://localhost:5008/api/flashcards?partOfSpeech=noun" curl "http://localhost:5008/api/flashcards?masteryLevel=low" # 3. 排序測試 curl "http://localhost:5008/api/flashcards?sortBy=word&sortOrder=asc" curl "http://localhost:5008/api/flashcards?sortBy=masteryLevel&sortOrder=desc" # 4. 組合測試 curl "http://localhost:5008/api/flashcards?difficultyLevel=A2&sortBy=word&page=1&limit=5" ``` #### 效能測試 ```python # 負載測試 import time import requests def performance_test(): start_time = time.time() response = requests.get('http://localhost:5008/api/flashcards', params={ 'search': 'test', 'difficultyLevel': 'A2', 'sortBy': 'createdAt', 'page': 1, 'limit': 20 }) end_time = time.time() response_time = (end_time - start_time) * 1000 print(f"Response time: {response_time:.2f}ms") return response_time # 目標:<300ms ``` --- ## 📈 效能指標 ### 成功標準 - **回應時間**: < 300ms (95th percentile) - **分頁查詢**: < 200ms - **搜尋查詢**: < 500ms - **快取命中率**: > 60% - **資料庫連接**: < 100ms ### 監控指標 ```python # API監控中間件 @app.before_request def before_request(): g.start_time = time.time() @app.after_request def after_request(response): response_time = (time.time() - g.start_time) * 1000 # 記錄慢查詢 if response_time > 500: logger.warning(f"Slow query: {request.url} - {response_time:.2f}ms") # 添加效能標頭 response.headers['X-Response-Time'] = f"{response_time:.2f}ms" return response ``` --- ## 🔄 實施計劃 ### 第1週:基礎功能 - ✅ 實作分頁功能 - ✅ 實作基本篩選 - ✅ 實作排序功能 - ✅ API測試 ### 第2週:優化與擴展 - ✅ 建立資料庫索引 - ✅ 實作進階篩選 - ✅ 效能優化 - ✅ 錯誤處理 ### 第3週:快取與監控 - ✅ 實作Redis快取 - ✅ 效能監控 - ✅ 負載測試 - ✅ 文檔完善 這個完整的後端API策略確保: - 🎯 **功能完整** - 支援所有前端需求 - ⚡ **高效能** - 資料庫和快取優化 - 🔒 **穩定性** - 完整的錯誤處理 - 📊 **可監控** - 效能指標追蹤 - 🧪 **可測試** - 完整的測試覆蓋 --- *文檔版本: 1.0* *最後更新: 2025-09-24*