dramaling-vocab-learning/note/複習系統/後端規格_v2.0.md

16 KiB
Raw Permalink Blame History

複習系統後端規格書 (更新版)

版本: 2.0 基於: 前端技術規格實作版 + 實際系統需求 技術棧: .NET 8 + Entity Framework + SQLite 架構: RESTful API + Clean Architecture 最後更新: 2025-10-06 狀態: 🚧 準備實作階段 - 前端已完成需要後端API支援


📊 前端需求分析

前端已實現功能

  • 完整的複習流程 (翻卡記憶 + 詞彙選擇)
  • 延遲計數系統 (skipCount + wrongCount)
  • 智能排序算法 (優先級排序)
  • localStorage 進度保存
  • 線性測驗項目系統
  • 信心度評估 (0=不熟悉, 1=一般, 2=熟悉)

前端急需的API

  1. 獲取詞卡數據 - 替換靜態 api_seeds.json
  2. 記錄複習結果 - 實現間隔重複算法
  3. 進度同步 - 支援多設備學習

🏗️ API端點設計 (實作優先級)

🔥 階段1: 核心API (立即需要)

1.1 獲取待複習詞卡

GET /api/flashcards/due
Headers:
  - Authorization: Bearer {token}
Query Parameters:
  - limit: number (default: 10, max: 50)
  - includeToday: boolean (default: true)
  - includeOverdue: boolean (default: true)
  - favoritesOnly: boolean (default: false)

Response:
{
  "success": true,
  "data": {
    "flashcards": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "word": "evidence",
        "translation": "證據",
        "definition": "facts or information indicating whether a belief is true",
        "partOfSpeech": "noun",
        "pronunciation": "/ˈevɪdəns/",
        "example": "There was evidence of forced entry.",
        "exampleTranslation": "有強行進入的證據。",
        "cefr": "B2",                           // 字串格式 (前端需要)
        "difficultyLevelNumeric": 4,            // 數字格式 (後端計算)
        "isFavorite": true,
        "hasExampleImage": false,
        "primaryImageUrl": null,
        "synonyms": ["proof", "testimony"],     // 前端需要
        "createdAt": "2025-10-01T12:48:11Z",
        "updatedAt": "2025-10-01T13:37:22Z",

        // 複習相關信息
        "reviewInfo": {
          "successCount": 2,
          "nextReviewDate": "2025-10-06T10:00:00Z",
          "lastReviewDate": "2025-10-04T15:30:00Z",
          "totalCorrectCount": 5,
          "totalWrongCount": 2,
          "totalSkipCount": 1,
          "isOverdue": false,
          "daysSinceLastReview": 2
        }
      }
    ],
    "count": 8,
    "metadata": {
      "todayDue": 5,
      "overdue": 3,
      "totalReviews": 45,
      "studyStreak": 7
    }
  },
  "message": null,
  "timestamp": "2025-10-06T09:00:00Z"
}

1.2 記錄複習結果

POST /api/flashcards/{flashcardId}/review
Headers:
  - Authorization: Bearer {token}
  - Content-Type: application/json

Request Body:
{
  "confidence": 1,                    // 0=不熟悉, 1=一般, 2=熟悉 (配合前端)
  "reviewType": "flip-card",          // "flip-card" | "vocab-choice"
  "responseTimeMs": 4200,             // 回應時間
  "wasSkipped": false,                // 是否跳過
  "sessionSkipCount": 0,              // 本次會話跳過次數 (前端統計)
  "sessionWrongCount": 1,             // 本次會話錯誤次數 (前端統計)

  // 可選: 詳細信息
  "testItemId": "card-123-flip-card", // 前端測驗項目ID
  "sessionData": {                    // 會話數據 (可選)
    "totalItems": 20,
    "completedItems": 8,
    "sessionScore": { "correct": 6, "total": 8 }
  }
}

