dramaling-vocab-learning/note/智能複習/智能複習系統-後端功能規格書.md

23 KiB
Raw Blame History

智能複習系統 - 後端功能規格書 (BFS)

目標讀者: 後端開發工程師、系統架構師 版本: 1.0 日期: 2025-09-25


🏗️ 系統架構 (基於現有ASP.NET Core)

整合到現有架構

┌─────────────────────────────────────────┐
│          FlashcardsController           │
│ ┌─────────────────────────────────────┐ │
│ │         智能複習端點群組              │ │
│ │ • /api/flashcards/due               │ │
│ │ • /api/flashcards/next-review       │ │
│ │ • /api/flashcards/{id}/review       │ │
│ │ • /api/flashcards/{id}/optimal-mode │ │
│ │ • /api/flashcards/{id}/question     │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────┘
                  │
        ┌─────────▼─────────┐
        │  智能複習服務層    │
        │ ┌───────────────┐ │
        │ │SpacedRepetition│ │
        │ │   Service     │ │
        │ ├───────────────┤ │
        │ │ReviewType     │ │
        │ │ Selector      │ │
        │ ├───────────────┤ │
        │ │A1Protection   │ │
        │ │   Service     │ │
        │ └───────────────┘ │
        └─────────┬─────────┘
                  │
        ┌─────────▼─────────┐
        │  現有DramaLing    │
        │    DbContext      │
        │ (擴展Flashcard)   │
        └───────────────────┘

智能複習服務層設計 (新增)

1. SpacedRepetitionService (核心間隔重複算法)

public interface ISpacedRepetitionService
{
    Task<ReviewResult> ProcessReviewAsync(Guid flashcardId, ReviewRequest request);
    int CalculateCurrentMasteryLevel(Flashcard flashcard);
    Task<List<Flashcard>> GetDueFlashcardsAsync(Guid userId, DateTime? date = null, int limit = 50);
    Task<Flashcard?> GetNextReviewCardAsync(Guid userId);
}

public class SpacedRepetitionService : ISpacedRepetitionService
{
    private readonly DramaLingDbContext _context;
    private readonly ILogger<SpacedRepetitionService> _logger;

    public async Task<ReviewResult> ProcessReviewAsync(Guid flashcardId, ReviewRequest request)
    {
        var flashcard = await _context.Flashcards.FindAsync(flashcardId);
        if (flashcard == null) throw new ArgumentException("Flashcard not found");

        // 1. 計算逾期天數
        var actualReviewDate = DateTime.Now.Date;
        var overdueDays = (actualReviewDate - flashcard.NextReviewDate.Date).Days;

        // 2. 計算新間隔 (基於演算法規格書)
        var newInterval = CalculateNewInterval(
            flashcard.IntervalDays,
            request.IsCorrect,
            request.ConfidenceLevel,
            request.QuestionType,
            overdueDays
        );

        // 3. 更新熟悉度
        var newMasteryLevel = CalculateMasteryLevel(
            flashcard.TimesCorrect + (request.IsCorrect ? 1 : 0),
            flashcard.TimesReviewed + 1,
            newInterval
        );

        // 4. 更新資料庫
        flashcard.MasteryLevel = newMasteryLevel;
        flashcard.TimesReviewed++;
        if (request.IsCorrect) flashcard.TimesCorrect++;
        flashcard.IntervalDays = newInterval;
        flashcard.NextReviewDate = actualReviewDate.AddDays(newInterval);
        flashcard.LastReviewedAt = DateTime.Now;
        flashcard.LastQuestionType = request.QuestionType;

        await _context.SaveChangesAsync();

        return new ReviewResult
        {
            NewInterval = newInterval,
            NextReviewDate = flashcard.NextReviewDate,
            MasteryLevel = newMasteryLevel,
            CurrentMasteryLevel = CalculateCurrentMasteryLevel(flashcard)
        };
    }
}

