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

362 lines
13 KiB
C#

using Microsoft.AspNetCore.Mvc;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Models.DTOs;
using DramaLing.Api.Repositories;
using DramaLing.Api.Services.Review;
using Microsoft.AspNetCore.Authorization;
using DramaLing.Api.Utils;
namespace DramaLing.Api.Controllers;
[Route("api/flashcards")]
[AllowAnonymous]
public class FlashcardsController : BaseController
{
private readonly IFlashcardRepository _flashcardRepository;
private readonly IReviewService _reviewService;
public FlashcardsController(
IFlashcardRepository flashcardRepository,
IReviewService reviewService,
ILogger<FlashcardsController> logger) : base(logger)
{
_flashcardRepository = flashcardRepository;
_reviewService = reviewService;
}
[HttpGet]
public async Task<IActionResult> GetFlashcards(
[FromQuery] string? search = null,
[FromQuery] bool favoritesOnly = false)
{
try
{
var userId = await GetCurrentUserIdAsync();
var flashcards = await _flashcardRepository.GetByUserIdAsync(userId, search, favoritesOnly);
var flashcardData = new
{
Flashcards = flashcards.Select(f => new
{
f.Id,
f.Word,
f.Translation,
f.Definition,
f.PartOfSpeech,
f.Pronunciation,
f.Example,
f.ExampleTranslation,
f.IsFavorite,
DifficultyLevelNumeric = f.DifficultyLevelNumeric,
CEFR = CEFRHelper.ToString(f.DifficultyLevelNumeric),
f.CreatedAt,
f.UpdatedAt,
// 添加圖片相關屬性
HasExampleImage = f.FlashcardExampleImages.Any(),
PrimaryImageUrl = f.FlashcardExampleImages
.Where(fei => fei.IsPrimary)
.Select(fei => $"/images/examples/{fei.ExampleImage.RelativePath}")
.FirstOrDefault()
}),
Count = flashcards.Count()
};
return SuccessResponse(flashcardData);
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting flashcards");
return ErrorResponse("INTERNAL_ERROR", "載入詞卡失敗");
}
}
[HttpPost]
public async Task<IActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
{
try
{
if (!ModelState.IsValid)
{
return HandleModelStateErrors();
}
var userId = await GetCurrentUserIdAsync();
var flashcard = new Flashcard
{
Id = Guid.NewGuid(),
UserId = userId,
Word = request.Word,
Translation = request.Translation,
Definition = request.Definition ?? "",
PartOfSpeech = request.PartOfSpeech,
Pronunciation = request.Pronunciation,
Example = request.Example,
ExampleTranslation = request.ExampleTranslation,
DifficultyLevelNumeric = CEFRHelper.ToNumeric(request.CEFR ?? "A0"),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
await _flashcardRepository.AddAsync(flashcard);
await _flashcardRepository.SaveChangesAsync();
return SuccessResponse(flashcard, "詞卡創建成功");
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating flashcard");
return ErrorResponse("INTERNAL_ERROR", "創建詞卡失敗");
}
}
[HttpGet("{id}")]
public async Task<IActionResult> GetFlashcard(Guid id)
{
try
{
var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
// 格式化返回數據,保持與列表 API 一致
var flashcardData = new
{
flashcard.Id,
flashcard.Word,
flashcard.Translation,
flashcard.Definition,
flashcard.PartOfSpeech,
flashcard.Pronunciation,
flashcard.Example,
flashcard.ExampleTranslation,
flashcard.IsFavorite,
DifficultyLevelNumeric = flashcard.DifficultyLevelNumeric,
CEFR = CEFRHelper.ToString(flashcard.DifficultyLevelNumeric),
flashcard.CreatedAt,
flashcard.UpdatedAt,
// 添加圖片相關屬性
HasExampleImage = flashcard.FlashcardExampleImages.Any(),
PrimaryImageUrl = flashcard.FlashcardExampleImages
.Where(fei => fei.IsPrimary)
.Select(fei => $"/images/examples/{fei.ExampleImage.RelativePath}")
.FirstOrDefault(),
// 添加複習相關屬性 (暫時預設值)
TimesReviewed = 0,
MasteryLevel = 0,
NextReviewDate = (DateTime?)null,
// 保留完整的圖片關聯數據供前端使用
FlashcardExampleImages = flashcard.FlashcardExampleImages
};
return SuccessResponse(flashcardData);
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting flashcard {FlashcardId}", id);
return ErrorResponse("INTERNAL_ERROR", "取得詞卡失敗");
}
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
{
try
{
if (!ModelState.IsValid)
{
return HandleModelStateErrors();
}
var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
// 更新詞卡資訊
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 _flashcardRepository.UpdateAsync(flashcard);
await _flashcardRepository.SaveChangesAsync();
return SuccessResponse(flashcard, "詞卡更新成功");
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating flashcard {FlashcardId}", id);
return ErrorResponse("INTERNAL_ERROR", "更新詞卡失敗");
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteFlashcard(Guid id)
{
try
{
var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
await _flashcardRepository.DeleteAsync(flashcard);
await _flashcardRepository.SaveChangesAsync();
return SuccessResponse(new { Id = id }, "詞卡已刪除");
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting flashcard {FlashcardId}", id);
return ErrorResponse("INTERNAL_ERROR", "刪除詞卡失敗");
}
}
[HttpPost("{id}/favorite")]
public async Task<IActionResult> ToggleFavorite(Guid id)
{
try
{
var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
flashcard.IsFavorite = !flashcard.IsFavorite;
flashcard.UpdatedAt = DateTime.UtcNow;
await _flashcardRepository.UpdateAsync(flashcard);
await _flashcardRepository.SaveChangesAsync();
var result = new {
Id = flashcard.Id,
IsFavorite = flashcard.IsFavorite
};
var message = flashcard.IsFavorite ? "已加入收藏" : "已取消收藏";
return SuccessResponse(result, message);
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error toggling favorite for flashcard {FlashcardId}", id);
return ErrorResponse("INTERNAL_ERROR", "切換收藏狀態失敗");
}
}
[HttpGet("due")]
public async Task<IActionResult> GetDueFlashcards(
[FromQuery] int limit = 10,
[FromQuery] bool includeToday = true,
[FromQuery] bool includeOverdue = true,
[FromQuery] bool favoritesOnly = false)
{
try
{
var userId = await GetCurrentUserIdAsync();
var query = new DueFlashcardsQuery
{
Limit = limit,
IncludeToday = includeToday,
IncludeOverdue = includeOverdue,
FavoritesOnly = favoritesOnly
};
var response = await _reviewService.GetDueFlashcardsAsync(userId, query);
return Ok(response);
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting due flashcards");
return ErrorResponse("INTERNAL_ERROR", "載入待複習詞卡失敗");
}
}
[HttpPost("{id}/review")]
public async Task<IActionResult> SubmitReview(Guid id, [FromBody] ReviewRequest request)
{
try
{
if (!ModelState.IsValid)
{
return HandleModelStateErrors();
}
var userId = await GetCurrentUserIdAsync();
var response = await _reviewService.SubmitReviewAsync(userId, id, request);
return Ok(response);
}
catch (UnauthorizedAccessException)
{
return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting review for flashcard {FlashcardId}", id);
return ErrorResponse("INTERNAL_ERROR", "提交複習結果失敗");
}
}
}
// 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; }
public string? CEFR { get; set; } = string.Empty;
}