Response:
{
  "success": true,
  "data": {
    "flashcardId": "550e8400-e29b-41d4-a716-446655440000",
    "reviewId": "review-uuid-123",
    "result": {
      "isCorrect": true,                // confidence >= 1 算答對
      "newSuccessCount": 3,             // 更新後的連續成功次數
      "nextReviewDate": "2025-10-14T09:00:00Z",  // 下次複習時間
      "intervalDays": 8,                // 間隔天數 (2^3)
      "masteryProgress": 0.75,          // 熟練度進度 (0-1)
      "studyStreak": 8                  // 學習連續天數
    },
    "statistics": {
      "totalCorrectCount": 6,           // 累計正確次數
      "totalWrongCount": 2,             // 累計錯誤次數
      "totalSkipCount": 1,              // 累計跳過次數
      "averageResponseTime": 3800,      // 平均回應時間
      "lastCorrectStreak": 3            // 最近連續正確次數
    }
  },
  "timestamp": "2025-10-06T09:15:00Z"
}

1.3 獲取複習統計

GET /api/review/stats
Headers:
  - Authorization: Bearer {token}
Query Parameters:
  - period: string ("today" | "week" | "month" | "all")

Response:
{
  "success": true,
  "data": {
    "today": {
      "reviewed": 12,
      "due": 15,
      "accuracy": 0.83,
      "averageTime": 3200
    },
    "week": {
      "reviewed": 85,
      "accuracy": 0.79,
      "studyDays": 6,
      "streak": 8
    },
    "overall": {
      "totalReviews": 456,
      "totalCards": 89,
      "masteryLevel": 0.67,
      "averageInterval": 12.5
    }
  }
}

階段2: 增強功能 (次要優先)

2.1 批量複習結果提交

POST /api/review/batch
Request Body:
{
  "reviews": [
    {
      "flashcardId": "uuid-1",
      "confidence": 2,
      "reviewType": "flip-card",
      "timestamp": "2025-10-06T09:10:00Z"
    },
    {
      "flashcardId": "uuid-2",
      "confidence": 0,
      "reviewType": "vocab-choice",
      "timestamp": "2025-10-06T09:12:00Z"
    }
  ]
}

2.2 複習計劃推薦

GET /api/review/plan
Response:
{
  "recommendedDailyGoal": 20,
  "optimalStudyTime": "09:00-11:00",
  "priorityCards": [...],
  "estimatedCompletionTime": 15
}

間隔重複算法 (核心業務邏輯)

算法公式 (配合前端信心度)

public class SpacedRepetitionAlgorithm
{
    public ReviewResult ProcessReview(FlashcardReview review, int confidence, bool wasSkipped)
    {
        if (wasSkipped)
        {
            // 跳過: 不改變成功次數,明天再複習
            review.TotalSkipCount++;
            review.NextReviewDate = DateTime.UtcNow.AddDays(1);
        }
        else
        {
            var isCorrect = confidence >= 1; // 前端: 0=不熟悉, 1=一般, 2=熟悉

            if (isCorrect)
            {
                // 答對: 增加成功次數,計算新間隔
                review.SuccessCount++;
                review.TotalCorrectCount++;
                review.LastSuccessDate = DateTime.UtcNow;

                // 核心公式: 間隔 = 2^成功次數 天
                var intervalDays = Math.Pow(2, review.SuccessCount);
                var maxInterval = 180; // 最大半年
                var finalInterval = Math.Min(intervalDays, maxInterval);

                review.NextReviewDate = DateTime.UtcNow.AddDays(finalInterval);
            }
            else
            {
                // 答錯: 重置成功次數,明天再複習
                review.SuccessCount = 0;
                review.TotalWrongCount++;
                review.NextReviewDate = DateTime.UtcNow.AddDays(1);
            }
        }

        review.LastReviewDate = DateTime.UtcNow;
        review.UpdatedAt = DateTime.UtcNow;

        return new ReviewResult
        {
            IsCorrect = !wasSkipped && confidence >= 1,
            NewSuccessCount = review.SuccessCount,
            NextReviewDate = review.NextReviewDate,
            IntervalDays = CalculateIntervalDays(review.NextReviewDate)
        };
    }
}

信心度映射表

前端信心度 標籤 後端判定 下次間隔
0 不熟悉 答錯 明天 (重置)
1 一般 答對 2^(n+1) 天
2 熟悉 答對 2^(n+1) 天
skip 跳過 ⏭️ 跳過 明天 (不變)

間隔計算示例

