dramaling-vocab-learning/backend/DramaLing.Api/Controllers/FlashcardsController.cs

443 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<FlashcardsController> _logger;
private readonly IImageStorageService _imageStorageService;
public FlashcardsController(
DramaLingDbContext context,
ILogger<FlashcardsController> 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<ActionResult> 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<object>();
foreach (var flashcard in flashcards)
{
var exampleImages = new List<ExampleImageDto>();
// 處理關聯的圖片
foreach (var flashcardImage in flashcard.FlashcardExampleImages)
{
var imageUrl = await _imageStorageService.GetImageUrlAsync(flashcardImage.ExampleImage.RelativePath);
exampleImages.Add(new ExampleImageDto
{
Id = flashcardImage.ExampleImage.Id.ToString(),
ImageUrl = imageUrl,
IsPrimary = flashcardImage.IsPrimary,
QualityScore = flashcardImage.ExampleImage.QualityScore,
FileSize = flashcardImage.ExampleImage.FileSize,
CreatedAt = flashcardImage.ExampleImage.CreatedAt
});
}
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,
HasExampleImage = exampleImages.Any(),
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<ActionResult> 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<string, object>(),
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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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; }
}