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