237 lines
8.7 KiB
C#
237 lines
8.7 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();
|
|
|
|
// 初始化WordLevel (如果是舊資料)
|
|
foreach (var card in dueCards.Where(c => c.WordLevel == 0))
|
|
{
|
|
card.WordLevel = CEFRMappingService.GetWordLevel(card.DifficultyLevel);
|
|
if (card.UserLevel == 0)
|
|
card.UserLevel = _options.DefaultUserLevel;
|
|
}
|
|
|
|
if (dueCards.Any(c => c.WordLevel != 0 || c.UserLevel != 0))
|
|
{
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
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));
|
|
}
|
|
} |