diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs index 54b7098..8c97fed 100644 --- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs +++ b/backend/DramaLing.Api/Controllers/FlashcardsController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; +using DramaLing.Api.Models.DTOs; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; @@ -39,7 +40,10 @@ public class FlashcardsController : ControllerBase [HttpGet] public async Task GetFlashcards( [FromQuery] string? search = null, - [FromQuery] bool favoritesOnly = false) + [FromQuery] bool favoritesOnly = false, + [FromQuery] string? cefrLevel = null, + [FromQuery] string? partOfSpeech = null, + [FromQuery] string? masteryLevel = null) { try { @@ -49,13 +53,15 @@ public class FlashcardsController : ControllerBase .Where(f => f.UserId == userId && !f.IsArchived) .AsQueryable(); - // 搜尋篩選 + // 搜尋篩選 (擴展支援例句內容) if (!string.IsNullOrEmpty(search)) { query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search) || - (f.Definition != null && f.Definition.Contains(search))); + (f.Definition != null && f.Definition.Contains(search)) || + (f.Example != null && f.Example.Contains(search)) || + (f.ExampleTranslation != null && f.ExampleTranslation.Contains(search))); } // 收藏篩選 @@ -64,7 +70,37 @@ public class FlashcardsController : ControllerBase query = query.Where(f => f.IsFavorite); } + // CEFR 等級篩選 + if (!string.IsNullOrEmpty(cefrLevel)) + { + query = query.Where(f => f.DifficultyLevel == cefrLevel); + } + + // 詞性篩選 + if (!string.IsNullOrEmpty(partOfSpeech)) + { + query = query.Where(f => f.PartOfSpeech == partOfSpeech); + } + + // 掌握度篩選 + if (!string.IsNullOrEmpty(masteryLevel)) + { + switch (masteryLevel.ToLower()) + { + case "high": + query = query.Where(f => f.MasteryLevel >= 80); + break; + case "medium": + query = query.Where(f => f.MasteryLevel >= 60 && f.MasteryLevel < 80); + break; + case "low": + query = query.Where(f => f.MasteryLevel < 60); + break; + } + } + var flashcards = await query + .AsNoTracking() // 效能優化:只讀查詢 .OrderByDescending(f => f.CreatedAt) .Select(f => new { diff --git a/backend/DramaLing.Api/Models/DTOs/FlashcardDto.cs b/backend/DramaLing.Api/Models/DTOs/FlashcardDto.cs new file mode 100644 index 0000000..16f9338 --- /dev/null +++ b/backend/DramaLing.Api/Models/DTOs/FlashcardDto.cs @@ -0,0 +1,70 @@ +using System.ComponentModel.DataAnnotations; + +namespace DramaLing.Api.Models.DTOs; + +public class CreateFlashcardRequest +{ + [Required(ErrorMessage = "詞彙為必填項目")] + [StringLength(255, ErrorMessage = "詞彙長度不得超過 255 字元")] + public string Word { get; set; } = string.Empty; + + [Required(ErrorMessage = "翻譯為必填項目")] + public string Translation { get; set; } = string.Empty; + + [Required(ErrorMessage = "定義為必填項目")] + public string Definition { get; set; } = string.Empty; + + [StringLength(255, ErrorMessage = "發音長度不得超過 255 字元")] + public string Pronunciation { get; set; } = string.Empty; + + [RegularExpression("^(noun|verb|adjective|adverb|preposition|interjection|phrase)$", + ErrorMessage = "詞性必須為有效值")] + public string PartOfSpeech { get; set; } = "noun"; + + [Required(ErrorMessage = "例句為必填項目")] + public string Example { get; set; } = string.Empty; + + public string? ExampleTranslation { get; set; } + + [RegularExpression("^(A1|A2|B1|B2|C1|C2)$", + ErrorMessage = "CEFR 等級必須為有效值")] + public string? DifficultyLevel { get; set; } = "A2"; +} + +public class UpdateFlashcardRequest : CreateFlashcardRequest +{ + // 繼承所有創建請求的欄位,用於更新操作 +} + +public class FlashcardResponse +{ + public Guid Id { get; set; } + public string Word { get; set; } = string.Empty; + public string Translation { get; set; } = string.Empty; + public string Definition { get; set; } = string.Empty; + public string? PartOfSpeech { get; set; } + public string? Pronunciation { get; set; } + public string? Example { get; set; } + public string? ExampleTranslation { get; set; } + public int MasteryLevel { get; set; } + public int TimesReviewed { get; set; } + public bool IsFavorite { get; set; } + public DateTime NextReviewDate { get; set; } + public string? DifficultyLevel { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public class BatchFavoriteRequest +{ + [Required] + public List FlashcardIds { get; set; } = new(); + + public bool IsFavorite { get; set; } +} + +public class BatchDeleteRequest +{ + [Required] + public List FlashcardIds { get; set; } = new(); +} \ No newline at end of file diff --git a/docs/03_development/backend-api-development-plan.md b/docs/03_development/backend-api-development-plan.md new file mode 100644 index 0000000..a341b6e --- /dev/null +++ b/docs/03_development/backend-api-development-plan.md @@ -0,0 +1,726 @@ +# DramaLing 後端 API 開發計劃 + +## 1. 概述 + +### 1.1 計劃目的 +本開發計劃旨在基於現有的詞卡管理 API 規格,完善和優化後端 API 實現,確保 API 功能完整性、效能和穩定性。 + +### 1.2 依賴文檔 + +> 📋 **參考文檔引用** +> +> 本開發計劃基於以下文檔制定: +> +> **🔧 API 規格文檔 (主要參考)** +> - [詞卡管理 API 規格](./api/flashcard-management-api.md) - API 介面定義和實現邏輯的完整規格 +> +> **🏗️ 技術架構文檔** +> - [後端架構詳細說明](../04_technical/backend-architecture.md) - 了解 ASP.NET Core 架構約束和設計模式 +> - [系統架構總覽](../04_technical/system-architecture.md) - 了解整體系統設計和技術棧 +> +> **📋 需求規格文檔** +> - [詞卡管理功能產品需求規格](../01_requirement/詞卡管理功能產品需求規格.md) - 了解業務需求和用戶故事 + +### 1.3 當前狀態評估 + +根據 [詞卡管理 API 規格](./api/flashcard-management-api.md) 分析,目前後端 API 狀態: + +#### ✅ **已完成的 API 端點** +- ✅ `GET /api/flashcards` - 取得詞卡列表 (含搜尋和收藏篩選) +- ✅ `GET /api/flashcards/{id}` - 取得單一詞卡 +- ✅ `POST /api/flashcards` - 創建新詞卡 (含重複檢測) +- ✅ `PUT /api/flashcards/{id}` - 更新詞卡 +- ✅ `DELETE /api/flashcards/{id}` - 刪除詞卡 (軟刪除) +- ✅ `POST /api/flashcards/{id}/favorite` - 切換收藏狀態 + +#### 🎯 **需要改進的項目** +- 🔄 搜尋功能擴展 (目前不支援例句搜尋) +- 🔄 進階篩選 API (CEFR 等級、詞性、掌握度) +- 🔄 批量操作 API (未來功能) +- 🔄 效能優化 (查詢索引、快取機制) +- 🔄 API 文檔生成 (Swagger 增強) + +## 2. 開發任務清單 + +### 2.1 搜尋功能增強 (優先級:🔴 高) + +#### 任務 1: 擴展搜尋範圍支援例句 +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 修改 GetFlashcards 方法 + +**當前實現**: +```csharp +// 目前搜尋邏輯 (第 53-59 行) +if (!string.IsNullOrEmpty(search)) +{ + query = query.Where(f => + f.Word.Contains(search) || + f.Translation.Contains(search) || + (f.Definition != null && f.Definition.Contains(search))); +} +``` + +**改進實現**: +```csharp +// 擴展搜尋範圍,新增例句搜尋 +if (!string.IsNullOrEmpty(search)) +{ + query = query.Where(f => + f.Word.Contains(search) || + f.Translation.Contains(search) || + (f.Definition != null && f.Definition.Contains(search)) || + (f.Example != null && f.Example.Contains(search)) || + (f.ExampleTranslation != null && f.ExampleTranslation.Contains(search))); +} +``` + +**驗收標準**: +- [ ] 搜尋範圍包含例句 (Example) 和例句翻譯 (ExampleTranslation) +- [ ] 搜尋效能無明顯下降 +- [ ] 搜尋結果準確性維持 100% + +#### 任務 2: 新增進階篩選查詢參數 +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 修改 GetFlashcards 方法參數 + +**新增查詢參數**: +```csharp +[HttpGet] +public async Task GetFlashcards( + [FromQuery] string? search = null, + [FromQuery] bool favoritesOnly = false, + [FromQuery] string? cefrLevel = null, // 新增: A1, A2, B1, B2, C1, C2 + [FromQuery] string? partOfSpeech = null, // 新增: noun, verb, adjective, etc. + [FromQuery] string? masteryLevel = null // 新增: high, medium, low +) +``` + +**篩選邏輯實現**: +```csharp +// CEFR 等級篩選 +if (!string.IsNullOrEmpty(cefrLevel)) +{ + query = query.Where(f => f.DifficultyLevel == cefrLevel); +} + +// 詞性篩選 +if (!string.IsNullOrEmpty(partOfSpeech)) +{ + query = query.Where(f => f.PartOfSpeech == partOfSpeech); +} + +// 掌握度篩選 +if (!string.IsNullOrEmpty(masteryLevel)) +{ + switch (masteryLevel.ToLower()) + { + case "high": + query = query.Where(f => f.MasteryLevel >= 80); + break; + case "medium": + query = query.Where(f => f.MasteryLevel >= 60 && f.MasteryLevel < 80); + break; + case "low": + query = query.Where(f => f.MasteryLevel < 60); + break; + } +} +``` + +**驗收標準**: +- [ ] 支援 CEFR 等級篩選 (A1-C2) +- [ ] 支援詞性篩選 (noun, verb, adjective 等) +- [ ] 支援掌握度篩選 (high, medium, low) +- [ ] 多重篩選條件正確組合 (AND 邏輯) + +### 2.2 效能優化 (優先級:🟡 中) + +#### 任務 3: 資料庫查詢優化 +**影響檔案**: +- `backend/DramaLing.Api/Data/DramaLingDbContext.cs` - 新增索引配置 + +**索引優化**: +```csharp +// 在 OnModelCreating 方法中新增索引 +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // 現有配置... + + // 搜尋優化索引 + modelBuilder.Entity() + .HasIndex(f => f.Word) + .HasDatabaseName("IX_Flashcards_Word"); + + modelBuilder.Entity() + .HasIndex(f => f.Translation) + .HasDatabaseName("IX_Flashcards_Translation"); + + // 複合查詢索引 + modelBuilder.Entity() + .HasIndex(f => new { f.UserId, f.IsArchived, f.IsFavorite }) + .HasDatabaseName("IX_Flashcards_UserId_IsArchived_IsFavorite"); + + // CEFR 等級索引 + modelBuilder.Entity() + .HasIndex(f => f.DifficultyLevel) + .HasDatabaseName("IX_Flashcards_DifficultyLevel"); +} +``` + +**查詢邏輯優化**: +```csharp +// 使用 AsNoTracking 提升查詢效能 +var flashcards = await query + .AsNoTracking() + .OrderByDescending(f => f.CreatedAt) + .ToListAsync(); +``` + +**驗收標準**: +- [ ] 新增適當的資料庫索引 +- [ ] 查詢時間 < 200ms (1000+ 詞卡) +- [ ] 搜尋響應時間 < 100ms + +#### 任務 4: 快取機制實現 +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 整合快取服務 +- `backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs` - 已有快取配置 + +**快取策略實現**: +```csharp +// 在 FlashcardsController 中使用快取 +private readonly ICacheService _cacheService; + +[HttpGet] +public async Task GetFlashcards(...) +{ + var cacheKey = $"flashcards:user:{userId}:search:{search}:favorites:{favoritesOnly}"; + + var cachedResult = await _cacheService.GetAsync(cacheKey); + if (cachedResult != null) + { + return Ok(cachedResult); + } + + // 執行資料庫查詢... + var result = new { Success = true, Data = ... }; + + // 快取結果 (30分鐘) + await _cacheService.SetAsync(cacheKey, result, TimeSpan.FromMinutes(30)); + + return Ok(result); +} +``` + +**驗收標準**: +- [ ] 詞卡列表查詢快取 30 分鐘 +- [ ] 快取命中率 > 70% +- [ ] 快取失效機制正確 (CRUD 操作後清除) + +### 2.3 API 增強功能 (優先級:🟡 中) + +#### 任務 5: 新增批量操作 API +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 新增批量操作端點 + +**新增 API 端點**: +```csharp +[HttpPost("batch/favorite")] +public async Task BatchToggleFavorite([FromBody] BatchFavoriteRequest request) +{ + try + { + var userId = GetUserId(); + var flashcards = await _context.Flashcards + .Where(f => request.FlashcardIds.Contains(f.Id) && f.UserId == userId && !f.IsArchived) + .ToListAsync(); + + foreach (var flashcard in flashcards) + { + flashcard.IsFavorite = request.IsFavorite; + flashcard.UpdatedAt = DateTime.UtcNow; + } + + await _context.SaveChangesAsync(); + + return Ok(new { Success = true, UpdatedCount = flashcards.Count }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in batch favorite operation"); + return StatusCode(500, new { Success = false, Error = "批量操作失敗" }); + } +} + +[HttpDelete("batch")] +public async Task BatchDelete([FromBody] BatchDeleteRequest request) +{ + // 批量軟刪除實現 +} +``` + +**新增 DTO 類別**: +```csharp +public class BatchFavoriteRequest +{ + public List FlashcardIds { get; set; } = new(); + public bool IsFavorite { get; set; } +} + +public class BatchDeleteRequest +{ + public List FlashcardIds { get; set; } = new(); +} +``` + +**驗收標準**: +- [ ] 支援批量收藏/取消收藏 +- [ ] 支援批量刪除 (軟刪除) +- [ ] 批量操作事務性 (全部成功或全部失敗) +- [ ] 操作日誌記錄完整 + +#### 任務 6: 統計資料 API +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 新增統計端點 + +**新增統計 API**: +```csharp +[HttpGet("statistics")] +public async Task GetStatistics() +{ + try + { + var userId = GetUserId(); + + var stats = await _context.Flashcards + .Where(f => f.UserId == userId && !f.IsArchived) + .GroupBy(f => 1) // 單一群組用於統計 + .Select(g => new + { + TotalCount = g.Count(), + FavoriteCount = g.Count(f => f.IsFavorite), + MasteredCount = g.Count(f => f.MasteryLevel >= 80), + LearningCount = g.Count(f => f.MasteryLevel >= 60 && f.MasteryLevel < 80), + NewCount = g.Count(f => f.MasteryLevel < 60), + CefrDistribution = g.GroupBy(f => f.DifficultyLevel) + .ToDictionary(cg => cg.Key, cg => cg.Count()) + }) + .FirstOrDefaultAsync(); + + return Ok(new { Success = true, Data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting flashcard statistics"); + return StatusCode(500, new { Success = false, Error = "統計資料載入失敗" }); + } +} +``` + +**驗收標準**: +- [ ] 提供詞卡總數、收藏數、掌握度分布統計 +- [ ] 提供 CEFR 等級分布統計 +- [ ] 統計資料準確性 100% +- [ ] 響應時間 < 200ms + +### 2.4 錯誤處理增強 (優先級:🟡 中) + +#### 任務 7: 標準化錯誤回應格式 +**影響檔案**: +- `backend/DramaLing.Api/Models/DTOs/` - 新增錯誤回應 DTO +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 統一錯誤處理 + +**錯誤回應 DTO**: +```csharp +public class ApiErrorResponse +{ + public bool Success { get; set; } = false; + public string Error { get; set; } = string.Empty; + public string? Details { get; set; } + public string? ErrorCode { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} + +public class ApiSuccessResponse +{ + public bool Success { get; set; } = true; + public T? Data { get; set; } + public string? Message { get; set; } +} +``` + +**統一錯誤處理**: +```csharp +// 基礎控制器類別 +public abstract class BaseApiController : ControllerBase +{ + protected ActionResult ApiError(string message, string? details = null, string? errorCode = null) + { + return BadRequest(new ApiErrorResponse + { + Error = message, + Details = details, + ErrorCode = errorCode + }); + } + + protected ActionResult ApiSuccess(T data, string? message = null) + { + return Ok(new ApiSuccessResponse + { + Data = data, + Message = message + }); + } +} +``` + +**驗收標準**: +- [ ] 所有 API 端點使用統一錯誤格式 +- [ ] 錯誤代碼標準化 (如 FLASHCARD_NOT_FOUND) +- [ ] 錯誤訊息本地化 (中文) +- [ ] 詳細錯誤信息僅在開發環境顯示 + +### 2.5 認證與授權準備 (優先級:🟢 低) + +#### 任務 8: 準備生產環境認證 +**影響檔案**: +- `backend/DramaLing.Api/Controllers/FlashcardsController.cs` - 準備認證代碼 + +**實現內容**: +```csharp +// 保留現有測試模式,準備生產環境切換 +private Guid GetUserId() +{ + // 開發環境:使用固定測試用戶 + if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") + { + return Guid.Parse("00000000-0000-0000-0000-000000000001"); + } + + // 生產環境:解析 JWT Token + 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"); +} + +// 準備生產環境控制器標註 +// [Authorize] // 生產環境時啟用 +[AllowAnonymous] // 開發環境暫時保持 +public class FlashcardsController : BaseApiController +``` + +**驗收標準**: +- [ ] 開發環境認證邏輯保持不變 +- [ ] 生產環境認證代碼已準備 +- [ ] 環境切換機制正確 +- [ ] JWT Token 解析邏輯完整 + +## 3. 資料庫改進 + +### 3.1 Entity Framework 優化 + +#### 任務 9: 新增資料庫索引遷移 +**影響檔案**: +- `backend/DramaLing.Api/Data/DramaLingDbContext.cs` - 索引配置 +- 新增 EF 遷移檔案 + +**遷移步驟**: +```bash +# 生成新的遷移 +dotnet ef migrations add AddFlashcardSearchIndexes + +# 更新資料庫 +dotnet ef database update +``` + +**索引策略**: +- 單欄索引:Word, Translation, DifficultyLevel, PartOfSpeech +- 複合索引:(UserId, IsArchived, IsFavorite) +- 搜尋優化:全文搜尋索引 (如果 SQLite 支援) + +**驗收標準**: +- [ ] 索引正確創建 +- [ ] 查詢計劃顯示索引使用 +- [ ] 搜尋效能明顯提升 + +#### 任務 10: 資料驗證增強 +**影響檔案**: +- `backend/DramaLing.Api/Models/DTOs/CreateFlashcardRequest.cs` - 新增驗證特性 + +**驗證規則**: +```csharp +public class CreateFlashcardRequest +{ + [Required(ErrorMessage = "詞彙為必填項目")] + [StringLength(255, ErrorMessage = "詞彙長度不得超過 255 字元")] + public string Word { get; set; } = string.Empty; + + [Required(ErrorMessage = "翻譯為必填項目")] + public string Translation { get; set; } = string.Empty; + + [Required(ErrorMessage = "定義為必填項目")] + public string Definition { get; set; } = string.Empty; + + [StringLength(255, ErrorMessage = "發音長度不得超過 255 字元")] + public string Pronunciation { get; set; } = string.Empty; + + [RegularExpression("^(noun|verb|adjective|adverb|preposition|interjection)$", + ErrorMessage = "詞性必須為有效值")] + public string PartOfSpeech { get; set; } = "noun"; + + [Required(ErrorMessage = "例句為必填項目")] + public string Example { get; set; } = string.Empty; + + public string? ExampleTranslation { get; set; } + + [RegularExpression("^(A1|A2|B1|B2|C1|C2)$", + ErrorMessage = "CEFR 等級必須為有效值")] + public string? DifficultyLevel { get; set; } = "A2"; +} +``` + +**驗收標準**: +- [ ] 所有輸入資料驗證完整 +- [ ] 錯誤訊息本地化和友善 +- [ ] 驗證失敗時返回具體錯誤信息 +- [ ] 防止無效資料進入資料庫 + +## 4. API 文檔與測試 + +### 4.1 Swagger 文檔增強 + +#### 任務 11: 完善 API 文檔 +**影響檔案**: +- `backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs` - Swagger 配置 + +**Swagger 增強**: +```csharp +services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { + Title = "DramaLing API - 詞卡管理", + Version = "v1", + Description = "DramaLing 詞卡管理功能的完整 API 文檔" + }); + + // XML 註解檔案 + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + + // API 範例 + c.SchemaFilter(); +}); +``` + +**XML 註解增強**: +```csharp +/// +/// 取得用戶的詞卡列表,支援搜尋和篩選 +/// +/// 搜尋關鍵字,搜尋範圍:詞彙、翻譯、定義、例句 +/// 僅顯示收藏詞卡 +/// CEFR 難度等級篩選 (A1-C2) +/// 詞性篩選 +/// 掌握度篩選 (high/medium/low) +/// 詞卡列表和數量 +[HttpGet] +public async Task GetFlashcards(...) +``` + +**驗收標準**: +- [ ] Swagger UI 顯示完整 API 文檔 +- [ ] 所有參數和回應格式有詳細說明 +- [ ] 提供 API 使用範例 +- [ ] 錯誤代碼和狀態碼說明完整 + +### 4.2 API 測試套件 + +#### 任務 12: 整合測試實現 +**影響檔案**: +- 新增 `backend/DramaLing.Api.Tests/Controllers/FlashcardsControllerTests.cs` + +**測試涵蓋範圍**: +```csharp +[TestClass] +public class FlashcardsControllerTests +{ + [TestMethod] + public async Task GetFlashcards_WithSearch_ReturnsFilteredResults() + { + // 測試搜尋功能 + } + + [TestMethod] + public async Task CreateFlashcard_WithValidData_CreatesSuccessfully() + { + // 測試詞卡創建 + } + + [TestMethod] + public async Task CreateFlashcard_WithDuplicateWord_ReturnsDuplicateError() + { + // 測試重複詞卡檢測 + } + + [TestMethod] + public async Task GetFlashcards_WithCefrFilter_ReturnsCorrectLevel() + { + // 測試 CEFR 等級篩選 + } +} +``` + +**驗收標準**: +- [ ] 所有 API 端點有對應測試 +- [ ] 測試覆蓋率 > 80% +- [ ] 包含邊界條件和錯誤情況測試 +- [ ] 測試可在 CI/CD 中自動執行 + +## 5. 實施時程 + +### 5.1 開發階段規劃 + +#### 第一階段:核心功能增強 (預估:2-3小時) +1. **擴展搜尋功能** (45分鐘) +2. **新增進階篩選參數** (60分鐘) +3. **資料驗證增強** (45分鐘) +4. **測試和驗證** (30分鐘) + +#### 第二階段:效能優化 (預估:2-3小時) +5. **資料庫索引優化** (60分鐘) +6. **快取機制實現** (90分鐘) +7. **效能測試和調整** (30分鐘) + +#### 第三階段:API 增強 (預估:3-4小時) +8. **批量操作 API** (120分鐘) +9. **統計資料 API** (90分鐘) +10. **Swagger 文檔完善** (30分鐘) + +#### 第四階段:測試和部署準備 (預估:2-3小時) +11. **整合測試實現** (120分鐘) +12. **生產環境認證準備** (60分鐘) + +### 5.2 里程碑檢查點 + +#### 里程碑 1: 基礎功能完善 ✅ +- 搜尋和篩選功能完整 +- 資料驗證機制健全 +- 基本測試通過 + +#### 里程碑 2: 效能達標 ✅ +- 查詢響應時間 < 200ms +- 快取命中率 > 70% +- 無明顯效能瓶頸 + +#### 里程碑 3: API 完整性 ✅ +- 所有計劃 API 端點實現 +- Swagger 文檔完整 +- 錯誤處理標準化 + +#### 里程碑 4: 生產準備 ✅ +- 整合測試覆蓋率 > 80% +- 生產環境配置準備 +- 部署文檔更新 + +## 6. 品質保證 + +### 6.1 程式碼審查檢查清單 + +#### API 設計檢查 +- [ ] RESTful 設計原則遵循 +- [ ] HTTP 狀態碼正確使用 +- [ ] 回應格式標準化 +- [ ] 查詢參數命名一致 + +#### 安全性檢查 +- [ ] 輸入驗證完整 +- [ ] SQL 注入防護 +- [ ] 用戶資料隔離 +- [ ] 敏感資訊保護 + +#### 效能檢查 +- [ ] 查詢優化 +- [ ] 索引使用合理 +- [ ] 記憶體使用最佳化 +- [ ] 併發處理安全 + +### 6.2 測試策略 + +> 📋 **測試策略參考** +> - [測試策略文檔](../04_testing/test-strategy.md) - 了解完整的測試方法和標準 + +#### 單元測試 +- Controller 方法邏輯測試 +- 資料驗證規則測試 +- 錯誤處理機制測試 + +#### 整合測試 +- API 端點完整流程測試 +- 資料庫操作測試 +- 快取機制測試 + +#### 效能測試 +- 大量資料載入測試 +- 並發請求壓力測試 +- 記憶體洩漏檢測 + +## 7. 部署與監控 + +### 7.1 部署準備 + +#### 環境配置 +```bash +# 生產環境變數 +export ASPNETCORE_ENVIRONMENT=Production +export DRAMALING_DB_CONNECTION="Data Source=production.db" +export USE_INMEMORY_DB=false +``` + +#### 健康檢查 +```csharp +// 增強健康檢查 +services.AddHealthChecks() + .AddDbContextCheck() + .AddCheck("cache") + .AddCheck("api"); +``` + +### 7.2 監控指標 + +#### 關鍵效能指標 (KPI) +- API 響應時間平均值 < 200ms +- API 成功率 > 99.5% +- 資料庫連接健康度 > 99% +- 快取命中率 > 70% + +#### 業務指標 +- 每日 API 呼叫次數 +- 詞卡創建成功率 +- 搜尋查詢頻率 +- 使用者活躍度 + +--- + +**計劃版本**: v1.0 +**制定日期**: 2025-09-24 +**預估完成時間**: 9-13小時 (分 4 個階段) +**負責開發**: 後端開發團隊 +**審核負責**: 技術主管 + +> 📋 **開發前必讀文檔** +> +> **🔧 主要規格參考** +> - [詞卡管理 API 規格](./api/flashcard-management-api.md) - 開發的主要依據,包含所有 API 介面定義 +> +> **🏗️ 架構約束** +> - [後端架構詳細說明](../04_technical/backend-architecture.md) - 必須遵循的技術架構約束 +> - [系統架構總覽](../04_technical/system-architecture.md) - 了解整體系統設計脈絡 +> +> **📋 業務需求** +> - [詞卡管理功能產品需求規格](../01_requirement/詞卡管理功能產品需求規格.md) - 理解業務需求和用戶期望 \ No newline at end of file diff --git a/frontend/app/flashcards/[id]/page.tsx b/frontend/app/flashcards/[id]/page.tsx index 6378cce..ec98431 100644 --- a/frontend/app/flashcards/[id]/page.tsx +++ b/frontend/app/flashcards/[id]/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, use } from 'react' import { useRouter } from 'next/navigation' import { Navigation } from '@/components/Navigation' import { ProtectedRoute } from '@/components/ProtectedRoute' -import { flashcardsService, type Flashcard } from '@/lib/services/simplifiedFlashcards' +import { flashcardsService, type Flashcard } from '@/lib/services/flashcards' interface FlashcardDetailPageProps { params: Promise<{ @@ -84,22 +84,14 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { return } - // 載入真實詞卡 - 直接使用假資料,因為getFlashcard API不存在 - const defaultCard = mockCards['mock1'] - setFlashcard({ - ...defaultCard, - id: cardId, - word: `示例詞卡`, - translation: '示例翻譯', - definition: 'This is a sample flashcard for demonstration purposes' - }) - setEditedCard({ - ...defaultCard, - id: cardId, - word: `示例詞卡`, - translation: '示例翻譯', - definition: 'This is a sample flashcard for demonstration purposes' - }) + // 載入真實詞卡 + const result = await flashcardsService.getFlashcard(cardId) + if (result.success && result.data) { + setFlashcard(result.data) + setEditedCard(result.data) + } else { + throw new Error(result.error || '詞卡不存在') + } } catch (err) { setError('載入詞卡時發生錯誤') } finally { @@ -173,11 +165,14 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { // 真實API調用 const result = await flashcardsService.updateFlashcard(flashcard.id, { - english: editedCard.word, - chinese: editedCard.translation, + word: editedCard.word, + translation: editedCard.translation, + definition: editedCard.definition, pronunciation: editedCard.pronunciation, partOfSpeech: editedCard.partOfSpeech, - example: editedCard.example + example: editedCard.example, + exampleTranslation: editedCard.exampleTranslation, + difficultyLevel: editedCard.difficultyLevel }) if (result.success) { diff --git a/frontend/app/flashcards/page.tsx b/frontend/app/flashcards/page.tsx index 6496eca..a65ae9f 100644 --- a/frontend/app/flashcards/page.tsx +++ b/frontend/app/flashcards/page.tsx @@ -90,6 +90,15 @@ function FlashcardsContent() { loadFlashcards() }, []) + // 監聽搜尋和篩選條件變化,重新載入資料 + useEffect(() => { + const timeoutId = setTimeout(() => { + loadFlashcards() + }, 300) // 300ms 防抖 + + return () => clearTimeout(timeoutId) + }, [searchTerm, searchFilters, activeTab]) + // 暫時移除 CardSets 功能,直接設定空陣列 // const loadCardSets = async () => { // setCardSets([]) @@ -99,7 +108,16 @@ function FlashcardsContent() { try { setLoading(true) setError(null) // 清除之前的錯誤 - const result = await flashcardsService.getFlashcards() + + // 使用進階篩選參數呼叫 API + const result = await flashcardsService.getFlashcards( + searchTerm || undefined, + activeTab === 'favorites', + searchFilters.cefrLevel || undefined, + searchFilters.partOfSpeech || undefined, + searchFilters.masteryLevel || undefined + ) + if (result.success && result.data) { setFlashcards(result.data.flashcards) console.log('✅ 詞卡載入成功:', result.data.flashcards.length, '個詞卡') @@ -188,46 +206,9 @@ function FlashcardsContent() { } - const allCards = [...flashcards, ...mockFlashcards] // 合併真實和假資料 - - // 進階搜尋邏輯 - 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 as any).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 - }) + // 由於後端已處理篩選,直接使用 API 回傳的結果 + const allCards = [...flashcards, ...mockFlashcards] // 保留模擬資料用於展示 + const filteredCards = flashcards // 直接使用從 API 取得的已篩選結果 // 清除所有篩選 const clearAllFilters = () => { @@ -423,6 +404,7 @@ function FlashcardsContent() { + @@ -478,7 +460,13 @@ function FlashcardsContent() { onClick={() => setSearchFilters(prev => ({ ...prev, cefrLevel: 'C1' }))} className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-medium hover:bg-purple-200 transition-colors" > - 高級詞彙 + 高級詞彙 (C1) + + {hasActiveFilters && (