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

17 KiB
Raw Permalink Blame History

複習系統後端規格書

版本: 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集成 (遠期需求)

獲取複習卡片:

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"
}

記錄複習結果:

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"
}

核心算法: 間隔重複系統

複習時間計算公式 (您的需求)

// 基礎公式: 下一次複習時間 = 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);
}

具體計算範例

// 學習進度示例
成功次數 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 (上限)

成功計數更新邏輯

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)
    };
}

與前端延遲計數的配合

// 前端延遲計數系統的後端記錄
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;
}

🗃️ 數據庫設計

現有表格 (已存在)

-- 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需要時)

-- 只需要一個簡單的複習記錄表
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

// 簡化的介面 (只保留核心功能)
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計算
避免功能:
  - 詳細的統計分析
  - 複雜的會話管理
  - 用戶行為追蹤

🔐 安全性規格

身份驗證和授權

// 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; }
}

資料保護

// 個人資料保護
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)
    {
        // 完全刪除用戶所有學習數據
    }
}

性能優化規格

資料庫優化

-- 重要索引
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性能目標

// 性能指標
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開發工具

// 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"
    });
});

測試環境

// 測試資料庫
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
        // 驗證結果
    }
}

📋 部署和監控規格

部署配置

# 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}

監控指標

// 健康檢查
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");
        }
    }
}

🚨 錯誤處理規格

統一錯誤回應

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     // 服務器錯誤
}

錯誤處理中介軟體

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集成需求確認時 目標: 支持前端複習功能的後端服務