2. ReviewTypeSelectorService (智能題型選擇)

public interface IReviewTypeSelectorService
{
    Task<ReviewModeResult> SelectOptimalReviewModeAsync(Guid flashcardId, int userLevel, int wordLevel);
    string[] GetAvailableReviewTypes(int userLevel, int wordLevel);
    bool IsA1Learner(int userLevel);
}

public class ReviewTypeSelectorService : IReviewTypeSelectorService
{
    public async Task<ReviewModeResult> SelectOptimalReviewModeAsync(
        Guid flashcardId, int userLevel, int wordLevel)
    {
        // 1. 四情境判斷
        var availableModes = GetAvailableReviewTypes(userLevel, wordLevel);

        // 2. 檢查復習歷史,避免重複
        var recentHistory = await GetRecentReviewHistory(flashcardId, 3);
        var filteredModes = ApplyAntiRepetitionLogic(availableModes, recentHistory);

        // 3. 智能選擇
        var selectedMode = SelectModeWithWeights(filteredModes, userLevel);

        return new ReviewModeResult
        {
            SelectedMode = selectedMode,
            AvailableModes = availableModes,
            AdaptationContext = GetAdaptationContext(userLevel, wordLevel),
            Reason = GetSelectionReason(selectedMode, userLevel, wordLevel)
        };
    }

    public string[] GetAvailableReviewTypes(int userLevel, int wordLevel)
    {
        var difficulty = wordLevel - userLevel;

        if (userLevel <= 20)
            return new[] { "flip-memory", "vocab-choice", "vocab-listening" }; // A1保護

        if (difficulty < -10)
            return new[] { "sentence-reorder", "sentence-fill" }; // 簡單詞彙

        if (difficulty >= -10 && difficulty <= 10)
            return new[] { "sentence-fill", "sentence-reorder", "sentence-speaking" }; // 適中詞彙

        return new[] { "flip-memory", "vocab-choice" }; // 困難詞彙
    }
}

3. QuestionGeneratorService (題目生成)

public interface IQuestionGeneratorService
{
    Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType);
}

public class QuestionGeneratorService : IQuestionGeneratorService
{
    public async Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType)
    {
        var flashcard = await _context.Flashcards.FindAsync(flashcardId);
        if (flashcard == null) throw new ArgumentException("Flashcard not found");

        return questionType switch
        {
            "vocab-choice" => await GenerateVocabChoiceOptions(flashcard),
            "sentence-fill" => GenerateFillBlankQuestion(flashcard),
            "sentence-reorder" => GenerateReorderQuestion(flashcard),
            "sentence-listening" => await GenerateSentenceListeningOptions(flashcard),
            _ => new QuestionData { QuestionType = questionType, CorrectAnswer = flashcard.Word }
        };
    }

    private async Task<QuestionData> GenerateVocabChoiceOptions(Flashcard flashcard)
    {
        // 從其他詞卡中選擇3個干擾選項
        var distractors = await _context.Flashcards
            .Where(f => f.UserId == flashcard.UserId && f.Id != flashcard.Id)
            .OrderBy(x => Guid.NewGuid())
            .Take(3)
            .Select(f => f.Word)
            .ToListAsync();

        var options = new List<string> { flashcard.Word };
        options.AddRange(distractors);

        return new QuestionData
        {
            QuestionType = "vocab-choice",
            Options = options.OrderBy(x => Guid.NewGuid()).ToArray(),
            CorrectAnswer = flashcard.Word
        };
    }
}

🔌 智能複習API設計 (新增到現有FlashcardsController)

1. GET /api/flashcards/due (新增)

描述: 取得當前用戶的到期詞卡列表

查詢參數

interface DueFlashcardsQuery {
  date?: string;    // 查詢日期 (預設今天)
  limit?: number;   // 回傳數量限制 (預設50)
}

響應格式

