# 複習系統後端規格書 **版本**: 1.0 **對應**: 前端規格.md + 技術實作規格.md **技術棧**: .NET 8 + Entity Framework + SQLite **架構**: RESTful API + Clean Architecture **最後更新**: 2025-10-03 --- ## 🏗️ **API端點設計** ### **階段1: 純靜態 (當前MVP)** ``` 前端: 使用 api_seeds.json 靜態數據 後端: 完全不需要開發 狀態: 前端React useState管理所有邏輯 目的: 驗證用戶體驗和延遲計數系統 ``` ### **階段2: 基礎持久化 (用戶要求時)** ``` 前端: 同階段1,但添加localStorage持久化 後端: 仍然不需要開發 新增: 本地進度保存和恢復 觸發: 用戶反饋希望保存進度 ``` ### **階段3: API集成 (遠期需求)** **獲取複習卡片**: ```http GET /api/flashcards/due Query Parameters: - limit: number (default: 10) - userId: string (from auth) Response: { "success": true, "data": { "flashcards": [ { "id": "uuid", "word": "evidence", "definition": "facts indicating truth", "partOfSpeech": "noun", "pronunciation": "/ˈevɪdəns/", "example": "There was evidence of...", "exampleTranslation": "有...的證據", "cefr": "B2", "difficultyLevelNumeric": 4, "isFavorite": true, "hasExampleImage": false, "primaryImageUrl": null, "createdAt": "2025-10-01T12:48:11Z", "updatedAt": "2025-10-01T13:37:22Z" } ], "count": 4 }, "message": null, "timestamp": "2025-10-03T18:57:25Z" } ``` **記錄複習結果**: ```http POST /api/flashcards/{flashcardId}/review Headers: - Authorization: Bearer {token} - Content-Type: application/json Request Body: { "confidence": 2, // 1=模糊, 2=一般, 3=熟悉 "isCorrect": true, // 基於 confidence >= 2 判斷 "reviewType": "flip-memory", "responseTimeMs": 3500, // 回應時間 "wasSkipped": false, // 是否是跳過的題目 "skipCount": 0, // 該卡片的跳過次數 (前端統計) "wrongCount": 1 // 該卡片的答錯次數 (前端統計) } Response: { "success": true, "data": { "flashcardId": "uuid", "newSuccessCount": 3, "nextReviewDate": "2025-10-08T12:00:00Z", "masteryLevelChange": 0.1 }, "timestamp": "2025-10-03T19:00:00Z" } ``` --- ## ⏰ **核心算法: 間隔重複系統** ### **複習時間計算公式** (您的需求) ```csharp // 基礎公式: 下一次複習時間 = 2^成功複習次數 public DateTime CalculateNextReviewDate(int successCount) { // 計算間隔天數 var intervalDays = Math.Pow(2, successCount); // 設定最大間隔限制 (避免間隔過長) var maxIntervalDays = 180; // 最多半年 var finalInterval = Math.Min(intervalDays, maxIntervalDays); return DateTime.UtcNow.AddDays(finalInterval); } ``` ### **具體計算範例** ```csharp // 學習進度示例 成功次數 0: 下次複習 = 今天 + 2^0 = 明天 (1天後) 成功次數 1: 下次複習 = 今天 + 2^1 = 後天 (2天後) 成功次數 2: 下次複習 = 今天 + 2^2 = 4天後 成功次數 3: 下次複習 = 今天 + 2^3 = 8天後 成功次數 4: 下次複習 = 今天 + 2^4 = 16天後 成功次數 5: 下次複習 = 今天 + 2^5 = 32天後 成功次數 6: 下次複習 = 今天 + 2^6 = 64天後 成功次數 7: 下次複習 = 今天 + 2^7 = 128天後 成功次數 8+: 下次複習 = 今天 + 180天 (上限) ``` ### **成功計數更新邏輯** ```csharp public async Task ProcessReviewAttemptAsync(string flashcardId, ReviewAttemptRequest request) { var stats = await GetFlashcardStatsAsync(flashcardId, request.UserId); if (request.IsCorrect) { // 答對: 增加成功次數,計算新的複習時間 stats.SuccessCount++; stats.NextReviewDate = CalculateNextReviewDate(stats.SuccessCount); stats.LastSuccessDate = DateTime.UtcNow; } else { // 答錯或跳過: 重置成功次數,明天再複習 stats.SuccessCount = 0; stats.NextReviewDate = DateTime.UtcNow.AddDays(1); } // 記錄延遲統計 (配合前端延遲計數) if (request.WasSkipped) { stats.TotalSkipCount += 1; } if (!request.IsCorrect && !request.WasSkipped) { stats.TotalWrongCount += 1; } await SaveStatsAsync(stats); return new ReviewResult { FlashcardId = flashcardId, NewSuccessCount = stats.SuccessCount, NextReviewDate = stats.NextReviewDate, IntervalDays = CalculateIntervalDays(stats.SuccessCount) }; } ``` ### **與前端延遲計數的配合** ```csharp // 前端延遲計數系統的後端記錄 public class FlashcardSessionState { public string FlashcardId { get; set; } public int SessionSkipCount { get; set; } // 本次會話跳過次數 public int SessionWrongCount { get; set; } // 本次會話答錯次數 public DateTime LastAttemptTime { get; set; } } // 處理會話中的延遲行為 public void RecordSessionDelay(string flashcardId, bool wasSkipped, bool wasWrong) { var sessionState = GetOrCreateSessionState(flashcardId); if (wasSkipped) sessionState.SessionSkipCount++; if (wasWrong) sessionState.SessionWrongCount++; sessionState.LastAttemptTime = DateTime.UtcNow; } ``` --- ## 🗃️ **數據庫設計** ### **現有表格** (已存在) ```sql -- Flashcards 表 (主要詞卡信息) CREATE TABLE Flashcards ( Id UNIQUEIDENTIFIER PRIMARY KEY, Word NVARCHAR(100) NOT NULL, Definition NVARCHAR(500), PartOfSpeech NVARCHAR(50), Pronunciation NVARCHAR(100), Example NVARCHAR(500), ExampleTranslation NVARCHAR(500), Cefr NVARCHAR(10), DifficultyLevelNumeric INT, IsFavorite BIT DEFAULT 0, CreatedAt DATETIME2, UpdatedAt DATETIME2 ) ``` ### **極簡數據庫設計** (階段3需要時) ```sql -- 只需要一個簡單的複習記錄表 CREATE TABLE FlashcardReviews ( Id UNIQUEIDENTIFIER PRIMARY KEY, FlashcardId UNIQUEIDENTIFIER NOT NULL, UserId UNIQUEIDENTIFIER NOT NULL, -- 核心欄位 (您的算法需要) SuccessCount INT DEFAULT 0, -- 連續成功次數 (用於2^n計算) NextReviewDate DATETIME2, -- 下次複習時間 LastReviewDate DATETIME2, -- 最後複習時間 -- 創建和更新時間 CreatedAt DATETIME2 DEFAULT GETUTCDATE(), UpdatedAt DATETIME2 DEFAULT GETUTCDATE(), FOREIGN KEY (FlashcardId) REFERENCES Flashcards(Id), UNIQUE(FlashcardId, UserId) ) -- 就這樣!不需要複雜的Sessions和Attempts表 -- 前端延遲計數在前端處理,不需要後端記錄 ``` --- ## 🎯 **業務邏輯服務** ### **極簡ReviewService** ```csharp // 簡化的介面 (只保留核心功能) public interface IReviewService { Task> GetDueFlashcardsAsync(string userId, int limit = 10); Task> UpdateReviewStatusAsync(string flashcardId, bool isCorrect, string userId); } // 極簡實作 (專注核心邏輯) public class ReviewService : IReviewService { public async Task> UpdateReviewStatusAsync( string flashcardId, bool isCorrect, string userId) { // 1. 獲取或創建複習記錄 var review = await GetOrCreateReviewAsync(flashcardId, userId); // 2. 更新成功次數 (您的核心算法) if (isCorrect) { review.SuccessCount++; } else { review.SuccessCount = 0; // 重置 } // 3. 計算下次複習時間 (您的公式) if (isCorrect) { var intervalDays = Math.Pow(2, review.SuccessCount); review.NextReviewDate = DateTime.UtcNow.AddDays(intervalDays); } else { review.NextReviewDate = DateTime.UtcNow.AddDays(1); // 明天再試 } review.LastReviewDate = DateTime.UtcNow; review.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return new ApiResponse { Success = true, Data = new ReviewResult { FlashcardId = flashcardId, SuccessCount = review.SuccessCount, NextReviewDate = review.NextReviewDate, IntervalDays = (int)Math.Pow(2, review.SuccessCount) } }; } // 獲取到期詞卡 (簡化邏輯) public async Task> GetDueFlashcardsAsync(string userId, int limit = 10) { var dueCards = await _context.Flashcards .Where(f => f.UserId == userId) .LeftJoin(_context.FlashcardReviews, f => f.Id, r => r.FlashcardId, (flashcard, review) => new { flashcard, review }) .Where(x => x.review == null || x.review.NextReviewDate <= DateTime.UtcNow) .Select(x => x.flashcard) .Take(limit) .ToArrayAsync(); return new ApiResponse { Success = true, Data = dueCards.Select(MapToDto).ToArray() }; } } ``` --- ## 📊 **階段性實作計劃** ### **階段1: 純前端 (當前)** ``` 實作範圍: 無後端開發 前端邏輯: 延遲計數系統、排序邏輯都在React中 數據來源: api_seeds.json 靜態文件 ``` ### **階段2: 本地持久化** ``` 實作範圍: 仍無後端開發 前端增強: localStorage 保存學習進度 觸發條件: 用戶要求保存進度 ``` ### **階段3: 基礎API** ``` 實作範圍: 極簡後端API 核心功能: - GET /api/flashcards/due (獲取到期詞卡) - POST /api/flashcards/{id}/review (更新複習狀態) - 簡單的NextReviewDate計算 避免功能: - 詳細的統計分析 - 複雜的會話管理 - 用戶行為追蹤 ``` --- ## 🔐 **安全性規格** ### **身份驗證和授權** ```csharp // JWT Token 驗證 [Authorize] [Route("api/flashcards")] public class FlashcardsController : ControllerBase { // 確保用戶只能存取自己的數據 private async Task ValidateUserAccessAsync(string flashcardId) { var userId = User.GetUserId(); return await _flashcardService.BelongsToUserAsync(flashcardId, userId); } } // 資料驗證 public class ReviewAttemptRequest { [Range(1, 3)] public int Confidence { get; set; } [Range(0, 300000)] // 最多5分鐘 public int ResponseTimeMs { get; set; } [Range(0, int.MaxValue)] public int SkipCount { get; set; } [Range(0, int.MaxValue)] public int WrongCount { get; set; } } ``` ### **資料保護** ```csharp // 個人資料保護 public class PrivacyService { // 匿名化統計數據 public async Task GetAnonymizedStatsAsync() { // 移除個人識別信息的統計 } // 資料導出 (GDPR) public async Task ExportUserDataAsync(string userId) { return new UserDataExport { ReviewSessions = await GetUserReviewSessions(userId), Flashcards = await GetUserFlashcards(userId), LearningStats = await GetUserStats(userId) }; } // 資料刪除 public async Task DeleteUserDataAsync(string userId) { // 完全刪除用戶所有學習數據 } } ``` --- ## ⚡ **性能優化規格** ### **資料庫優化** ```sql -- 重要索引 CREATE INDEX IX_ReviewAttempts_FlashcardId_UserId ON ReviewAttempts (FlashcardId, UserId) CREATE INDEX IX_FlashcardReviewStats_UserId_NextReviewDate ON FlashcardReviewStats (UserId, NextReviewDate) CREATE INDEX IX_ReviewSessions_UserId_StartTime ON ReviewSessions (UserId, StartTime) ``` ### **API性能目標** ```csharp // 性能指標 public static class PerformanceTargets { public const int GetDueCardsMaxMs = 500; // 獲取卡片 < 500ms public const int RecordAttemptMaxMs = 200; // 記錄結果 < 200ms public const int DatabaseQueryMaxMs = 100; // 資料庫查詢 < 100ms } // 快取策略 [ResponseCache(Duration = 300)] // 5分鐘快取 public async Task>> GetDueFlashcards() { // 實作快取邏輯 } ``` --- ## 📊 **階段性實作計劃** ### **階段1: 靜態數據 (已完成)** ``` 前端: 使用 api_seeds.json 後端: 無需開發 目的: 驗證前端邏輯 ``` ### **階段2: 基礎API (階段3觸發時)** ``` 實作範圍: ✅ GET /api/flashcards/due (基礎版) ✅ POST /api/flashcards/{id}/review (簡化版) ✅ 基礎的錯誤處理 不實作: ❌ 複雜的快取機制 ❌ 高級統計分析 ❌ 複雜的權限控制 ``` ### **階段3: 完整API (遠期)** ``` 實作範圍: ✅ 完整的用戶權限驗證 ✅ 詳細的學習統計 ✅ 系統監控和分析 ✅ 性能優化和快取 ``` --- ## 🔧 **開發環境設置** ### **API開發工具** ```csharp // Swagger文檔配置 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "DramaLing Review API", Version = "v1" }); // 加入JWT驗證 c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter JWT token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "bearer" }); }); ``` ### **測試環境** ```csharp // 測試資料庫 public class TestDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseInMemoryDatabase("TestReviewDb"); } } // API測試 [TestClass] public class ReviewApiTests { [TestMethod] public async Task GetDueCards_ShouldReturnUserCards() { // 準備測試數據 // 調用API // 驗證結果 } } ``` --- ## 📋 **部署和監控規格** ### **部署配置** ```yaml # Docker配置 services: review-api: image: dramaling/review-api:latest environment: - ConnectionStrings__DefaultConnection=${DB_CONNECTION} - JwtSettings__Secret=${JWT_SECRET} ports: - "5008:8080" depends_on: - database database: image: postgres:15 environment: - POSTGRES_DB=dramaling_review - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} ``` ### **監控指標** ```csharp // 健康檢查 services.AddHealthChecks() .AddDbContextCheck() .AddCheck("api-health"); // 性能監控 public class PerformanceMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var stopwatch = Stopwatch.StartNew(); await next(context); stopwatch.Stop(); // 記錄慢查詢 if (stopwatch.ElapsedMilliseconds > 1000) { _logger.LogWarning($"Slow request: {context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms"); } } } ``` --- ## 🚨 **錯誤處理規格** ### **統一錯誤回應** ```csharp public class ApiResponse { public bool Success { get; set; } public T? Data { get; set; } public string? Message { get; set; } public string? Error { get; set; } public DateTime Timestamp { get; set; } = DateTime.UtcNow; } // 錯誤類型 public enum ErrorType { Validation, // 資料驗證錯誤 NotFound, // 資源不存在 Unauthorized, // 權限不足 RateLimit, // 請求頻率限制 ServerError // 服務器錯誤 } ``` ### **錯誤處理中介軟體** ```csharp public class ErrorHandlingMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { try { await next(context); } catch (ValidationException ex) { await HandleValidationErrorAsync(context, ex); } catch (UnauthorizedAccessException ex) { await HandleUnauthorizedErrorAsync(context, ex); } catch (Exception ex) { await HandleGenericErrorAsync(context, ex); } } } ``` --- *後端規格維護: 後端開發團隊* *實作觸發: 階段3 API集成需求確認時* *目標: 支持前端複習功能的後端服務*