using DramaLing.Api.Data; using DramaLing.Api.Models.Configuration; using DramaLing.Api.Models.DTOs.SpacedRepetition; using DramaLing.Api.Models.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace DramaLing.Api.Services; /// /// 智能複習間隔重複服務介面 /// public interface ISpacedRepetitionService { Task ProcessReviewAsync(Guid flashcardId, ReviewRequest request); int CalculateCurrentMasteryLevel(Flashcard flashcard); Task> GetDueFlashcardsAsync(Guid userId, DateTime? date = null, int limit = 50); Task GetNextReviewCardAsync(Guid userId); } /// /// 智能複習間隔重複服務實現 (基於現有SM2Algorithm擴展) /// public class SpacedRepetitionService : ISpacedRepetitionService { private readonly DramaLingDbContext _context; private readonly ILogger _logger; private readonly SpacedRepetitionOptions _options; public SpacedRepetitionService( DramaLingDbContext context, ILogger logger, IOptions options) { _context = context; _logger = logger; _options = options.Value; } /// /// 處理復習結果並更新間隔重複算法 /// public async Task ProcessReviewAsync(Guid flashcardId, ReviewRequest request) { var flashcard = await _context.Flashcards.FindAsync(flashcardId); if (flashcard == null) throw new ArgumentException($"Flashcard {flashcardId} not found"); var actualReviewDate = DateTime.Now.Date; var overdueDays = Math.Max(0, (actualReviewDate - flashcard.NextReviewDate.Date).Days); _logger.LogInformation("Processing review for flashcard {FlashcardId}, word: {Word}, overdue: {OverdueDays} days", flashcardId, flashcard.Word, overdueDays); // 1. 基於現有SM2Algorithm計算基礎間隔 var quality = GetQualityFromRequest(request); var sm2Input = new SM2Input(quality, flashcard.EasinessFactor, flashcard.Repetitions, flashcard.IntervalDays); var sm2Result = SM2Algorithm.Calculate(sm2Input); // 2. 應用智能複習系統的增強邏輯 var enhancedInterval = ApplyEnhancedSpacedRepetitionLogic(sm2Result.IntervalDays, request, overdueDays); // 3. 計算表現係數和增長係數 var performanceFactor = GetPerformanceFactor(request); var growthFactor = _options.GrowthFactors.GetGrowthFactor(flashcard.IntervalDays); var penaltyFactor = _options.OverduePenalties.GetPenaltyFactor(overdueDays); // 4. 更新熟悉度 var newMasteryLevel = CalculateMasteryLevel( flashcard.TimesCorrect + (request.IsCorrect ? 1 : 0), flashcard.TimesReviewed + 1, enhancedInterval ); // 5. 更新資料庫 flashcard.EasinessFactor = sm2Result.EasinessFactor; flashcard.Repetitions = sm2Result.Repetitions; flashcard.IntervalDays = enhancedInterval; flashcard.NextReviewDate = actualReviewDate.AddDays(enhancedInterval); flashcard.MasteryLevel = newMasteryLevel; flashcard.TimesReviewed++; if (request.IsCorrect) flashcard.TimesCorrect++; flashcard.LastReviewedAt = DateTime.Now; flashcard.LastQuestionType = request.QuestionType; flashcard.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return new ReviewResult { NewInterval = enhancedInterval, NextReviewDate = flashcard.NextReviewDate, MasteryLevel = newMasteryLevel, CurrentMasteryLevel = CalculateCurrentMasteryLevel(flashcard), IsOverdue = overdueDays > 0, OverdueDays = overdueDays, PerformanceFactor = performanceFactor, GrowthFactor = growthFactor, PenaltyFactor = penaltyFactor }; } /// /// 計算當前熟悉度 (考慮記憶衰減) /// public int CalculateCurrentMasteryLevel(Flashcard flashcard) { if (flashcard.LastReviewedAt == null) return flashcard.MasteryLevel; var daysSinceReview = (DateTime.Now.Date - flashcard.LastReviewedAt.Value.Date).Days; if (daysSinceReview <= 0) return flashcard.MasteryLevel; // 應用記憶衰減 var decayRate = _options.MemoryDecayRate; var maxDecayDays = 30; var effectiveDays = Math.Min(daysSinceReview, maxDecayDays); var decayFactor = Math.Pow(1 - decayRate, effectiveDays); return Math.Max(0, (int)(flashcard.MasteryLevel * decayFactor)); } /// /// 取得到期詞卡列表 /// public async Task> GetDueFlashcardsAsync(Guid userId, DateTime? date = null, int limit = 50) { var queryDate = date ?? DateTime.Now.Date; var dueCards = await _context.Flashcards .Where(f => f.UserId == userId && !f.IsArchived && f.NextReviewDate.Date <= queryDate) .OrderBy(f => f.NextReviewDate) // 最逾期的優先 .ThenByDescending(f => f.MasteryLevel) // 熟悉度低的優先 .Take(limit) .ToListAsync(); // UserLevel和WordLevel欄位已移除 - 改用即時CEFR轉換 // 不需要初始化數值欄位 return dueCards; } /// /// 取得下一張需要復習的詞卡 (最高優先級) /// public async Task GetNextReviewCardAsync(Guid userId) { var dueCards = await GetDueFlashcardsAsync(userId, limit: 1); return dueCards.FirstOrDefault(); } /// /// 應用增強的間隔重複邏輯 (基於演算法規格書) /// private int ApplyEnhancedSpacedRepetitionLogic(int baseInterval, ReviewRequest request, int overdueDays) { var performanceFactor = GetPerformanceFactor(request); var growthFactor = _options.GrowthFactors.GetGrowthFactor(baseInterval); var penaltyFactor = _options.OverduePenalties.GetPenaltyFactor(overdueDays); var enhancedInterval = (int)(baseInterval * growthFactor * performanceFactor * penaltyFactor); return Math.Clamp(enhancedInterval, 1, _options.MaxInterval); } /// /// 根據題型和表現計算表現係數 /// private double GetPerformanceFactor(ReviewRequest request) { return request.QuestionType switch { "flip-memory" => GetFlipCardPerformanceFactor(request.ConfidenceLevel ?? 3), "vocab-choice" or "sentence-fill" or "sentence-reorder" => request.IsCorrect ? 1.1 : 0.6, "vocab-listening" or "sentence-listening" => request.IsCorrect ? 1.2 : 0.5, // 聽力題難度加權 "sentence-speaking" => 1.0, // 口說題重在參與 _ => 0.9 }; } /// /// 翻卡題信心等級映射 /// private double GetFlipCardPerformanceFactor(int confidenceLevel) { return confidenceLevel switch { 1 => 0.5, // 很不確定 2 => 0.7, // 不確定 3 => 0.9, // 一般 4 => 1.1, // 確定 5 => 1.4, // 很確定 _ => 0.9 }; } /// /// 從請求轉換為SM2Algorithm需要的品質分數 /// private int GetQualityFromRequest(ReviewRequest request) { if (request.QuestionType == "flip-memory") { return request.ConfidenceLevel ?? 3; } return request.IsCorrect ? 4 : 2; // 客觀題簡化映射 } /// /// 計算基礎熟悉度 (基於現有算法調整) /// private int CalculateMasteryLevel(int timesCorrect, int totalReviews, int currentInterval) { var successRate = totalReviews > 0 ? (double)timesCorrect / totalReviews : 0; var baseScore = Math.Min(timesCorrect * 8, 60); // 答對次數貢獻 (最多60%) var intervalBonus = Math.Min(currentInterval / 365.0 * 25, 25); // 間隔貢獻 (最多25%) var accuracyBonus = successRate * 15; // 正確率貢獻 (最多15%) return Math.Min(100, (int)Math.Round(baseScore + intervalBonus + accuracyBonus)); } }