{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "word": "sophisticated",
      "translation": "精密的",
      "definition": "Highly developed or complex",
      "example": "A sophisticated system",
      "exampleTranslation": "一個精密的系統",
      "masteryLevel": 75,
      "nextReviewDate": "2025-09-25",
      "isOverdue": true,
      "overdueDays": 2,
      // 智能複習需要的欄位
      "userLevel": 60,        // 學習者程度
      "wordLevel": 85,        // 詞彙難度
      "baseMasteryLevel": 75,
      "lastReviewDate": "2025-09-20"
    }
  ],
  "count": 12
}

2. GET /api/flashcards/next-review (新增)

描述: 取得下一張需要復習的詞卡 (依優先級排序)

響應格式

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "word": "sophisticated",
    "translation": "精密的",
    "definition": "Highly developed or complex",
    "pronunciation": "/səˈfɪstɪkeɪtɪd/",
    "partOfSpeech": "adjective",
    "example": "The software uses sophisticated algorithms.",
    "exampleTranslation": "該軟體使用精密的算法。",
    "masteryLevel": 25,
    "timesReviewed": 3,
    "isFavorite": false,
    "nextReviewDate": "2025-09-25",
    "difficultyLevel": "C1",
    // 智能複習擴展欄位
    "userLevel": 50,           // 從用戶資料計算
    "wordLevel": 85,           // 從CEFR等級映射
    "baseMasteryLevel": 30,
    "lastReviewDate": "2025-09-20",
    "currentInterval": 7,
    "isOverdue": true,
    "overdueDays": 5
  }
}

3. POST /api/flashcards/{id}/optimal-review-mode (新增)

描述: 系統自動選擇最適合的複習題型

請求格式

{
  "userLevel": 50,
  "wordLevel": 85,
  "includeHistory": true
}

響應格式

{
  "success": true,
  "data": {
    "selectedMode": "flip-memory",
    "reason": "困難詞彙,使用基礎題型重建記憶",
    "availableModes": ["flip-memory", "vocab-choice"],
    "adaptationContext": "困難詞彙情境"
  }
}

4. POST /api/flashcards/{id}/review (更新)

描述: 提交復習結果並更新間隔重複算法

請求格式

{
  "isCorrect": boolean,
  "confidenceLevel": number,    // 1-5 (翻卡題)
  "questionType": "flip-memory" | "vocab-choice" | "vocab-listening" |
                  "sentence-listening" | "sentence-fill" |
                  "sentence-reorder" | "sentence-speaking",
  "userAnswer": string,         // 用戶的答案
  "timeTaken": number,          // 答題時間(毫秒)
  "timestamp": number
}

響應格式

{
  "success": true,
  "data": {
    "newInterval": 15,
    "nextReviewDate": "2025-10-10",
    "masteryLevel": 65,           // 更新後的熟悉度
    "currentMasteryLevel": 65,    // 當前熟悉度
    "isOverdue": false,
    "performanceFactor": 1.1,     // 表現係數
    "growthFactor": 1.4          // 成長係數
  }
}

5. POST /api/flashcards/{id}/question (新增)

描述: 為指定題型生成題目選項和資料

請求格式

{
  "questionType": "vocab-choice" | "sentence-listening" | "sentence-fill"
}

響應格式

{
  "success": true,
  "data": {
    "questionType": "vocab-choice",
    "options": ["sophisticated", "simple", "basic", "complex"],
    "correctAnswer": "sophisticated",
    "audioUrl": "/audio/sophisticated.mp3",
    "sentence": "The software uses sophisticated algorithms.",
    "blankedSentence": "The software uses _______ algorithms.",
    "scrambledWords": ["The", "software", "uses", "sophisticated", "algorithms"]
  }
}

---

## 🛡️ **安全與驗證**

