using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Services; using Microsoft.EntityFrameworkCore; namespace DramaLing.Api.Services; /// /// 學習會話服務介面 /// public interface IStudySessionService { Task StartSessionAsync(Guid userId); Task GetCurrentTestAsync(Guid sessionId); Task SubmitTestAsync(Guid sessionId, SubmitTestRequestDto request); Task GetNextTestAsync(Guid sessionId); Task GetProgressAsync(Guid sessionId); Task CompleteSessionAsync(Guid sessionId); } /// /// 學習會話服務實現 /// public class StudySessionService : IStudySessionService { private readonly DramaLingDbContext _context; private readonly ILogger _logger; private readonly IReviewModeSelector _reviewModeSelector; public StudySessionService( DramaLingDbContext context, ILogger logger, IReviewModeSelector reviewModeSelector) { _context = context; _logger = logger; _reviewModeSelector = reviewModeSelector; } /// /// 開始新的學習會話 /// public async Task StartSessionAsync(Guid userId) { _logger.LogInformation("Starting new study session for user {UserId}", userId); // 獲取到期詞卡 var dueCards = await GetDueCardsAsync(userId); if (!dueCards.Any()) { throw new InvalidOperationException("No due cards available for study"); } // 獲取用戶CEFR等級 var user = await _context.Users.FindAsync(userId); var userCEFRLevel = user?.EnglishLevel ?? "A2"; // 創建學習會話 var session = new StudySession { Id = Guid.NewGuid(), UserId = userId, SessionType = "mixed", // 混合模式 StartedAt = DateTime.UtcNow, Status = SessionStatus.Active, TotalCards = dueCards.Count, CurrentCardIndex = 0 }; _context.StudySessions.Add(session); // 為每張詞卡創建學習進度記錄 int totalTests = 0; for (int i = 0; i < dueCards.Count; i++) { var card = dueCards[i]; var wordCEFRLevel = card.DifficultyLevel ?? "A2"; var plannedTests = _reviewModeSelector.GetPlannedTests(userCEFRLevel, wordCEFRLevel); var studyCard = new StudyCard { Id = Guid.NewGuid(), StudySessionId = session.Id, FlashcardId = card.Id, Word = card.Word, PlannedTests = plannedTests, Order = i, StartedAt = DateTime.UtcNow }; _context.StudyCards.Add(studyCard); totalTests += plannedTests.Count; } session.TotalTests = totalTests; // 設置第一個測驗 if (session.StudyCards.Any()) { var firstCard = session.StudyCards.OrderBy(c => c.Order).First(); session.CurrentTestType = firstCard.PlannedTests.First(); } await _context.SaveChangesAsync(); _logger.LogInformation("Study session created: {SessionId}, Cards: {CardCount}, Tests: {TestCount}", session.Id, session.TotalCards, session.TotalTests); return session; } /// /// 獲取當前測驗 /// public async Task GetCurrentTestAsync(Guid sessionId) { var session = await GetSessionWithDetailsAsync(sessionId); if (session == null || session.Status != SessionStatus.Active) { throw new InvalidOperationException("Session not found or not active"); } var currentCard = session.StudyCards.OrderBy(c => c.Order).Skip(session.CurrentCardIndex).FirstOrDefault(); if (currentCard == null) { throw new InvalidOperationException("No current card found"); } var flashcard = await _context.Flashcards .Include(f => f.CardSet) .FirstOrDefaultAsync(f => f.Id == currentCard.FlashcardId); return new CurrentTestDto { SessionId = sessionId, TestType = session.CurrentTestType ?? "flip-memory", Card = new CardDto { Id = flashcard!.Id, Word = flashcard.Word, Translation = flashcard.Translation, Definition = flashcard.Definition, Example = flashcard.Example, ExampleTranslation = flashcard.ExampleTranslation, Pronunciation = flashcard.Pronunciation, DifficultyLevel = flashcard.DifficultyLevel }, Progress = new ProgressSummaryDto { CurrentCardIndex = session.CurrentCardIndex, TotalCards = session.TotalCards, CompletedTests = session.CompletedTests, TotalTests = session.TotalTests, CompletedCards = session.CompletedCards } }; } /// /// 提交測驗結果 /// public async Task SubmitTestAsync(Guid sessionId, SubmitTestRequestDto request) { var session = await GetSessionWithDetailsAsync(sessionId); if (session == null || session.Status != SessionStatus.Active) { throw new InvalidOperationException("Session not found or not active"); } var currentCard = session.StudyCards.OrderBy(c => c.Order).Skip(session.CurrentCardIndex).FirstOrDefault(); if (currentCard == null) { throw new InvalidOperationException("No current card found"); } // 記錄測驗結果 var testResult = new TestResult { Id = Guid.NewGuid(), StudyCardId = currentCard.Id, TestType = request.TestType, IsCorrect = request.IsCorrect, UserAnswer = request.UserAnswer, ConfidenceLevel = request.ConfidenceLevel, ResponseTimeMs = request.ResponseTimeMs, CompletedAt = DateTime.UtcNow }; _context.TestResults.Add(testResult); // 更新會話進度 session.CompletedTests++; // 檢查當前詞卡是否完成所有測驗 var completedTestsForCard = await _context.TestResults .Where(tr => tr.StudyCardId == currentCard.Id) .CountAsync() + 1; // +1 因為當前測驗還未保存 if (completedTestsForCard >= currentCard.PlannedTestsCount) { // 詞卡完成,觸發SM2算法更新 currentCard.IsCompleted = true; currentCard.CompletedAt = DateTime.UtcNow; session.CompletedCards++; await UpdateFlashcardWithSM2Async(currentCard, testResult); } await _context.SaveChangesAsync(); return new SubmitTestResponseDto { Success = true, IsCardCompleted = currentCard.IsCompleted, Progress = new ProgressSummaryDto { CurrentCardIndex = session.CurrentCardIndex, TotalCards = session.TotalCards, CompletedTests = session.CompletedTests, TotalTests = session.TotalTests, CompletedCards = session.CompletedCards } }; } /// /// 獲取下一個測驗 /// public async Task GetNextTestAsync(Guid sessionId) { var session = await GetSessionWithDetailsAsync(sessionId); if (session == null || session.Status != SessionStatus.Active) { throw new InvalidOperationException("Session not found or not active"); } var currentCard = session.StudyCards.OrderBy(c => c.Order).Skip(session.CurrentCardIndex).FirstOrDefault(); if (currentCard == null) { return new NextTestDto { HasNextTest = false, Message = "All cards completed" }; } // 檢查當前詞卡是否還有未完成的測驗 var completedTestTypes = await _context.TestResults .Where(tr => tr.StudyCardId == currentCard.Id) .Select(tr => tr.TestType) .ToListAsync(); var nextTestType = currentCard.PlannedTests.FirstOrDefault(t => !completedTestTypes.Contains(t)); if (nextTestType != null) { // 當前詞卡還有測驗 session.CurrentTestType = nextTestType; await _context.SaveChangesAsync(); return new NextTestDto { HasNextTest = true, TestType = nextTestType, SameCard = true, Message = $"Next test: {nextTestType}" }; } else { // 當前詞卡完成,移到下一張詞卡 session.CurrentCardIndex++; if (session.CurrentCardIndex < session.TotalCards) { var nextCard = session.StudyCards.OrderBy(c => c.Order).Skip(session.CurrentCardIndex).FirstOrDefault(); session.CurrentTestType = nextCard?.PlannedTests.FirstOrDefault(); await _context.SaveChangesAsync(); return new NextTestDto { HasNextTest = true, TestType = session.CurrentTestType!, SameCard = false, Message = "Moving to next card" }; } else { // 所有詞卡完成 session.Status = SessionStatus.Completed; session.EndedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return new NextTestDto { HasNextTest = false, Message = "Session completed" }; } } } /// /// 獲取詳細進度 /// public async Task GetProgressAsync(Guid sessionId) { var session = await GetSessionWithDetailsAsync(sessionId); if (session == null) { throw new InvalidOperationException("Session not found"); } var cardProgress = session.StudyCards.Select(card => new CardProgressDto { CardId = card.FlashcardId, Word = card.Word, PlannedTests = card.PlannedTests, CompletedTestsCount = card.TestResults.Count, IsCompleted = card.IsCompleted, Tests = card.TestResults.Select(tr => new TestProgressDto { TestType = tr.TestType, IsCorrect = tr.IsCorrect, CompletedAt = tr.CompletedAt }).ToList() }).ToList(); return new ProgressDto { SessionId = sessionId, Status = session.Status.ToString(), CurrentCardIndex = session.CurrentCardIndex, TotalCards = session.TotalCards, CompletedTests = session.CompletedTests, TotalTests = session.TotalTests, CompletedCards = session.CompletedCards, Cards = cardProgress }; } /// /// 完成學習會話 /// public async Task CompleteSessionAsync(Guid sessionId) { var session = await GetSessionWithDetailsAsync(sessionId); if (session == null) { throw new InvalidOperationException("Session not found"); } session.Status = SessionStatus.Completed; session.EndedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Study session completed: {SessionId}", sessionId); return session; } // Helper Methods private async Task GetSessionWithDetailsAsync(Guid sessionId) { return await _context.StudySessions .Include(s => s.StudyCards) .ThenInclude(sc => sc.TestResults) .Include(s => s.StudyCards) .ThenInclude(sc => sc.Flashcard) .FirstOrDefaultAsync(s => s.Id == sessionId); } private async Task> GetDueCardsAsync(Guid userId, int limit = 50) { var today = DateTime.Today; return await _context.Flashcards .Where(f => f.UserId == userId && (f.NextReviewDate <= today || f.Repetitions == 0)) .OrderBy(f => f.NextReviewDate) .Take(limit) .ToListAsync(); } private async Task UpdateFlashcardWithSM2Async(StudyCard studyCard, TestResult latestResult) { var flashcard = await _context.Flashcards.FindAsync(studyCard.FlashcardId); if (flashcard == null) return; // 計算詞卡的綜合表現 var allResults = await _context.TestResults .Where(tr => tr.StudyCardId == studyCard.Id) .ToListAsync(); var correctCount = allResults.Count(r => r.IsCorrect); var totalTests = allResults.Count; var accuracy = totalTests > 0 ? (double)correctCount / totalTests : 0; // 使用現有的SM2Algorithm var quality = accuracy >= 0.8 ? 5 : accuracy >= 0.6 ? 4 : accuracy >= 0.4 ? 3 : 2; var sm2Input = new SM2Input(quality, flashcard.EasinessFactor, flashcard.Repetitions, flashcard.IntervalDays); var sm2Result = SM2Algorithm.Calculate(sm2Input); // 更新詞卡 flashcard.EasinessFactor = sm2Result.EasinessFactor; flashcard.Repetitions = sm2Result.Repetitions; flashcard.IntervalDays = sm2Result.IntervalDays; flashcard.NextReviewDate = sm2Result.NextReviewDate; flashcard.MasteryLevel = SM2Algorithm.CalculateMastery(sm2Result.Repetitions, sm2Result.EasinessFactor); flashcard.TimesReviewed++; if (accuracy >= 0.7) flashcard.TimesCorrect++; flashcard.LastReviewedAt = DateTime.UtcNow; _logger.LogInformation("Updated flashcard {Word} with SM2: Mastery={Mastery}, NextReview={NextReview}", flashcard.Word, flashcard.MasteryLevel, sm2Result.NextReviewDate); } } // DTOs public class CurrentTestDto { public Guid SessionId { get; set; } public string TestType { get; set; } = string.Empty; public CardDto Card { get; set; } = new(); public ProgressSummaryDto Progress { get; set; } = new(); } public class SubmitTestRequestDto { public string TestType { get; set; } = string.Empty; public bool IsCorrect { get; set; } public string? UserAnswer { get; set; } public int? ConfidenceLevel { get; set; } public int ResponseTimeMs { get; set; } } public class SubmitTestResponseDto { public bool Success { get; set; } public bool IsCardCompleted { get; set; } public ProgressSummaryDto Progress { get; set; } = new(); public string Message { get; set; } = string.Empty; } public class NextTestDto { public bool HasNextTest { get; set; } public string? TestType { get; set; } public bool SameCard { get; set; } public string Message { get; set; } = string.Empty; } public class ProgressDto { public Guid SessionId { get; set; } public string Status { get; set; } = string.Empty; public int CurrentCardIndex { get; set; } public int TotalCards { get; set; } public int CompletedTests { get; set; } public int TotalTests { get; set; } public int CompletedCards { get; set; } public List Cards { get; set; } = new(); } public class CardProgressDto { public Guid CardId { get; set; } public string Word { get; set; } = string.Empty; public List PlannedTests { get; set; } = new(); public int CompletedTestsCount { get; set; } public bool IsCompleted { get; set; } public List Tests { get; set; } = new(); } public class TestProgressDto { public string TestType { get; set; } = string.Empty; public bool IsCorrect { get; set; } public DateTime CompletedAt { get; set; } } public class ProgressSummaryDto { public int CurrentCardIndex { get; set; } public int TotalCards { get; set; } public int CompletedTests { get; set; } public int TotalTests { get; set; } public int CompletedCards { get; set; } } public class CardDto { public Guid Id { get; set; } public string Word { get; set; } = string.Empty; public string Translation { get; set; } = string.Empty; public string Definition { get; set; } = string.Empty; public string Example { get; set; } = string.Empty; public string ExampleTranslation { get; set; } = string.Empty; public string Pronunciation { get; set; } = string.Empty; public string DifficultyLevel { get; set; } = string.Empty; }