成功次數 0 → 1: 明天 (1天)
成功次數 1 → 2: 後天 (2天)
成功次數 2 → 3: 4天後
成功次數 3 → 4: 8天後
成功次數 4 → 5: 16天後
成功次數 5 → 6: 32天後
成功次數 6 → 7: 64天後
成功次數 7+: 128天後 (最大 180天)

🗃️ 數據庫設計

FlashcardReviews 表

CREATE TABLE FlashcardReviews (
    Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    FlashcardId UNIQUEIDENTIFIER NOT NULL,
    UserId UNIQUEIDENTIFIER NOT NULL,

    -- 核心間隔重複欄位
    SuccessCount INT DEFAULT 0,                    -- 連續成功次數
    NextReviewDate DATETIME2 NOT NULL,             -- 下次複習時間
    LastReviewDate DATETIME2 NULL,                 -- 最後複習時間
    LastSuccessDate DATETIME2 NULL,                -- 最後成功時間

    -- 統計欄位
    TotalCorrectCount INT DEFAULT 0,               -- 累計正確次數
    TotalWrongCount INT DEFAULT 0,                 -- 累計錯誤次數
    TotalSkipCount INT DEFAULT 0,                  -- 累計跳過次數

    -- 系統欄位
    CreatedAt DATETIME2 DEFAULT GETUTCDATE(),
    UpdatedAt DATETIME2 DEFAULT GETUTCDATE(),

    -- 外鍵約束
    FOREIGN KEY (FlashcardId) REFERENCES Flashcards(Id),
    FOREIGN KEY (UserId) REFERENCES Users(Id),

    -- 唯一性約束 (每個用戶每張卡片只能有一條記錄)
    UNIQUE(FlashcardId, UserId)
);

-- 性能索引
CREATE INDEX IX_FlashcardReviews_NextReviewDate ON FlashcardReviews(NextReviewDate);
CREATE INDEX IX_FlashcardReviews_UserId_NextReviewDate ON FlashcardReviews(UserId, NextReviewDate);

ReviewSessions 表 (可選)

-- 會話記錄表 (用於分析和統計)
CREATE TABLE ReviewSessions (
    Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    UserId UNIQUEIDENTIFIER NOT NULL,
    StartTime DATETIME2 NOT NULL,
    EndTime DATETIME2 NULL,
    TotalItems INT DEFAULT 0,
    CompletedItems INT DEFAULT 0,
    CorrectItems INT DEFAULT 0,
    SkippedItems INT DEFAULT 0,
    AverageResponseTime INT NULL,
    SessionType NVARCHAR(50) DEFAULT 'mixed',      -- 'flip-only', 'choice-only', 'mixed'
    CreatedAt DATETIME2 DEFAULT GETUTCDATE()
);

🎯 業務邏輯服務設計

IReviewService 接口

public interface IReviewService
{
    // 核心功能
    Task<ApiResponse<List<FlashcardDto>>> GetDueFlashcardsAsync(
        string userId,
        DueFlashcardsQuery query);

    Task<ApiResponse<ReviewResult>> SubmitReviewAsync(
        string userId,
        Guid flashcardId,
        ReviewRequest request);

    // 統計功能
    Task<ApiResponse<ReviewStats>> GetReviewStatsAsync(
        string userId,
        string period = "today");

    // 批量處理
    Task<ApiResponse<List<ReviewResult>>> SubmitBatchReviewAsync(
        string userId,
        List<ReviewRequest> reviews);
}

ReviewService 實作重點

