dramaling-vocab-learning/note/done/BACKEND_API_STRATEGY.md

461 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🚀 後端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*