### **輸入驗證規則**
```csharp
public class ReviewRequestValidator : AbstractValidator<ReviewRequest>
{
    public ReviewRequestValidator()
    {
        RuleFor(x => x.IsCorrect).NotNull();

        RuleFor(x => x.ConfidenceLevel)
            .InclusiveBetween(1, 5)
            .When(x => x.QuestionType == "flipcard");

        RuleFor(x => x.QuestionType)
            .Must(BeValidQuestionType)
            .WithMessage("questionType 必須是 flipcard, multiple_choice 或 fill_blank");
    }
}

錯誤處理策略

  • 4xx 錯誤: 客戶端輸入錯誤,返回詳細錯誤訊息
  • 5xx 錯誤: 服務器錯誤,記錄日誌並返回通用錯誤訊息
  • 資料庫錯誤: 重試機制最多3次重試

💾 資料庫設計 (基於現有DramaLingDbContext)

現有Flashcard模型分析

// 現有欄位 (已存在,無需修改)
public class Flashcard
{
    public Guid Id { get; set; }
    public string Word { get; set; }
    public string Translation { get; set; }
    public string Definition { get; set; }
    public string? Example { get; set; }
    public string? ExampleTranslation { get; set; }
    public int MasteryLevel { get; set; }        // ✅ 可直接使用
    public int TimesReviewed { get; set; }       // ✅ 可直接使用
    public DateTime NextReviewDate { get; set; } // ✅ 可直接使用
    public DateTime? LastReviewedAt { get; set; } // ✅ 可重命名使用
    public string? DifficultyLevel { get; set; } // ✅ 用於CEFR等級
}

需要新增的智能複習欄位

-- 新增到現有 Flashcards 表
ALTER TABLE Flashcards ADD COLUMN
    IntervalDays INT DEFAULT 1,        -- 當前間隔天數
    TimesCorrect INT DEFAULT 0,        -- 答對次數
    UserLevel INT DEFAULT 50,          -- 學習者程度 (1-100)
    WordLevel INT DEFAULT 50,          -- 詞彙難度 (1-100)
    ReviewHistory TEXT,                -- JSON格式的復習歷史
    LastQuestionType VARCHAR(50);      -- 最後使用的題型

-- 重新命名現有欄位 (可選)
-- LastReviewedAt → LastReviewDate (語義更清楚)

CEFR等級到詞彙難度映射

public static class CEFRMapper
{
    private static readonly Dictionary<string, int> CEFRToWordLevel = new()
    {
        { "A1", 20 },  // 基礎詞彙
        { "A2", 35 },  // 常用詞彙
        { "B1", 50 },  // 中級詞彙
        { "B2", 65 },  // 中高級詞彙
        { "C1", 80 },  // 高級詞彙
        { "C2", 95 }   // 精通詞彙
    };

    public static int GetWordLevel(string? cefrLevel)
    {
        return CEFRToWordLevel.GetValueOrDefault(cefrLevel ?? "B1", 50);
    }
}

索引優化 (基於現有表結構)

-- 智能複習相關索引
CREATE INDEX IX_Flashcards_DueReview
ON Flashcards(UserId, NextReviewDate)
WHERE IsArchived = 0;

-- 逾期詞卡快速查詢
CREATE INDEX IX_Flashcards_Overdue
ON Flashcards(UserId, NextReviewDate, LastReviewedAt)
WHERE IsArchived = 0 AND NextReviewDate < DATE('now');

-- 學習統計查詢優化
CREATE INDEX IX_Flashcards_UserStats
ON Flashcards(UserId, MasteryLevel, TimesReviewed)
WHERE IsArchived = 0;

⚙️ 服務註冊與配置 (整合到現有架構)

依賴注入配置 (Program.cs 或 ServiceCollectionExtensions.cs)

// 新增智能複習服務到現有服務註冊
public static IServiceCollection AddSpacedRepetitionServices(this IServiceCollection services)
{
    // 核心智能複習服務
    services.AddScoped<ISpacedRepetitionService, SpacedRepetitionService>();
    services.AddScoped<IReviewTypeSelectorService, ReviewTypeSelectorService>();
    services.AddScoped<IQuestionGeneratorService, QuestionGeneratorService>();

    // 配置選項
    services.Configure<SpacedRepetitionOptions>(configuration.GetSection("SpacedRepetition"));

    return services;
}

// 在 Program.cs 中調用
builder.Services.AddSpacedRepetitionServices();

appsettings.json 配置

{
  "SpacedRepetition": {
    "GrowthFactors": {
      "ShortTerm": 1.8,     // ≤7天間隔
      "MediumTerm": 1.4,    // 8-30天間隔
      "LongTerm": 1.2,      // 31-90天間隔
      "VeryLongTerm": 1.1   // >90天間隔
    },
    "OverduePenalties": {
      "Light": 0.9,         // 1-3天逾期
      "Medium": 0.75,       // 4-7天逾期
      "Heavy": 0.5,         // 8-30天逾期
      "Extreme": 0.3        // >30天逾期
    },
    "MemoryDecayRate": 0.05,  // 每天5%衰減率
    "MaxInterval": 365,       // 最大間隔天數
    "A1ProtectionLevel": 20,  // A1學習者程度門檻
    "DefaultUserLevel": 50    // 新用戶預設程度
  }
}

FlashcardsController 擴展

// 在現有 FlashcardsController 中新增智能複習端點
[ApiController]
[Route("api/flashcards")]
[AllowAnonymous] // 開發階段
public class FlashcardsController : ControllerBase
{
    private readonly DramaLingDbContext _context;
    private readonly ISpacedRepetitionService _spacedRepetitionService;
    private readonly IReviewTypeSelectorService _reviewTypeSelectorService;
    private readonly IQuestionGeneratorService _questionGeneratorService;

    // ... 現有的CRUD端點保持不變 ...

    // ================== 新增智能複習端點 ==================

    [HttpGet("due")]
    public async Task<ActionResult> GetDueFlashcards(
        [FromQuery] string? date = null,
        [FromQuery] int limit = 50)
    {
        var userId = GetUserId();
        var queryDate = DateTime.TryParse(date, out var parsed) ? parsed : DateTime.Now.Date;

        var dueCards = await _spacedRepetitionService.GetDueFlashcardsAsync(userId, queryDate, limit);

        return Ok(new { success = true, data = dueCards, count = dueCards.Count });
    }

    [HttpGet("next-review")]
    public async Task<ActionResult> GetNextReviewCard()
    {
        var userId = GetUserId();
        var nextCard = await _spacedRepetitionService.GetNextReviewCardAsync(userId);

        if (nextCard == null)
            return Ok(new { success = true, data = (object?)null, message = "沒有到期的詞卡" });

        return Ok(new { success = true, data = nextCard });
    }

    [HttpPost("{id}/optimal-review-mode")]
    public async Task<ActionResult> GetOptimalReviewMode(Guid id, [FromBody] OptimalModeRequest request)
    {
        var result = await _reviewTypeSelectorService.SelectOptimalReviewModeAsync(
            id, request.UserLevel, request.WordLevel);

        return Ok(new { success = true, data = result });
    }

    [HttpPost("{id}/question")]
    public async Task<ActionResult> GenerateQuestion(Guid id, [FromBody] QuestionRequest request)
    {
        var questionData = await _questionGeneratorService.GenerateQuestionAsync(id, request.QuestionType);

        return Ok(new { success = true, data = questionData });
    }

    [HttpPost("{id}/review")]
    public async Task<ActionResult> SubmitReview(Guid id, [FromBody] ReviewRequest request)
    {
        var result = await _spacedRepetitionService.ProcessReviewAsync(id, request);

        return Ok(new { success = true, data = result });
    }
}

---

## 🔍 **監控與日誌**

### **關鍵指標監控**
```csharp
public class ReviewMetrics
{
    [Counter("reviews_processed_total")]
    public static readonly Counter ReviewsProcessed;