public class ReviewService : IReviewService
{
    public async Task<ApiResponse<List<FlashcardDto>>> GetDueFlashcardsAsync(
        string userId, DueFlashcardsQuery query)
    {
        var now = DateTime.UtcNow;

        // 1. 獲取用戶的詞卡
        var flashcardsQuery = _context.Flashcards
            .Where(f => f.UserId == Guid.Parse(userId) && !f.IsArchived);

        // 2. Left Join 複習記錄
        var flashcardsWithReviews = await flashcardsQuery
            .GroupJoin(_context.FlashcardReviews,
                f => f.Id,
                r => r.FlashcardId,
                (flashcard, reviews) => new {
                    Flashcard = flashcard,
                    Review = reviews.FirstOrDefault()
                })
            .Where(x =>
                // 沒有複習記錄的新卡片
                x.Review == null ||
                // 或者到期需要複習的卡片
                x.Review.NextReviewDate <= now.AddDays(query.IncludeToday ? 1 : 0))
            .Take(query.Limit)
            .ToListAsync();

        // 3. 轉換為 DTO
        return new ApiResponse<List<FlashcardDto>>
        {
            Success = true,
            Data = flashcardsWithReviews.Select(x => new FlashcardDto
            {
                // 基本詞卡信息
                Id = x.Flashcard.Id,
                Word = x.Flashcard.Word,
                // ... 其他欄位

                // 複習信息
                ReviewInfo = x.Review != null ? new ReviewInfo
                {
                    SuccessCount = x.Review.SuccessCount,
                    NextReviewDate = x.Review.NextReviewDate,
                    LastReviewDate = x.Review.LastReviewDate,
                    // ... 統計信息
                } : null
            }).ToList()
        };
    }

    public async Task<ApiResponse<ReviewResult>> SubmitReviewAsync(
        string userId, Guid flashcardId, ReviewRequest request)
    {
        // 1. 獲取或創建複習記錄
        var review = await GetOrCreateReviewAsync(userId, flashcardId);

        // 2. 使用間隔重複算法處理
        var algorithm = new SpacedRepetitionAlgorithm();
        var result = algorithm.ProcessReview(review, request.Confidence, request.WasSkipped);

        // 3. 保存到數據庫
        _context.FlashcardReviews.Update(review);
        await _context.SaveChangesAsync();

        // 4. 返回結果
        return new ApiResponse<ReviewResult>
        {
            Success = true,
            Data = result
        };
    }
}

🔄 前端集成策略

階段性集成計劃

// 階段1: API降級策略
const useReviewData = () => {
  const [dataSource, setDataSource] = useState<'static' | 'api'>('static');

  const loadFlashcards = async () => {
    try {
      // 嘗試 API 調用
      const response = await fetch('/api/flashcards/due');
      if (response.ok) {
        const data = await response.json();
        setDataSource('api');
        return data.data.flashcards;
      }
    } catch (error) {
      console.warn('API 不可用,使用靜態數據');
    }

    // 降級到靜態數據
    setDataSource('static');
    return SIMPLE_CARDS;
  };
};

// 階段2: 複習結果同步
const submitReview = async (flashcardId: string, confidence: number) => {
  // 立即更新前端狀態
  updateLocalState(confidence);

  // 異步提交到後端
  if (dataSource === 'api') {
    try {
      await apiService.submitReview(flashcardId, {
        confidence,
        reviewType: currentTestItem.testType,
        responseTimeMs: calculateResponseTime(),
        wasSkipped: false
      });
    } catch (error) {
      console.warn('API 提交失敗,保持本地狀態');
    }
  }
};

📋 開發優先級與時程

🔥 第1週: 核心API (必須完成)

  • FlashcardReview 實體設計
  • FlashcardsController 基礎端點
  • 間隔重複算法實作
  • 數據庫遷移

第2週: 前端集成 (關鍵)

  • API服務層封裝
  • 前端降級處理
  • 信心度映射統一
  • 錯誤處理完善

📊 第3週: 優化與測試

  • 性能優化
  • 批量處理
  • 統計功能
  • 完整測試

🎯 第4週: 完善與部署

  • 文檔更新
  • 監控集成
  • 生產部署
  • 用戶驗收

🎯 成功標準

功能驗收標準

  • 前端可以從 API 獲取真實詞卡數據
  • 複習結果正確提交並計算間隔時間
  • 間隔重複算法按 2^n 公式運作
  • API 失敗時前端仍可正常運作
  • 多設備間複習進度同步

性能標準

  • 獲取詞卡 API < 500ms
  • 提交複習結果 < 200ms
  • 支援並發用戶 > 100
  • 數據庫查詢優化 < 100ms

可靠性標準

  • API 可用性 > 99.5%
  • 數據一致性保證
  • 錯誤處理完善
  • 監控和日誌完整

此規格書基於前端實作需求撰寫確保後端API能完美支援現有前端功能 維護責任: 後端開發團隊 更新觸發: 前端需求變更或API優化