# 複習系統後端規格書

**版本**: 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<ReviewResult> 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<ApiResponse<FlashcardDto[]>> GetDueFlashcardsAsync(string userId, int limit = 10);
    Task<ApiResponse<ReviewResult>> UpdateReviewStatusAsync(string flashcardId, bool isCorrect, string userId);
}

// 極簡實作 (專注核心邏輯)
public class ReviewService : IReviewService
{
    public async Task<ApiResponse<ReviewResult>> 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<ReviewResult>
        {
            Success = true,
            Data = new ReviewResult
            {
                FlashcardId = flashcardId,
                SuccessCount = review.SuccessCount,
                NextReviewDate = review.NextReviewDate,
                IntervalDays = (int)Math.Pow(2, review.SuccessCount)
            }
        };
    }

    // 獲取到期詞卡 (簡化邏輯)
    public async Task<ApiResponse<FlashcardDto[]>> 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<FlashcardDto[]>
        {
            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<bool> 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<AnonymizedStats> GetAnonymizedStatsAsync()
    {
        // 移除個人識別信息的統計
    }

    // 資料導出 (GDPR)
    public async Task<UserDataExport> 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<ActionResult<ApiResponse<DueCardsResult>>> 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<ApplicationDbContext>()
    .AddCheck<ApiHealthCheck>("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<T>
{
    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集成需求確認時*
*目標: 支持前端複習功能的後端服務*