    [Histogram("review_calculation_duration_ms")]
    public static readonly Histogram CalculationDuration;

    [Histogram("mastery_calculation_duration_ms")]
    public static readonly Histogram MasteryCalculationDuration;

    [Gauge("overdue_reviews_current")]
    public static readonly Gauge OverdueReviews;

    [Counter("mastery_calculations_total")]
    public static readonly Counter MasteryCalculations;
}

日誌記錄

  • INFO: 正常復習記錄
  • WARN: 逾期復習、異常參數
  • ERROR: 計算失敗、資料庫錯誤

🚀 部署與實施 (基於現有ASP.NET Core)

實施步驟

  1. 資料庫遷移 (1天)

    # 新增智能複習欄位
    dotnet ef migrations add AddSpacedRepetitionFields
    dotnet ef database update
    
  2. 服務層實施 (2天)

    • 實施3個智能複習服務
    • 整合到現有DI容器
    • 配置選項設定
  3. API端點實施 (1天)

    • 在現有FlashcardsController中新增5個端點
    • 保持現有API格式一致性
    • 錯誤處理整合
  4. 測試與驗證 (1天)

    • 前後端API整合測試
    • 四情境自動適配驗證
    • 性能測試

現有架構相容性

  • 零破壞性變更: 現有詞卡功能完全不受影響
  • 資料庫擴展: 只新增欄位,不修改現有結構
  • API向後相容: 新端點不影響現有API
  • 服務層整合: 使用現有DI和配置系統

