using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Models.DTOs; 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; public FlashcardsController( DramaLingDbContext context, ILogger logger, IImageStorageService imageStorageService) { _context = context; _logger = logger; _imageStorageService = imageStorageService; } 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" }); } } } // 請求 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; } }