678 lines
26 KiB
C#
678 lines
26 KiB
C#
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<FlashcardsController> _logger;
|
||
private readonly IImageStorageService _imageStorageService;
|
||
private readonly IAuthService _authService;
|
||
// 🆕 智能複習服務依賴
|
||
private readonly ISpacedRepetitionService _spacedRepetitionService;
|
||
private readonly IReviewTypeSelectorService _reviewTypeSelectorService;
|
||
private readonly IQuestionGeneratorService _questionGeneratorService;
|
||
// 🆕 智能填空題服務依賴
|
||
private readonly IBlankGenerationService _blankGenerationService;
|
||
|
||
public FlashcardsController(
|
||
DramaLingDbContext context,
|
||
ILogger<FlashcardsController> logger,
|
||
IImageStorageService imageStorageService,
|
||
IAuthService authService,
|
||
ISpacedRepetitionService spacedRepetitionService,
|
||
IReviewTypeSelectorService reviewTypeSelectorService,
|
||
IQuestionGeneratorService questionGeneratorService,
|
||
IBlankGenerationService blankGenerationService)
|
||
{
|
||
_context = context;
|
||
_logger = logger;
|
||
_imageStorageService = imageStorageService;
|
||
_authService = authService;
|
||
_spacedRepetitionService = spacedRepetitionService;
|
||
_reviewTypeSelectorService = reviewTypeSelectorService;
|
||
_questionGeneratorService = questionGeneratorService;
|
||
_blankGenerationService = blankGenerationService;
|
||
}
|
||
|
||
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();
|
||
_logger.LogInformation("GetFlashcards called for user: {UserId}", userId);
|
||
|
||
var query = _context.Flashcards
|
||
.Include(f => f.FlashcardExampleImages)
|
||
.ThenInclude(fei => fei.ExampleImage)
|
||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||
.AsQueryable();
|
||
|
||
_logger.LogInformation("Base query created successfully");
|
||
|
||
// 搜尋篩選 (擴展支援例句內容)
|
||
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;
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("Executing database query...");
|
||
var flashcards = await query
|
||
.AsNoTracking() // 效能優化:只讀查詢
|
||
.OrderByDescending(f => f.CreatedAt)
|
||
.ToListAsync();
|
||
|
||
_logger.LogInformation("Found {Count} flashcards", flashcards.Count);
|
||
|
||
// 生成圖片資訊
|
||
var flashcardDtos = new List<object>();
|
||
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<object>(),
|
||
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: {Message}", ex.Message);
|
||
_logger.LogError("Stack trace: {StackTrace}", ex.StackTrace);
|
||
return StatusCode(500, new { Success = false, Error = "Failed to load flashcards", Details = ex.Message });
|
||
}
|
||
}
|
||
|
||
[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,
|
||
Word = request.Word,
|
||
Translation = request.Translation,
|
||
Definition = request.Definition ?? "",
|
||
PartOfSpeech = request.PartOfSpeech,
|
||
Pronunciation = request.Pronunciation,
|
||
Example = request.Example,
|
||
ExampleTranslation = request.ExampleTranslation,
|
||
Synonyms = request.Synonyms != null && request.Synonyms.Any()
|
||
? System.Text.Json.JsonSerializer.Serialize(request.Synonyms)
|
||
: null,
|
||
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
|
||
.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<object>(),
|
||
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<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" });
|
||
}
|
||
}
|
||
|
||
// ================== 🆕 智能複習API端點 ==================
|
||
|
||
/// <summary>
|
||
/// 取得到期詞卡列表
|
||
/// </summary>
|
||
[HttpGet("due")]
|
||
public async Task<ActionResult> 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);
|
||
|
||
// 🆕 智能挖空處理:檢查並生成缺失的填空題目
|
||
var cardsToUpdate = new List<Flashcard>();
|
||
foreach(var flashcard in dueCards)
|
||
{
|
||
if(string.IsNullOrEmpty(flashcard.FilledQuestionText) && !string.IsNullOrEmpty(flashcard.Example))
|
||
{
|
||
_logger.LogDebug("Generating blank question for flashcard {Id}, word: {Word}",
|
||
flashcard.Id, flashcard.Word);
|
||
|
||
var blankQuestion = await _blankGenerationService.GenerateBlankQuestionAsync(
|
||
flashcard.Word, flashcard.Example);
|
||
|
||
if(!string.IsNullOrEmpty(blankQuestion))
|
||
{
|
||
flashcard.FilledQuestionText = blankQuestion;
|
||
flashcard.UpdatedAt = DateTime.UtcNow;
|
||
cardsToUpdate.Add(flashcard);
|
||
|
||
_logger.LogInformation("Generated blank question for flashcard {Id}, word: {Word}",
|
||
flashcard.Id, flashcard.Word);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("Failed to generate blank question for flashcard {Id}, word: {Word}",
|
||
flashcard.Id, flashcard.Word);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批次更新資料庫
|
||
if (cardsToUpdate.Count > 0)
|
||
{
|
||
_context.UpdateRange(cardsToUpdate);
|
||
await _context.SaveChangesAsync();
|
||
_logger.LogInformation("Updated {Count} flashcards with blank questions", cardsToUpdate.Count);
|
||
}
|
||
|
||
_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" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取得下一張需要復習的詞卡 (最高優先級)
|
||
/// </summary>
|
||
[HttpGet("next-review")]
|
||
public async Task<ActionResult> 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欄位已移除 - 改用即時CEFR轉換
|
||
|
||
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,
|
||
// 智能複習擴展欄位 (改用即時CEFR轉換)
|
||
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" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 系統自動選擇最適合的複習題型 (基於CEFR等級)
|
||
/// </summary>
|
||
[HttpPost("{id}/optimal-review-mode")]
|
||
public async Task<ActionResult> 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" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成指定題型的題目選項
|
||
/// </summary>
|
||
[HttpPost("{id}/question")]
|
||
public async Task<ActionResult> 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" });
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提交復習結果並更新間隔重複算法
|
||
/// </summary>
|
||
[HttpPost("{id}/review")]
|
||
public async Task<ActionResult> 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; }
|
||
public List<string>? Synonyms { get; set; }
|
||
} |