部署檢查清單

  • 資料庫遷移腳本執行
  • appsettings.json 新增SpacedRepetition配置
  • 服務註冊 AddSpacedRepetitionServices()
  • Swagger文檔更新 (新增5個端點)
  • 前端API整合測試
  • 四情境適配邏輯驗證

🧪 測試策略 (針對智能複習功能)

API整合測試

[Test]
public async Task GetNextReviewCard_ShouldReturnDueCard_WhenCardsAvailable()
{
    // Arrange
    var userId = Guid.NewGuid();
    var dueCard = CreateTestFlashcard(userId, nextReviewDate: DateTime.Now.AddDays(-1));
    await _context.Flashcards.AddAsync(dueCard);
    await _context.SaveChangesAsync();

    // Act
    var result = await _controller.GetNextReviewCard();

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var response = okResult.Value;
    Assert.NotNull(response);
}

[Test]
public async Task SelectOptimalReviewMode_ShouldReturnA1BasicModes_WhenA1Learner()
{
    // Arrange
    var flashcardId = Guid.NewGuid();
    var request = new OptimalModeRequest { UserLevel = 15, WordLevel = 30 };

    // Act
    var result = await _controller.GetOptimalReviewMode(flashcardId, request);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var data = okResult.Value as dynamic;
    var selectedMode = data?.data?.SelectedMode;
    Assert.Contains(selectedMode, new[] { "flip-memory", "vocab-choice", "vocab-listening" });
}

四情境適配測試

[TestCase(15, 25, ExpectedResult = "A1學習者")]           // A1保護
[TestCase(70, 40, ExpectedResult = "簡單詞彙")]           // 簡單詞彙
[TestCase(60, 65, ExpectedResult = "適中詞彙")]           // 適中詞彙
[TestCase(50, 85, ExpectedResult = "困難詞彙")]           // 困難詞彙
public string GetAdaptationContext_ShouldReturnCorrectContext(int userLevel, int wordLevel)
{
    return _reviewTypeSelectorService.GetAdaptationContext(userLevel, wordLevel);
}

📋 實施時程更新

實施時間: 3-4個工作日 (比原估少1天因為基於現有架構) 測試時間: 1個工作日 上線影響: 零停機時間 (純擴展功能) 技術風險: 極低 (基於成熟架構)