using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Models.DTOs; using DramaLing.Api.Models.DTOs.SpacedRepetition; using DramaLing.Api.Services; using DramaLing.Api.Services.Storage; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; namespace DramaLing.Api.Controllers; [ApiController] [Route("api/flashcards")] [AllowAnonymous] // 暫時移除認證要求,修復網路錯誤 public class FlashcardsController : ControllerBase { private readonly DramaLingDbContext _context; private readonly ILogger _logger; private readonly IImageStorageService _imageStorageService; // 🆕 智能複習服務依賴 private readonly ISpacedRepetitionService _spacedRepetitionService; private readonly IReviewTypeSelectorService _reviewTypeSelectorService; private readonly IQuestionGeneratorService _questionGeneratorService; public FlashcardsController( DramaLingDbContext context, ILogger logger, IImageStorageService imageStorageService, ISpacedRepetitionService spacedRepetitionService, IReviewTypeSelectorService reviewTypeSelectorService, IQuestionGeneratorService questionGeneratorService) { _context = context; _logger = logger; _imageStorageService = imageStorageService; _spacedRepetitionService = spacedRepetitionService; _reviewTypeSelectorService = reviewTypeSelectorService; _questionGeneratorService = questionGeneratorService; } private Guid GetUserId() { // 暫時使用固定測試用戶 ID,避免認證問題 // TODO: 恢復真實認證後改回 JWT Token 解析 return Guid.Parse("00000000-0000-0000-0000-000000000001"); // 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 in token"); } [HttpGet] public async Task GetFlashcards( [FromQuery] string? search = null, [FromQuery] bool favoritesOnly = false, [FromQuery] string? cefrLevel = null, [FromQuery] string? partOfSpeech = null, [FromQuery] string? masteryLevel = null) { try { var userId = GetUserId(); var query = _context.Flashcards .Include(f => f.FlashcardExampleImages) .ThenInclude(fei => fei.ExampleImage) .Where(f => f.UserId == userId && !f.IsArchived) .AsQueryable(); // 搜尋篩選 (擴展支援例句內容) if (!string.IsNullOrEmpty(search)) { query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search) || (f.Definition != null && f.Definition.Contains(search)) || (f.Example != null && f.Example.Contains(search)) || (f.ExampleTranslation != null && f.ExampleTranslation.Contains(search))); } // 收藏篩選 if (favoritesOnly) { query = query.Where(f => f.IsFavorite); } // CEFR 等級篩選 if (!string.IsNullOrEmpty(cefrLevel)) { query = query.Where(f => f.DifficultyLevel == cefrLevel); } // 詞性篩選 if (!string.IsNullOrEmpty(partOfSpeech)) { query = query.Where(f => f.PartOfSpeech == partOfSpeech); } // 掌握度篩選 if (!string.IsNullOrEmpty(masteryLevel)) { switch (masteryLevel.ToLower()) { case "high": query = query.Where(f => f.MasteryLevel >= 80); break; case "medium": query = query.Where(f => f.MasteryLevel >= 60 && f.MasteryLevel < 80); break; case "low": query = query.Where(f => f.MasteryLevel < 60); break; } } var flashcards = await query .AsNoTracking() // 效能優化:只讀查詢 .OrderByDescending(f => f.CreatedAt) .ToListAsync(); // 生成圖片資訊 var flashcardDtos = new List(); foreach (var flashcard in flashcards) { // 獲取例句圖片資料 (與 GetFlashcard 方法保持一致) var exampleImages = flashcard.FlashcardExampleImages? .Select(fei => new { Id = fei.ExampleImage.Id, ImageUrl = $"http://localhost:5008/images/examples/{fei.ExampleImage.RelativePath}", IsPrimary = fei.IsPrimary, QualityScore = fei.ExampleImage.QualityScore, FileSize = fei.ExampleImage.FileSize, CreatedAt = fei.ExampleImage.CreatedAt }) .ToList(); flashcardDtos.Add(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, // 新增圖片相關欄位 ExampleImages = exampleImages ?? (object)new List(), HasExampleImage = exampleImages?.Any() ?? false, PrimaryImageUrl = exampleImages?.FirstOrDefault(img => img.IsPrimary)?.ImageUrl }); } return Ok(new { Success = true, Data = new { Flashcards = flashcardDtos, Count = flashcardDtos.Count } }); } catch (Exception ex) { _logger.LogError(ex, "Error getting flashcards for user"); return StatusCode(500, new { Success = false, Error = "Failed to load flashcards" }); } } [HttpPost] public async Task CreateFlashcard([FromBody] CreateFlashcardRequest request) { try { var userId = GetUserId(); // 確保測試用戶存在 var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); if (testUser == null) { testUser = new User { Id = userId, Username = "testuser", Email = "test@example.com", PasswordHash = "test_hash", DisplayName = "測試用戶", SubscriptionType = "free", Preferences = new Dictionary(), EnglishLevel = "A2", LevelUpdatedAt = DateTime.UtcNow, IsLevelVerified = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Users.Add(testUser); await _context.SaveChangesAsync(); } // 檢測重複詞卡 var existing = await _context.Flashcards .FirstOrDefaultAsync(f => f.UserId == userId && f.Word.ToLower() == request.Word.ToLower() && !f.IsArchived); if (existing != null) { return Ok(new { Success = false, Error = "詞卡已存在", IsDuplicate = true, ExistingCard = new { existing.Id, existing.Word, existing.Translation, existing.CreatedAt } }); } var flashcard = new Flashcard { Id = Guid.NewGuid(), UserId = userId, CardSetId = null, // 暫時不使用 CardSet Word = request.Word, Translation = request.Translation, Definition = request.Definition ?? "", PartOfSpeech = request.PartOfSpeech, Pronunciation = request.Pronunciation, Example = request.Example, ExampleTranslation = request.ExampleTranslation, MasteryLevel = 0, TimesReviewed = 0, IsFavorite = false, NextReviewDate = DateTime.Today, DifficultyLevel = "A2", // 預設等級 EasinessFactor = 2.5f, IntervalDays = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Flashcards.Add(flashcard); await _context.SaveChangesAsync(); return Ok(new { Success = true, Data = new { flashcard.Id, flashcard.Word, flashcard.Translation, flashcard.Definition, flashcard.CreatedAt }, Message = "詞卡創建成功" }); } catch (Exception ex) { _logger.LogError(ex, "Error creating flashcard"); return StatusCode(500, new { Success = false, Error = "Failed to create flashcard" }); } } [HttpGet("{id}")] public async Task GetFlashcard(Guid id) { try { var userId = GetUserId(); var flashcard = await _context.Flashcards .Include(f => f.FlashcardExampleImages) .ThenInclude(fei => fei.ExampleImage) .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId); if (flashcard == null) { return NotFound(new { Success = false, Error = "Flashcard not found" }); } // 獲取例句圖片資料 var exampleImages = flashcard.FlashcardExampleImages ?.Select(fei => new { Id = fei.ExampleImage.Id, ImageUrl = $"http://localhost:5008/images/examples/{fei.ExampleImage.RelativePath}", IsPrimary = fei.IsPrimary, QualityScore = fei.ExampleImage.QualityScore, FileSize = fei.ExampleImage.FileSize, CreatedAt = fei.ExampleImage.CreatedAt }) .ToList(); 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, // 新增圖片相關欄位 ExampleImages = exampleImages ?? (object)new List(), HasExampleImage = exampleImages?.Any() ?? false, PrimaryImageUrl = flashcard.FlashcardExampleImages? .Where(fei => fei.IsPrimary) .Select(fei => $"http://localhost:5008/images/examples/{fei.ExampleImage.RelativePath}") .FirstOrDefault() } }); } 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] CreateFlashcardRequest 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) { 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 = "詞卡已刪除" }); } catch (Exception ex) { _logger.LogError(ex, "Error deleting flashcard {FlashcardId}", id); return StatusCode(500, new { Success = false, Error = "Failed to delete flashcard" }); } } [HttpPost("{id}/favorite")] public async Task ToggleFavorite(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" }); } flashcard.IsFavorite = !flashcard.IsFavorite; flashcard.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return Ok(new { Success = true, IsFavorite = flashcard.IsFavorite, Message = flashcard.IsFavorite ? "已加入收藏" : "已取消收藏" }); } catch (Exception ex) { _logger.LogError(ex, "Error toggling favorite for flashcard {FlashcardId}", id); return StatusCode(500, new { Success = false, Error = "Failed to toggle favorite" }); } } // ================== 🆕 智能複習API端點 ================== /// /// 取得到期詞卡列表 /// [HttpGet("due")] public async Task GetDueFlashcards( [FromQuery] string? date = null, [FromQuery] int limit = 50) { try { var userId = GetUserId(); var queryDate = DateTime.TryParse(date, out var parsed) ? parsed : DateTime.Now.Date; var dueCards = await _spacedRepetitionService.GetDueFlashcardsAsync(userId, queryDate, limit); _logger.LogInformation("Retrieved {Count} due flashcards for user {UserId}", dueCards.Count, userId); return Ok(new { success = true, data = dueCards, count = dueCards.Count }); } catch (Exception ex) { _logger.LogError(ex, "Error getting due flashcards"); return StatusCode(500, new { success = false, error = "Failed to get due flashcards" }); } } /// /// 取得下一張需要復習的詞卡 (最高優先級) /// [HttpGet("next-review")] public async Task GetNextReviewCard() { try { var userId = GetUserId(); var nextCard = await _spacedRepetitionService.GetNextReviewCardAsync(userId); if (nextCard == null) { return Ok(new { success = true, data = (object?)null, message = "沒有到期的詞卡" }); } // 計算當前熟悉度 var currentMasteryLevel = _spacedRepetitionService.CalculateCurrentMasteryLevel(nextCard); // 設置UserLevel和WordLevel (如果是舊資料) if (nextCard.UserLevel == 0) nextCard.UserLevel = CEFRMappingService.GetDefaultUserLevel(); if (nextCard.WordLevel == 0) nextCard.WordLevel = CEFRMappingService.GetWordLevel(nextCard.DifficultyLevel); var response = new { nextCard.Id, nextCard.Word, nextCard.Translation, nextCard.Definition, nextCard.Pronunciation, nextCard.PartOfSpeech, nextCard.Example, nextCard.ExampleTranslation, nextCard.MasteryLevel, nextCard.TimesReviewed, nextCard.IsFavorite, nextCard.NextReviewDate, nextCard.DifficultyLevel, // 智能複習擴展欄位 nextCard.UserLevel, nextCard.WordLevel, BaseMasteryLevel = nextCard.MasteryLevel, LastReviewDate = nextCard.LastReviewedAt, CurrentInterval = nextCard.IntervalDays, IsOverdue = DateTime.Now.Date > nextCard.NextReviewDate.Date, OverdueDays = Math.Max(0, (DateTime.Now.Date - nextCard.NextReviewDate.Date).Days), CurrentMasteryLevel = currentMasteryLevel }; _logger.LogInformation("Retrieved next review card: {Word} for user {UserId}", nextCard.Word, userId); return Ok(new { success = true, data = response }); } catch (Exception ex) { _logger.LogError(ex, "Error getting next review card"); return StatusCode(500, new { success = false, error = "Failed to get next review card" }); } } /// /// 系統自動選擇最適合的複習題型 (基於CEFR等級) /// [HttpPost("{id}/optimal-review-mode")] public async Task GetOptimalReviewMode(Guid id, [FromBody] OptimalModeRequest request) { try { var result = await _reviewTypeSelectorService.SelectOptimalReviewModeAsync( id, request.UserCEFRLevel, request.WordCEFRLevel); _logger.LogInformation("Selected optimal review mode {SelectedMode} for flashcard {FlashcardId}, userCEFR: {UserCEFR}, wordCEFR: {WordCEFR}", result.SelectedMode, id, request.UserCEFRLevel, request.WordCEFRLevel); return Ok(new { success = true, data = result }); } catch (Exception ex) { _logger.LogError(ex, "Error selecting optimal review mode for flashcard {FlashcardId}", id); return StatusCode(500, new { success = false, error = "Failed to select optimal review mode" }); } } /// /// 生成指定題型的題目選項 /// [HttpPost("{id}/question")] public async Task GenerateQuestion(Guid id, [FromBody] QuestionRequest request) { try { var questionData = await _questionGeneratorService.GenerateQuestionAsync(id, request.QuestionType); _logger.LogInformation("Generated {QuestionType} question for flashcard {FlashcardId}", request.QuestionType, id); return Ok(new { success = true, data = questionData }); } catch (Exception ex) { _logger.LogError(ex, "Error generating {QuestionType} question for flashcard {FlashcardId}", request.QuestionType, id); return StatusCode(500, new { success = false, error = "Failed to generate question" }); } } /// /// 提交復習結果並更新間隔重複算法 /// [HttpPost("{id}/review")] public async Task SubmitReview(Guid id, [FromBody] ReviewRequest request) { try { var result = await _spacedRepetitionService.ProcessReviewAsync(id, request); _logger.LogInformation("Processed review for flashcard {FlashcardId}, questionType: {QuestionType}, isCorrect: {IsCorrect}", id, request.QuestionType, request.IsCorrect); return Ok(new { success = true, data = result }); } catch (Exception ex) { _logger.LogError(ex, "Error processing review for flashcard {FlashcardId}", id); return StatusCode(500, new { success = false, error = "Failed to process review" }); } } } // 請求 DTO public class CreateFlashcardRequest { 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; } = string.Empty; public string Pronunciation { get; set; } = string.Empty; public string Example { get; set; } = string.Empty; public string? ExampleTranslation { get; set; } }