# DramaLing 詞卡管理 API 規格書 ## 1. API 概覽 ### 1.1 基本資訊 - **基礎 URL**: `http://localhost:5008/api` (開發環境) - **控制器**: `FlashcardsController` - **路由前綴**: `/api/flashcards` - **認證方式**: JWT Bearer Token (開發階段暫時關閉) - **資料格式**: JSON (UTF-8) ### 1.2 架構依賴 > 📋 **技術架構參考文檔** > > 本 API 規格書依賴以下文檔,建議閱讀順序: > > **🏗️ 系統架構文檔** > - [系統架構總覽](../../04_technical/system-architecture.md) - 了解整體架構設計 > - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解 ASP.NET Core 架構細節 > > **📋 需求規格文檔** > - [詞卡管理功能產品需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 了解功能需求和用戶故事 ## 2. 資料模型定義 ### 2.1 詞卡實體 (Flashcard Entity) #### C# 實體模型 ```csharp public class Flashcard { // 基本識別 public Guid Id { get; set; } public Guid UserId { get; set; } public Guid? CardSetId { get; set; } // 詞卡內容 [Required, MaxLength(255)] public string Word { get; set; } [Required] public string Translation { get; set; } [Required] public string Definition { get; set; } [MaxLength(50)] public string? PartOfSpeech { get; set; } [MaxLength(255)] public string? Pronunciation { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } // SM-2 學習算法參數 public float EasinessFactor { get; set; } = 2.5f; public int Repetitions { get; set; } = 0; public int IntervalDays { get; set; } = 1; public DateTime NextReviewDate { get; set; } // 學習統計 [Range(0, 100)] public int MasteryLevel { get; set; } = 0; public int TimesReviewed { get; set; } = 0; public int TimesCorrect { get; set; } = 0; public DateTime? LastReviewedAt { get; set; } // 狀態管理 public bool IsFavorite { get; set; } = false; public bool IsArchived { get; set; } = false; [MaxLength(10)] public string? DifficultyLevel { get; set; } // A1-C2 // 時間戳記 public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } ``` #### TypeScript 前端型別定義 ```typescript interface Flashcard { id: string; word: string; translation: string; definition: string; partOfSpeech: string; pronunciation: string; example: string; exampleTranslation?: string; masteryLevel: number; // 0-100 timesReviewed: number; isFavorite: boolean; nextReviewDate: string; // ISO Date difficultyLevel: string; // A1, A2, B1, B2, C1, C2 createdAt: string; // ISO Date updatedAt?: string; // ISO Date } interface CreateFlashcardRequest { word: string; translation: string; definition: string; pronunciation: string; partOfSpeech: string; example: string; exampleTranslation?: string; } ``` ### 2.2 API 回應格式標準 #### 成功回應格式 ```json { "success": true, "data": { // 實際資料內容 }, "message": "操作成功描述" // 可選 } ``` #### 錯誤回應格式 ```json { "success": false, "error": "錯誤描述", "isDuplicate": true, // 特殊情況:重複資料 "existingCard": { /* 現有詞卡資料 */ } // 重複時的現有資料 } ``` ## 3. API 端點規格 ### 3.1 端點清單 | 方法 | 端點 | 描述 | 狀態 | |------|------|------|------| | GET | `/api/flashcards` | 取得詞卡列表 | ✅ 已實現 | | GET | `/api/flashcards/{id}` | 取得單一詞卡 | ✅ 已實現 | | POST | `/api/flashcards` | 創建新詞卡 | ✅ 已實現 | | PUT | `/api/flashcards/{id}` | 更新詞卡 | ✅ 已實現 | | DELETE | `/api/flashcards/{id}` | 刪除詞卡 | ✅ 已實現 | | POST | `/api/flashcards/{id}/favorite` | 切換收藏狀態 | ✅ 已實現 | ### 3.2 詳細 API 規格 #### 📖 GET /api/flashcards **功能**: 取得用戶的詞卡列表,支援搜尋和篩選 **查詢參數**: ```typescript interface GetFlashcardsParams { search?: string; // 搜尋關鍵字,搜尋範圍:詞彙、翻譯、定義 favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false) } ``` **實際實現邏輯**: ```csharp // 搜尋篩選邏輯 if (!string.IsNullOrEmpty(search)) { query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search) || (f.Definition != null && f.Definition.Contains(search))); } // 收藏篩選 if (favoritesOnly) { query = query.Where(f => f.IsFavorite); } // 排序:按創建時間降序 var flashcards = await query.OrderByDescending(f => f.CreatedAt).ToListAsync(); ``` **成功回應**: ```json { "success": true, "data": { "flashcards": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "word": "sophisticated", "translation": "精密的", "definition": "Highly developed or complex", "partOfSpeech": "adjective", "pronunciation": "/səˈfɪstɪkeɪtɪd/", "example": "A sophisticated system", "exampleTranslation": "一個精密的系統", "masteryLevel": 75, "timesReviewed": 12, "isFavorite": true, "nextReviewDate": "2025-09-25T00:00:00Z", "difficultyLevel": "C1", "createdAt": "2025-09-20T08:30:00Z", "updatedAt": "2025-09-24T10:15:00Z" } ], "count": 1 } } ``` **請求範例**: ```bash # 取得所有詞卡 curl "http://localhost:5008/api/flashcards" # 搜尋包含 "sophisticated" 的詞卡 curl "http://localhost:5008/api/flashcards?search=sophisticated" # 僅取得收藏詞卡 curl "http://localhost:5008/api/flashcards?favoritesOnly=true" # 組合搜尋:搜尋收藏詞卡中包含 "精密" 的詞卡 curl "http://localhost:5008/api/flashcards?search=精密&favoritesOnly=true" ``` #### 📖 GET /api/flashcards/{id} **功能**: 取得單一詞卡的完整資訊 **路徑參數**: - `id`: 詞卡唯一識別碼 (GUID 格式) **成功回應**: ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "word": "sophisticated", // ... 完整詞卡資料,格式同列表 API "createdAt": "2025-09-20T08:30:00Z", "updatedAt": "2025-09-24T10:15:00Z" } } ``` **錯誤回應**: ```json { "success": false, "error": "詞卡不存在" } ``` #### ✏️ POST /api/flashcards **功能**: 創建新的詞卡 **請求體**: ```json { "word": "elaborate", "translation": "詳細說明", "definition": "To explain in detail", "pronunciation": "/ɪˈlæbərət/", "partOfSpeech": "verb", "example": "Please elaborate on your idea", "exampleTranslation": "請詳細說明你的想法" } ``` **實際實現邏輯**: ```csharp // 1. 自動創建測試用戶 (開發階段) var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); if (testUser == null) { // 自動創建測試用戶邏輯 } // 2. 重複詞卡檢測 var existing = await _context.Flashcards .FirstOrDefaultAsync(f => f.UserId == userId && f.Word.ToLower() == request.Word.ToLower() && !f.IsArchived); // 3. 創建新詞卡 var flashcard = new Flashcard { Id = Guid.NewGuid(), UserId = userId, Word = request.Word, Translation = request.Translation, // ... 其他欄位 CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; ``` **成功回應**: ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "word": "elaborate", // ... 完整創建的詞卡資料 "createdAt": "2025-09-24T10:30:00Z" }, "message": "詞卡創建成功" } ``` **重複詞卡回應**: ```json { "success": false, "error": "詞卡已存在", "isDuplicate": true, "existingCard": { "id": "existing-id", "word": "elaborate", // ... 現有詞卡資料 } } ``` #### ✏️ PUT /api/flashcards/{id} **功能**: 更新現有詞卡 **路徑參數**: - `id`: 詞卡唯一識別碼 (GUID) **請求體**: 與 POST 相同格式 **實際實現邏輯**: ```csharp // 1. 查找現有詞卡 var flashcard = await _context.Flashcards .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); if (flashcard == null) { return NotFound(new { Success = false, Error = "詞卡不存在" }); } // 2. 更新欄位 flashcard.Word = request.Word; flashcard.Translation = request.Translation; // ... 更新其他欄位 flashcard.UpdatedAt = DateTime.UtcNow; // 3. 保存變更 await _context.SaveChangesAsync(); ``` **成功回應**: ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", // ... 更新後的完整詞卡資料 "updatedAt": "2025-09-24T10:35:00Z" }, "message": "詞卡更新成功" } ``` #### 🗑️ DELETE /api/flashcards/{id} **功能**: 刪除詞卡 (軟刪除機制) **路徑參數**: - `id`: 詞卡唯一識別碼 (GUID) **實際實現邏輯**: ```csharp // 軟刪除:設定 IsArchived = true var flashcard = await _context.Flashcards .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); if (flashcard == null) { return NotFound(new { Success = false, Error = "詞卡不存在" }); } flashcard.IsArchived = true; flashcard.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); ``` **成功回應**: ```json { "success": true, "message": "詞卡已刪除" } ``` #### ⭐ POST /api/flashcards/{id}/favorite **功能**: 切換詞卡的收藏狀態 **路徑參數**: - `id`: 詞卡唯一識別碼 (GUID) **實際實現邏輯**: ```csharp var flashcard = await _context.Flashcards .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived); if (flashcard == null) { return NotFound(new { Success = false, Error = "詞卡不存在" }); } // 切換收藏狀態 flashcard.IsFavorite = !flashcard.IsFavorite; flashcard.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); ``` **成功回應**: ```json { "success": true, "data": { "isFavorite": true }, "message": "已加入收藏" } ``` ## 4. 前端整合規格 ### 4.1 FlashcardsService 類別 #### TypeScript 服務實現 ```typescript class FlashcardsService { private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`; // 統一請求處理 private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { const response = await fetch(`${this.baseURL}${endpoint}`, { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Network error' })); throw new Error(errorData.error || `HTTP ${response.status}`); } return response.json(); } // API 方法實現 async getFlashcards(search?: string, favoritesOnly: boolean = false): Promise> { const params = new URLSearchParams(); if (search) params.append('search', search); if (favoritesOnly) params.append('favoritesOnly', 'true'); const queryString = params.toString(); const endpoint = `/flashcards${queryString ? `?${queryString}` : ''}`; return await this.makeRequest>(endpoint); } async createFlashcard(data: CreateFlashcardRequest): Promise> { return await this.makeRequest>('/flashcards', { method: 'POST', body: JSON.stringify(data), }); } async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise> { return await this.makeRequest>(`/flashcards/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteFlashcard(id: string): Promise> { return await this.makeRequest>(`/flashcards/${id}`, { method: 'DELETE', }); } async toggleFavorite(id: string): Promise> { return await this.makeRequest>(`/flashcards/${id}/favorite`, { method: 'POST', }); } } export const flashcardsService = new FlashcardsService(); ``` ### 4.2 前端使用範例 #### 詞卡列表載入 ```typescript const loadFlashcards = async () => { try { setLoading(true); const result = await flashcardsService.getFlashcards(); if (result.success && result.data) { setFlashcards(result.data.flashcards); } else { setError(result.error || 'Failed to load flashcards'); } } catch (err) { setError('Failed to load flashcards'); } finally { setLoading(false); } }; ``` #### 詞卡保存 (含重複檢測) ```typescript const handleSaveWord = async (word: string, analysis: any) => { try { const cardData = { word: word, translation: analysis.translation || '', definition: analysis.definition || '', pronunciation: analysis.pronunciation || `/${word}/`, partOfSpeech: analysis.partOfSpeech || 'unknown', example: `Example sentence with ${word}.` }; const response = await flashcardsService.createFlashcard(cardData); if (response.success) { alert(`✅ 已成功將「${word}」保存到詞卡庫!`); return { success: true }; } else if (response.error && response.error.includes('已存在')) { alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`); return { success: false, error: 'duplicate' }; } else { throw new Error(response.error || '保存失敗'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '保存失敗'; alert(`❌ 保存詞卡失敗: ${errorMessage}`); return { success: false, error: errorMessage }; } }; ``` ## 5. 搜尋與篩選功能 ### 5.1 後端搜尋實現 #### 支援的搜尋欄位 ```csharp // 目前實現的搜尋範圍 query = query.Where(f => f.Word.Contains(search) || // 詞彙本身 f.Translation.Contains(search) || // 中文翻譯 (f.Definition != null && f.Definition.Contains(search)) // 英文定義 ); // 未來可擴展的搜尋範圍 // f.Example.Contains(search) || // 例句內容 // f.ExampleTranslation.Contains(search) // 例句翻譯 ``` #### 搜尋邏輯特性 - **大小寫敏感**: 目前使用 `Contains()` 進行大小寫敏感搜尋 - **部分匹配**: 支援關鍵字部分匹配 - **邏輯運算**: OR 邏輯 (任一欄位包含關鍵字即匹配) ### 5.2 前端搜尋與篩選 #### 即時搜尋實現 ```typescript // 前端即時篩選邏輯 const filteredCards = allCards.filter(card => { // 基本文字搜尋 if (searchTerm) { const searchLower = searchTerm.toLowerCase(); const matchesText = card.word?.toLowerCase().includes(searchLower) || card.translation?.toLowerCase().includes(searchLower) || card.definition?.toLowerCase().includes(searchLower); if (!matchesText) return false; } // CEFR 等級篩選 if (searchFilters.cefrLevel && card.difficultyLevel !== searchFilters.cefrLevel) { return false; } // 詞性篩選 if (searchFilters.partOfSpeech && card.partOfSpeech !== searchFilters.partOfSpeech) { return false; } // 掌握度篩選 if (searchFilters.masteryLevel) { const mastery = card.masteryLevel || 0; if (searchFilters.masteryLevel === 'high' && mastery < 80) return false; if (searchFilters.masteryLevel === 'medium' && (mastery < 60 || mastery >= 80)) return false; if (searchFilters.masteryLevel === 'low' && mastery >= 60) return false; } // 收藏篩選 if (searchFilters.onlyFavorites && !card.isFavorite) { return false; } return true; }); ``` #### 進階篩選選項 ```typescript interface SearchFilters { cefrLevel: string; // A1, A2, B1, B2, C1, C2 partOfSpeech: string; // noun, verb, adjective, adverb, preposition, interjection masteryLevel: string; // high (80%+), medium (60-79%), low (<60%) onlyFavorites: boolean; // 僅收藏詞卡 } ``` ## 6. 錯誤處理機制 ### 6.1 後端錯誤處理 #### 統一錯誤處理模式 ```csharp try { // API 邏輯 return Ok(new { Success = true, Data = result }); } catch (DbUpdateException ex) { _logger.LogError(ex, "Database error during flashcard operation"); return StatusCode(500, new { Success = false, Error = "資料庫操作失敗" }); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Invalid argument for flashcard operation"); return BadRequest(new { Success = false, Error = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during flashcard operation"); return StatusCode(500, new { Success = false, Error = "內部伺服器錯誤" }); } ``` #### 特殊情況處理 ```csharp // 重複詞卡檢測 if (existing != null) { return Ok(new { Success = false, Error = "詞卡已存在", IsDuplicate = true, ExistingCard = new { /* 現有詞卡資料 */ } }); } // 詞卡不存在 if (flashcard == null) { return NotFound(new { Success = false, Error = "詞卡不存在" }); } ``` ### 6.2 前端錯誤處理 #### API 服務層錯誤處理 ```typescript private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { try { const response = await fetch(`${this.baseURL}${endpoint}`, options); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Network error' })); throw new Error(errorData.error || errorData.details || `HTTP ${response.status}`); } return response.json(); } catch (error) { console.error('API request failed:', error); throw error; } } ``` #### 用戶反饋機制 ```typescript // 成功操作反饋 if (response.success) { alert(`✅ 已成功將「${word}」保存到詞卡庫!`); return { success: true }; } // 重複詞卡反饋 else if (response.error && response.error.includes('已存在')) { alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`); return { success: false, error: 'duplicate' }; } // 一般錯誤反饋 else { alert(`❌ 保存詞卡失敗: ${response.error}`); return { success: false, error: response.error }; } ``` ## 7. 認證與授權 ### 7.1 開發階段認證 #### 目前實現 (測試模式) ```csharp [AllowAnonymous] // 暫時移除認證要求 public class FlashcardsController : ControllerBase { private Guid GetUserId() { // 使用固定測試用戶 ID return Guid.Parse("00000000-0000-0000-0000-000000000001"); } } ``` #### 自動測試用戶創建 ```csharp // 確保測試用戶存在 var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); if (testUser == null) { testUser = new User { Id = userId, Username = "testuser", Email = "test@example.com", DisplayName = "測試用戶", SubscriptionType = "free", EnglishLevel = "A2", CreatedAt = DateTime.UtcNow }; _context.Users.Add(testUser); await _context.SaveChangesAsync(); } ``` ### 7.2 生產環境認證 (未來啟用) #### JWT Token 解析 ```csharp [Authorize] // 生產環境啟用 public class FlashcardsController : ControllerBase { private Guid GetUserId() { var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value; if (Guid.TryParse(userIdString, out var userId)) return userId; throw new UnauthorizedAccessException("Invalid user ID in token"); } } ``` ## 8. 效能優化 ### 8.1 資料庫查詢優化 #### 索引建議 ```sql -- 用戶詞卡查詢索引 CREATE INDEX IX_Flashcards_UserId_IsArchived ON Flashcards(UserId, IsArchived); -- 搜尋優化索引 CREATE INDEX IX_Flashcards_Word ON Flashcards(Word); CREATE INDEX IX_Flashcards_Translation ON Flashcards(Translation); -- 收藏篩選索引 CREATE INDEX IX_Flashcards_IsFavorite ON Flashcards(IsFavorite); -- 複合查詢索引 CREATE INDEX IX_Flashcards_UserId_IsFavorite_IsArchived ON Flashcards(UserId, IsFavorite, IsArchived); ``` #### 查詢優化技巧 ```csharp // 使用 AsNoTracking 提升查詢效能 (只讀查詢) var flashcards = await query .AsNoTracking() .OrderByDescending(f => f.CreatedAt) .ToListAsync(); // 選擇性載入欄位 (避免載入不必要的關聯資料) .Select(f => new { f.Id, f.Word, f.Translation, f.Definition, // 僅選擇需要的欄位 }) ``` ### 8.2 快取策略 (未來實現) #### 記憶體快取 ```csharp // 用戶詞卡列表快取 (30分鐘) var cacheKey = $"flashcards:user:{userId}"; var cachedCards = await _cacheService.GetAsync>(cacheKey); if (cachedCards == null) { cachedCards = await LoadFlashcardsFromDatabase(userId); await _cacheService.SetAsync(cacheKey, cachedCards, TimeSpan.FromMinutes(30)); } ``` #### 搜尋結果快取 ```csharp // 搜尋結果快取 (10分鐘) var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}"; var cachedResults = await _cacheService.GetAsync(searchCacheKey); ``` ## 9. 測試規格 ### 9.1 API 測試用例 #### 功能測試 ```bash # 測試詞卡創建 curl -X POST http://localhost:5008/api/flashcards \ -H "Content-Type: application/json" \ -d '{ "word": "test", "translation": "測試", "definition": "A trial or examination", "pronunciation": "/test/", "partOfSpeech": "noun", "example": "This is a test sentence" }' # 測試搜尋功能 curl "http://localhost:5008/api/flashcards?search=test" # 測試收藏功能 curl -X POST http://localhost:5008/api/flashcards/{id}/favorite # 測試詞卡更新 curl -X PUT http://localhost:5008/api/flashcards/{id} \ -H "Content-Type: application/json" \ -d '{ /* 更新的詞卡資料 */ }' # 測試詞卡刪除 curl -X DELETE http://localhost:5008/api/flashcards/{id} ``` #### 邊界條件測試 ```bash # 測試重複詞卡創建 curl -X POST http://localhost:5008/api/flashcards \ -d '{"word": "existing-word", ...}' # 預期回應: success: false, isDuplicate: true # 測試不存在的詞卡操作 curl http://localhost:5008/api/flashcards/non-existent-id # 預期回應: 404 Not Found # 測試空搜尋 curl "http://localhost:5008/api/flashcards?search=" # 預期回應: 返回所有詞卡 ``` ### 9.2 效能測試 #### 載入測試 ```bash # 測試大量詞卡載入 (1000+ 詞卡) time curl "http://localhost:5008/api/flashcards" # 預期: < 2秒 # 測試搜尋效能 time curl "http://localhost:5008/api/flashcards?search=sophisticated" # 預期: < 300ms ``` ## 10. 部署與監控 ### 10.1 健康檢查 #### API 健康檢查端點 ```csharp // Program.cs 中配置 services.AddHealthChecks() .AddDbContextCheck(); app.MapHealthChecks("/health"); ``` **健康檢查請求**: ```bash curl http://localhost:5008/health ``` ### 10.2 日誌監控 #### 結構化日誌 ```csharp _logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}", userId, request.Word); _logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId); _logger.LogWarning("Duplicate flashcard creation attempt: {Word} for user {UserId}", request.Word, userId); ``` #### 關鍵指標監控 - **API 響應時間**: 平均 < 200ms - **成功率**: > 99.5% - **重複詞卡檢測**: 準確率 100% - **資料庫連接**: 健康狀態監控 --- **文檔版本**: v1.0 **建立日期**: 2025-09-24 **基於**: FlashcardsController.cs v1.0 **維護負責**: API 開發團隊 **更新頻率**: 控制器變更時同步更新 > 📋 **相關參考文檔** > > **📋 需求與規格** > - [詞卡管理功能需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 查看完整功能需求和用戶故事 > > **🏗️ 技術架構** > - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解後端技術實現細節 > - [前端架構詳細說明](../../04_technical/frontend-architecture.md) - 了解前端整合方式