diff --git a/backend/DramaLing.Api/Controllers/CardSetsController.cs b/backend/DramaLing.Api/Controllers/CardSetsController.cs deleted file mode 100644 index 5be8260..0000000 --- a/backend/DramaLing.Api/Controllers/CardSetsController.cs +++ /dev/null @@ -1,297 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; - -namespace DramaLing.Api.Controllers; - -[ApiController] -[Route("api/[controller]")] -[Authorize] -public class CardSetsController : ControllerBase -{ - private readonly DramaLingDbContext _context; - - public CardSetsController(DramaLingDbContext context) - { - _context = context; - } - - private Guid GetUserId() - { - var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? - User.FindFirst("sub")?.Value; - - if (Guid.TryParse(userIdString, out var userId)) - return userId; - - throw new UnauthorizedAccessException("Invalid user ID"); - } - - private async Task EnsureDefaultCardSetAsync(Guid userId) - { - // 檢查用戶是否已有預設卡組 - var hasDefaultCardSet = await _context.CardSets - .AnyAsync(cs => cs.UserId == userId && cs.IsDefault); - - if (!hasDefaultCardSet) - { - // 創建預設「未分類」卡組 - var defaultCardSet = new CardSet - { - Id = Guid.NewGuid(), - UserId = userId, - Name = "未分類", - Description = "系統預設卡組,用於存放尚未分類的詞卡", - Color = "bg-slate-700", - IsDefault = true - }; - - _context.CardSets.Add(defaultCardSet); - await _context.SaveChangesAsync(); - } - } - - [HttpGet] - public async Task GetCardSets() - { - try - { - var userId = GetUserId(); - - // 確保用戶有預設卡組 - await EnsureDefaultCardSetAsync(userId); - - var cardSets = await _context.CardSets - .Where(cs => cs.UserId == userId) - .OrderBy(cs => cs.IsDefault ? 0 : 1) // 預設卡組排在最前面 - .ThenByDescending(cs => cs.CreatedAt) - .Select(cs => new - { - cs.Id, - cs.Name, - cs.Description, - cs.Color, - cs.CardCount, - cs.CreatedAt, - cs.UpdatedAt, - cs.IsDefault, - // 計算進度 (簡化版) - Progress = cs.CardCount > 0 ? - _context.Flashcards - .Where(f => f.CardSetId == cs.Id) - .Average(f => (double?)f.MasteryLevel) ?? 0 : 0, - LastStudied = cs.UpdatedAt, - Tags = new string[] { } // Phase 1 簡化 - }) - .ToListAsync(); - - return Ok(new - { - Success = true, - Data = new { Sets = cardSets } - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to fetch card sets", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPost] - public async Task CreateCardSet([FromBody] CreateCardSetRequest request) - { - try - { - var userId = GetUserId(); - - if (string.IsNullOrWhiteSpace(request.Name)) - return BadRequest(new { Success = false, Error = "Name is required" }); - - if (request.Name.Length > 255) - return BadRequest(new { Success = false, Error = "Name must be less than 255 characters" }); - - var cardSet = new CardSet - { - Id = Guid.NewGuid(), - UserId = userId, - Name = request.Name.Trim(), - Description = request.Description?.Trim(), - Color = request.Color ?? "bg-blue-500" - }; - - _context.CardSets.Add(cardSet); - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Data = cardSet, - Message = "Card set created successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to create card set", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPut("{id}")] - public async Task UpdateCardSet(Guid id, [FromBody] UpdateCardSetRequest request) - { - try - { - var userId = GetUserId(); - - var cardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.Id == id && cs.UserId == userId); - - if (cardSet == null) - return NotFound(new { Success = false, Error = "Card set not found" }); - - if (!string.IsNullOrEmpty(request.Name)) - cardSet.Name = request.Name.Trim(); - if (request.Description != null) - cardSet.Description = request.Description?.Trim(); - if (!string.IsNullOrEmpty(request.Color)) - cardSet.Color = request.Color; - - cardSet.UpdatedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Data = cardSet, - Message = "Card set updated successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to update card set", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpDelete("{id}")] - public async Task DeleteCardSet(Guid id) - { - try - { - var userId = GetUserId(); - - var cardSet = await _context.CardSets - .Include(cs => cs.Flashcards) - .FirstOrDefaultAsync(cs => cs.Id == id && cs.UserId == userId); - - if (cardSet == null) - return NotFound(new { Success = false, Error = "Card set not found" }); - - // 防止刪除預設卡組 - if (cardSet.IsDefault) - return BadRequest(new { Success = false, Error = "Cannot delete default card set" }); - - _context.CardSets.Remove(cardSet); - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Message = "Card set deleted successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to delete card set", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPost("ensure-default")] - public async Task EnsureDefaultCardSet() - { - try - { - var userId = GetUserId(); - await EnsureDefaultCardSetAsync(userId); - - // 返回預設卡組 - var defaultCardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault); - - if (defaultCardSet == null) - return StatusCode(500, new { Success = false, Error = "Failed to create default card set" }); - - return Ok(new - { - Success = true, - Data = defaultCardSet, - Message = "Default card set ensured" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to ensure default card set", - Timestamp = DateTime.UtcNow - }); - } - } -} - -// Request DTOs -public class CreateCardSetRequest -{ - public string Name { get; set; } = string.Empty; - public string? Description { get; set; } - public string? Color { get; set; } -} - -public class UpdateCardSetRequest -{ - public string? Name { get; set; } - public string? Description { get; set; } - public string? Color { get; set; } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs deleted file mode 100644 index 1626f83..0000000 --- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs +++ /dev/null @@ -1,462 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; -using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; - -namespace DramaLing.Api.Controllers; - -[ApiController] -[Route("api/[controller]")] -[Authorize] -public class FlashcardsController : ControllerBase -{ - private readonly DramaLingDbContext _context; - - public FlashcardsController(DramaLingDbContext context) - { - _context = context; - } - - private Guid GetUserId() - { - var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? - User.FindFirst("sub")?.Value; - - if (Guid.TryParse(userIdString, out var userId)) - return userId; - - throw new UnauthorizedAccessException("Invalid user ID"); - } - - private async Task GetOrCreateDefaultCardSetAsync(Guid userId) - { - // 嘗試找到預設卡組 - var defaultCardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault); - - if (defaultCardSet != null) - return defaultCardSet.Id; - - // 如果沒有預設卡組,創建一個 - var newDefaultCardSet = new CardSet - { - Id = Guid.NewGuid(), - UserId = userId, - Name = "未分類", - Description = "系統預設卡組,用於存放尚未分類的詞卡", - Color = "bg-slate-700", - IsDefault = true - }; - - _context.CardSets.Add(newDefaultCardSet); - await _context.SaveChangesAsync(); - return newDefaultCardSet.Id; - } - - [HttpGet] - public async Task GetFlashcards( - [FromQuery] Guid? setId, - [FromQuery] string? search, - [FromQuery] bool favoritesOnly = false, - [FromQuery] int limit = 50, - [FromQuery] int offset = 0) - { - try - { - var userId = GetUserId(); - - var query = _context.Flashcards - .Include(f => f.CardSet) - .Where(f => f.UserId == userId); - - if (setId.HasValue) - query = query.Where(f => f.CardSetId == setId); - - if (!string.IsNullOrEmpty(search)) - query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search)); - - if (favoritesOnly) - query = query.Where(f => f.IsFavorite); - - var total = await query.CountAsync(); - var flashcards = await query - .OrderByDescending(f => f.CreatedAt) - .Skip(offset) - .Take(Math.Min(limit, 100)) - .Select(f => new - { - f.Id, - f.Word, - f.Translation, - f.Definition, - f.PartOfSpeech, - f.Pronunciation, - f.Example, - f.ExampleTranslation, - f.MasteryLevel, - f.TimesReviewed, - f.IsFavorite, - f.NextReviewDate, - f.CreatedAt, - CardSet = new - { - f.CardSet.Name, - f.CardSet.Color - } - }) - .ToListAsync(); - - return Ok(new - { - Success = true, - Data = new - { - Flashcards = flashcards, - Total = total, - HasMore = offset + limit < total - } - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to fetch flashcards", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPost] - public async Task CreateFlashcard([FromBody] CreateFlashcardRequest request) - { - try - { - var userId = GetUserId(); - - // 確定要使用的卡組ID - Guid cardSetId; - if (request.CardSetId.HasValue) - { - // 如果指定了卡組,驗證是否屬於用戶 - var cardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId); - - if (cardSet == null) - return NotFound(new { Success = false, Error = "Card set not found" }); - - cardSetId = request.CardSetId.Value; - } - else - { - // 如果沒有指定卡組,使用或創建預設卡組 - cardSetId = await GetOrCreateDefaultCardSetAsync(userId); - } - - var flashcard = new Flashcard - { - Id = Guid.NewGuid(), - UserId = userId, - CardSetId = cardSetId, - Word = request.Word.Trim(), - Translation = request.Translation.Trim(), - Definition = request.Definition.Trim(), - PartOfSpeech = request.PartOfSpeech?.Trim(), - Pronunciation = request.Pronunciation?.Trim(), - Example = request.Example?.Trim(), - ExampleTranslation = request.ExampleTranslation?.Trim() - }; - - _context.Flashcards.Add(flashcard); - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Data = flashcard, - Message = "Flashcard created successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to create flashcard", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpGet("{id}")] - public async Task GetFlashcard(Guid id) - { - try - { - var userId = GetUserId(); - - var flashcard = await _context.Flashcards - .Include(f => f.CardSet) - .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); - - if (flashcard == null) - return NotFound(new { Success = false, Error = "Flashcard not found" }); - - return Ok(new { Success = true, Data = flashcard }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to fetch flashcard", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPut("{id}")] - public async Task UpdateFlashcard(Guid id, [FromBody] UpdateFlashcardRequest request) - { - try - { - var userId = GetUserId(); - - var flashcard = await _context.Flashcards - .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); - - if (flashcard == null) - return NotFound(new { Success = false, Error = "Flashcard not found" }); - - // 更新欄位 - if (!string.IsNullOrEmpty(request.Word)) - flashcard.Word = request.Word.Trim(); - if (!string.IsNullOrEmpty(request.Translation)) - flashcard.Translation = request.Translation.Trim(); - if (!string.IsNullOrEmpty(request.Definition)) - flashcard.Definition = request.Definition.Trim(); - if (request.PartOfSpeech != null) - flashcard.PartOfSpeech = request.PartOfSpeech?.Trim(); - if (request.Pronunciation != null) - flashcard.Pronunciation = request.Pronunciation?.Trim(); - if (request.Example != null) - flashcard.Example = request.Example?.Trim(); - if (request.ExampleTranslation != null) - flashcard.ExampleTranslation = request.ExampleTranslation?.Trim(); - if (request.IsFavorite.HasValue) - flashcard.IsFavorite = request.IsFavorite.Value; - - flashcard.UpdatedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Data = flashcard, - Message = "Flashcard updated successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to update flashcard", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpDelete("{id}")] - public async Task DeleteFlashcard(Guid id) - { - try - { - var userId = GetUserId(); - - var flashcard = await _context.Flashcards - .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); - - if (flashcard == null) - return NotFound(new { Success = false, Error = "Flashcard not found" }); - - _context.Flashcards.Remove(flashcard); - await _context.SaveChangesAsync(); - - return Ok(new - { - Success = true, - Message = "Flashcard deleted successfully" - }); - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to delete flashcard", - Timestamp = DateTime.UtcNow - }); - } - } - - [HttpPost("batch")] - public async Task BatchCreateFlashcards([FromBody] BatchCreateFlashcardsRequest request) - { - try - { - var userId = GetUserId(); - - if (request.Cards == null || !request.Cards.Any()) - return BadRequest(new { Success = false, Error = "No cards provided" }); - - if (request.Cards.Count > 50) - return BadRequest(new { Success = false, Error = "Maximum 50 cards per batch" }); - - // 確定要使用的卡組ID - Guid cardSetId; - if (request.CardSetId.HasValue) - { - var cardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId); - - if (cardSet == null) - return NotFound(new { Success = false, Error = "Card set not found" }); - - cardSetId = request.CardSetId.Value; - } - else - { - cardSetId = await GetOrCreateDefaultCardSetAsync(userId); - } - - var savedCards = new List(); - var errors = new List(); - - using var transaction = await _context.Database.BeginTransactionAsync(); - - try - { - foreach (var cardRequest in request.Cards) - { - try - { - var flashcard = new Flashcard - { - Id = Guid.NewGuid(), - UserId = userId, - CardSetId = cardSetId, - Word = cardRequest.Word.Trim(), - Translation = cardRequest.Translation.Trim(), - Definition = cardRequest.Definition.Trim(), - PartOfSpeech = cardRequest.PartOfSpeech?.Trim(), - Pronunciation = cardRequest.Pronunciation?.Trim(), - Example = cardRequest.Example?.Trim(), - ExampleTranslation = cardRequest.ExampleTranslation?.Trim() - }; - - _context.Flashcards.Add(flashcard); - savedCards.Add(new - { - Id = flashcard.Id, - Word = flashcard.Word, - Translation = flashcard.Translation - }); - } - catch (Exception ex) - { - errors.Add($"Failed to save card '{cardRequest.Word}': {ex.Message}"); - } - } - - await _context.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(new - { - Success = true, - Data = new - { - SavedCards = savedCards, - SavedCount = savedCards.Count, - ErrorCount = errors.Count, - Errors = errors - }, - Message = $"Successfully saved {savedCards.Count} flashcards" - }); - } - catch (Exception ex) - { - await transaction.RollbackAsync(); - throw; - } - } - catch (UnauthorizedAccessException) - { - return Unauthorized(new { Success = false, Error = "Unauthorized" }); - } - catch (Exception ex) - { - return StatusCode(500, new - { - Success = false, - Error = "Failed to create flashcards", - Timestamp = DateTime.UtcNow - }); - } - } -} - -// DTOs -public class CreateFlashcardRequest -{ - public Guid? CardSetId { 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? PartOfSpeech { get; set; } - public string? Pronunciation { get; set; } - public string? Example { get; set; } - public string? ExampleTranslation { get; set; } -} - -public class UpdateFlashcardRequest -{ - public string? Word { get; set; } - public string? Translation { get; set; } - public string? Definition { get; set; } - public string? PartOfSpeech { get; set; } - public string? Pronunciation { get; set; } - public string? Example { get; set; } - public string? ExampleTranslation { get; set; } - public bool? IsFavorite { get; set; } -} - -public class BatchCreateFlashcardsRequest -{ - public Guid? CardSetId { get; set; } - public List Cards { get; set; } = new(); -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/SimplifiedFlashcardsController.cs b/backend/DramaLing.Api/Controllers/SimplifiedFlashcardsController.cs index c1743bd..a243694 100644 --- a/backend/DramaLing.Api/Controllers/SimplifiedFlashcardsController.cs +++ b/backend/DramaLing.Api/Controllers/SimplifiedFlashcardsController.cs @@ -204,6 +204,100 @@ public class SimplifiedFlashcardsController : ControllerBase } } + [HttpGet("{id}")] + public async Task GetFlashcard(Guid id) + { + try + { + var userId = GetUserId(); + + var flashcard = await _context.Flashcards + .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); + + if (flashcard == null) + { + return NotFound(new { Success = false, Error = "Flashcard not found" }); + } + + return Ok(new + { + Success = true, + Data = new + { + flashcard.Id, + flashcard.Word, + flashcard.Translation, + flashcard.Definition, + flashcard.PartOfSpeech, + flashcard.Pronunciation, + flashcard.Example, + flashcard.ExampleTranslation, + flashcard.MasteryLevel, + flashcard.TimesReviewed, + flashcard.IsFavorite, + flashcard.NextReviewDate, + flashcard.DifficultyLevel, + flashcard.CreatedAt, + flashcard.UpdatedAt + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting flashcard {FlashcardId}", id); + return StatusCode(500, new { Success = false, Error = "Failed to get flashcard" }); + } + } + + [HttpPut("{id}")] + public async Task UpdateFlashcard(Guid id, [FromBody] CreateSimpleFlashcardRequest request) + { + try + { + var userId = GetUserId(); + + var flashcard = await _context.Flashcards + .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); + + if (flashcard == null) + { + return NotFound(new { Success = false, Error = "Flashcard not found" }); + } + + // 更新詞卡資訊 + flashcard.Word = request.Word; + flashcard.Translation = request.Translation; + flashcard.Definition = request.Definition ?? ""; + flashcard.PartOfSpeech = request.PartOfSpeech; + flashcard.Pronunciation = request.Pronunciation; + flashcard.Example = request.Example; + flashcard.ExampleTranslation = request.ExampleTranslation; + flashcard.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return Ok(new + { + Success = true, + Data = new + { + flashcard.Id, + flashcard.Word, + flashcard.Translation, + flashcard.Definition, + flashcard.CreatedAt, + flashcard.UpdatedAt + }, + Message = "詞卡更新成功" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating flashcard {FlashcardId}", id); + return StatusCode(500, new { Success = false, Error = "Failed to update flashcard" }); + } + } + [HttpDelete("{id}")] public async Task DeleteFlashcard(Guid id) { diff --git a/backend/DramaLing.Api/Repositories/FlashcardRepository.cs b/backend/DramaLing.Api/Repositories/FlashcardRepository.cs deleted file mode 100644 index 48b0fc9..0000000 --- a/backend/DramaLing.Api/Repositories/FlashcardRepository.cs +++ /dev/null @@ -1,338 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; - -namespace DramaLing.Api.Repositories; - -/// -/// Flashcard Repository 實作,包含所有與詞卡相關的數據存取邏輯 -/// -public class FlashcardRepository : BaseRepository, IFlashcardRepository -{ - public FlashcardRepository(DramaLingDbContext context, ILogger logger) - : base(context, logger) - { - } - - #region 用戶相關查詢 - - public async Task> GetFlashcardsByUserIdAsync(Guid userId) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId && !f.IsArchived) - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting flashcards for user: {UserId}", userId); - throw; - } - } - - public async Task> GetFlashcardsByCardSetIdAsync(Guid cardSetId) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.CardSetId == cardSetId && !f.IsArchived) - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting flashcards for card set: {CardSetId}", cardSetId); - throw; - } - } - - #endregion - - #region 學習相關查詢 - - public async Task> GetDueFlashcardsAsync(Guid userId, DateTime dueDate) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId - && !f.IsArchived - && f.NextReviewDate <= dueDate - && f.MasteryLevel < 5) // 未完全掌握的卡片 - .OrderBy(f => f.NextReviewDate) - .ThenBy(f => f.EasinessFactor) // 難度較高的優先 - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting due flashcards for user: {UserId}, date: {DueDate}", userId, dueDate); - throw; - } - } - - public async Task> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId - && !f.IsArchived - && f.DifficultyLevel == difficultyLevel) - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting flashcards by difficulty for user: {UserId}, level: {DifficultyLevel}", - userId, difficultyLevel); - throw; - } - } - - public async Task> GetRecentlyAddedAsync(Guid userId, int count) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId && !f.IsArchived) - .OrderByDescending(f => f.CreatedAt) - .Take(count) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recently added flashcards for user: {UserId}", userId); - throw; - } - } - - public async Task> GetMostReviewedAsync(Guid userId, int count) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId && !f.IsArchived) - .OrderByDescending(f => f.TimesReviewed) - .Take(count) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting most reviewed flashcards for user: {UserId}", userId); - throw; - } - } - - #endregion - - #region 統計查詢 - - public async Task GetTotalFlashcardsCountAsync(Guid userId) - { - try - { - return await _dbSet - .Where(f => f.UserId == userId && !f.IsArchived) - .CountAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting total flashcards count for user: {UserId}", userId); - throw; - } - } - - public async Task GetMasteredFlashcardsCountAsync(Guid userId) - { - try - { - return await _dbSet - .Where(f => f.UserId == userId && !f.IsArchived && f.MasteryLevel >= 5) - .CountAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting mastered flashcards count for user: {UserId}", userId); - throw; - } - } - - public async Task> GetFlashcardsByDifficultyStatsAsync(Guid userId) - { - try - { - return await _dbSet - .Where(f => f.UserId == userId && !f.IsArchived) - .GroupBy(f => f.DifficultyLevel) - .Select(g => new { Level = g.Key, Count = g.Count() }) - .ToDictionaryAsync(x => x.Level ?? "Unknown", x => x.Count); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting flashcards difficulty stats for user: {UserId}", userId); - throw; - } - } - - #endregion - - #region 搜尋功能 - - public async Task> SearchFlashcardsAsync(Guid userId, string searchTerm) - { - try - { - var term = searchTerm.ToLower(); - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId - && !f.IsArchived - && (f.Word.ToLower().Contains(term) - || f.Translation.ToLower().Contains(term) - || (f.Definition != null && f.Definition.ToLower().Contains(term)) - || (f.Example != null && f.Example.ToLower().Contains(term)))) - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error searching flashcards for user: {UserId}, term: {SearchTerm}", userId, searchTerm); - throw; - } - } - - public async Task> GetFavoriteFlashcardsAsync(Guid userId) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId && f.IsFavorite && !f.IsArchived) - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting favorite flashcards for user: {UserId}", userId); - throw; - } - } - - public async Task> GetArchivedFlashcardsAsync(Guid userId) - { - try - { - return await _dbSet - .AsNoTracking() - .Where(f => f.UserId == userId && f.IsArchived) - .OrderByDescending(f => f.UpdatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting archived flashcards for user: {UserId}", userId); - throw; - } - } - - #endregion - - #region 批次操作 - - public async Task BulkUpdateMasteryLevelAsync(IEnumerable flashcardIds, int newMasteryLevel) - { - try - { - var idList = flashcardIds.ToList(); - var flashcards = await _dbSet - .Where(f => idList.Contains(f.Id)) - .ToListAsync(); - - foreach (var flashcard in flashcards) - { - flashcard.MasteryLevel = newMasteryLevel; - flashcard.UpdatedAt = DateTime.UtcNow; - } - - _dbSet.UpdateRange(flashcards); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error bulk updating mastery level for flashcards: {FlashcardIds}", - string.Join(",", flashcardIds)); - return false; - } - } - - public async Task BulkUpdateNextReviewDateAsync(IEnumerable flashcardIds, DateTime newDate) - { - try - { - var idList = flashcardIds.ToList(); - var flashcards = await _dbSet - .Where(f => idList.Contains(f.Id)) - .ToListAsync(); - - foreach (var flashcard in flashcards) - { - flashcard.NextReviewDate = newDate; - flashcard.UpdatedAt = DateTime.UtcNow; - } - - _dbSet.UpdateRange(flashcards); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error bulk updating next review date for flashcards: {FlashcardIds}", - string.Join(",", flashcardIds)); - return false; - } - } - - #endregion - - #region 性能優化查詢 - - public async Task> GetFlashcardsWithIncludesAsync(Guid userId, - bool includeTags = false, - bool includeStudyRecords = false) - { - try - { - var query = _dbSet.AsNoTracking() - .Where(f => f.UserId == userId && !f.IsArchived); - - if (includeTags) - { - query = query.Include(f => f.FlashcardTags!) - .ThenInclude(ft => ft.Tag); - } - - if (includeStudyRecords) - { - query = query.Include(f => f.StudyRecords!.OrderByDescending(sr => sr.StudiedAt).Take(10)); - } - - return await query - .OrderByDescending(f => f.CreatedAt) - .ToListAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting flashcards with includes for user: {UserId}", userId); - throw; - } - } - - #endregion -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Repositories/IFlashcardRepository.cs b/backend/DramaLing.Api/Repositories/IFlashcardRepository.cs deleted file mode 100644 index bfc36b1..0000000 --- a/backend/DramaLing.Api/Repositories/IFlashcardRepository.cs +++ /dev/null @@ -1,38 +0,0 @@ -using DramaLing.Api.Models.Entities; - -namespace DramaLing.Api.Repositories; - -/// -/// Flashcard 專門的 Repository 介面,包含業務特定的查詢方法 -/// -public interface IFlashcardRepository : IRepository -{ - // 用戶相關查詢 - Task> GetFlashcardsByUserIdAsync(Guid userId); - Task> GetFlashcardsByCardSetIdAsync(Guid cardSetId); - - // 學習相關查詢 - Task> GetDueFlashcardsAsync(Guid userId, DateTime dueDate); - Task> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel); - Task> GetRecentlyAddedAsync(Guid userId, int count); - Task> GetMostReviewedAsync(Guid userId, int count); - - // 統計查詢 - Task GetTotalFlashcardsCountAsync(Guid userId); - Task GetMasteredFlashcardsCountAsync(Guid userId); - Task> GetFlashcardsByDifficultyStatsAsync(Guid userId); - - // 搜尋功能 - Task> SearchFlashcardsAsync(Guid userId, string searchTerm); - Task> GetFavoriteFlashcardsAsync(Guid userId); - Task> GetArchivedFlashcardsAsync(Guid userId); - - // 批次操作 - Task BulkUpdateMasteryLevelAsync(IEnumerable flashcardIds, int newMasteryLevel); - Task BulkUpdateNextReviewDateAsync(IEnumerable flashcardIds, DateTime newDate); - - // 性能優化查詢 - Task> GetFlashcardsWithIncludesAsync(Guid userId, - bool includeTags = false, - bool includeStudyRecords = false); -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Domain/Learning/IFlashcardService.cs.bak b/backend/DramaLing.Api/Services/Domain/Learning/IFlashcardService.cs.bak deleted file mode 100644 index 5802965..0000000 --- a/backend/DramaLing.Api/Services/Domain/Learning/IFlashcardService.cs.bak +++ /dev/null @@ -1,116 +0,0 @@ -using DramaLing.Api.Models.Entities; -using DramaLing.Api.Models.DTOs; - -namespace DramaLing.Api.Services.Domain.Learning; - -/// -/// 詞卡服務介面,封裝詞卡相關的業務邏輯 -/// -public interface IFlashcardService -{ - // 基本 CRUD 操作 - Task CreateFlashcardAsync(CreateFlashcardRequest request); - Task GetFlashcardAsync(Guid flashcardId, Guid userId); - Task UpdateFlashcardAsync(Guid flashcardId, UpdateFlashcardRequest request); - Task DeleteFlashcardAsync(Guid flashcardId, Guid userId); - - // 查詢操作 - Task> GetUserFlashcardsAsync(Guid userId, FlashcardQueryOptions? options = null); - Task> GetDueFlashcardsAsync(Guid userId, int limit = 20); - Task> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel); - Task> SearchFlashcardsAsync(Guid userId, string searchTerm); - - // 學習相關操作 - Task GetStudyRecommendationsAsync(Guid userId); - Task UpdateMasteryLevelAsync(Guid flashcardId, int masteryLevel, Guid userId); - Task MarkAsReviewedAsync(Guid flashcardId, StudyResult result, Guid userId); - - // 批次操作 - Task> CreateFlashcardsFromAnalysisAsync(SentenceAnalysisData analysis, Guid userId); - Task BulkUpdateMasteryAsync(IEnumerable flashcardIds, int masteryLevel, Guid userId); - - // 統計功能 - Task GetFlashcardStatsAsync(Guid userId); - Task GetLearningProgressAsync(Guid userId); -} - -/// -/// 詞卡查詢選項 -/// -public class FlashcardQueryOptions -{ - public int? Limit { get; set; } - public int? Offset { get; set; } - public string? SortBy { get; set; } = "CreatedAt"; - public bool SortDescending { get; set; } = true; - public bool? IsFavorite { get; set; } - public bool? IsArchived { get; set; } - public string? DifficultyLevel { get; set; } - public Guid? CardSetId { get; set; } -} - -/// -/// 學習推薦 -/// -public class StudyRecommendations -{ - public IEnumerable DueCards { get; set; } = new List(); - public IEnumerable NewCards { get; set; } = new List(); - public IEnumerable ReviewCards { get; set; } = new List(); - public IEnumerable ChallengingCards { get; set; } = new List(); - public int RecommendedStudyTimeMinutes { get; set; } - public string RecommendationReason { get; set; } = string.Empty; -} - -/// -/// 學習結果 -/// -public class StudyResult -{ - public int QualityRating { get; set; } // 1-5 SM2 算法評分 - public int ResponseTimeMs { get; set; } - public bool IsCorrect { get; set; } - public string? UserAnswer { get; set; } - public DateTime StudiedAt { get; set; } = DateTime.UtcNow; -} - -/// -/// 詞卡統計 -/// -public class FlashcardStats -{ - public int TotalCards { get; set; } - public int MasteredCards { get; set; } - public int DueCards { get; set; } - public int NewCards { get; set; } - public double MasteryRate => TotalCards > 0 ? (double)MasteredCards / TotalCards : 0; - public Dictionary DifficultyDistribution { get; set; } = new(); - public TimeSpan AverageStudyTime { get; set; } -} - -/// -/// 學習進度 -/// -public class LearningProgress -{ - public int ConsecutiveDays { get; set; } - public int TotalStudyDays { get; set; } - public int WordsLearned { get; set; } - public int WordsMastered { get; set; } - public string CurrentLevel { get; set; } = "A2"; - public double ProgressToNextLevel { get; set; } - public DateTime LastStudyDate { get; set; } - public IEnumerable RecentProgress { get; set; } = new List(); -} - -/// -/// 每日進度 -/// -public class DailyProgress -{ - public DateOnly Date { get; set; } - public int CardsStudied { get; set; } - public int CorrectAnswers { get; set; } - public TimeSpan StudyTime { get; set; } - public double AccuracyRate => CardsStudied > 0 ? (double)CorrectAnswers / CardsStudied : 0; -} \ No newline at end of file