dramaling-vocab-learning/backend/DramaLing.Api/Services/SpacedRepetitionService.cs

227 lines
8.4 KiB
C#

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;
/// <summary>
/// 智能複習間隔重複服務介面
/// </summary>
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);
}
/// <summary>
/// 智能複習間隔重複服務實現 (基於現有SM2Algorithm擴展)
/// </summary>
public class SpacedRepetitionService : ISpacedRepetitionService
{
private readonly DramaLingDbContext _context;
private readonly ILogger<SpacedRepetitionService> _logger;
private readonly SpacedRepetitionOptions _options;
public SpacedRepetitionService(
DramaLingDbContext context,
ILogger<SpacedRepetitionService> logger,
IOptions<SpacedRepetitionOptions> options)
{
_context = context;
_logger = logger;
_options = options.Value;
}
/// <summary>
/// 處理復習結果並更新間隔重複算法
/// </summary>
public async Task<ReviewResult> 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
};
}
/// <summary>
/// 計算當前熟悉度 (考慮記憶衰減)
/// </summary>
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));
}
/// <summary>
/// 取得到期詞卡列表
/// </summary>
public async Task<List<Flashcard>> 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;
}
/// <summary>
/// 取得下一張需要復習的詞卡 (最高優先級)
/// </summary>
public async Task<Flashcard?> GetNextReviewCardAsync(Guid userId)
{
var dueCards = await GetDueFlashcardsAsync(userId, limit: 1);
return dueCards.FirstOrDefault();
}
/// <summary>
/// 應用增強的間隔重複邏輯 (基於演算法規格書)
/// </summary>
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);
}
/// <summary>
/// 根據題型和表現計算表現係數
/// </summary>
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
};
}
/// <summary>
/// 翻卡題信心等級映射
/// </summary>
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
};
}
/// <summary>
/// 從請求轉換為SM2Algorithm需要的品質分數
/// </summary>
private int GetQualityFromRequest(ReviewRequest request)
{
if (request.QuestionType == "flip-memory")
{
return request.ConfidenceLevel ?? 3;
}
return request.IsCorrect ? 4 : 2; // 客觀題簡化映射
}
/// <summary>
/// 計算基礎熟悉度 (基於現有算法調整)
/// </summary>
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));
}
}