refactor: 完成詞卡API架構統一與舊版代碼清理
**主要重構**:
- 統一到SimplifiedFlashcardsController,移除CardSets概念衝突
- 補全新版API:添加GET /{id}和PUT /{id}端點
- 重構FlashcardForm.tsx完全移除CardSets依賴
**刪除舊版代碼**:
- 移除FlashcardsController.cs (舊版API)
- 移除CardSetsController.cs (廢棄功能)
- 移除flashcards.ts服務 (舊版前端服務)
- 清理相關Repository和介面文件
**API端點現況**:
✅ POST /api/flashcards-simple - 創建詞卡
✅ GET /api/flashcards-simple - 獲取詞卡列表
✅ GET /api/flashcards-simple/{id} - 獲取單個詞卡
✅ PUT /api/flashcards-simple/{id} - 更新詞卡
✅ DELETE /api/flashcards-simple/{id} - 刪除詞卡
✅ POST /api/flashcards-simple/{id}/favorite - 切換收藏
**架構優化**:
- 統一API路由和回應格式
- 移除複雜的CardSets關聯邏輯
- 簡化前端組件介面
- 降低維護成本
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4989609da7
commit
8edcfc7545
|
|
@ -1,297 +0,0 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DramaLing.Api.Data;
|
||||
using DramaLing.Api.Models.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DramaLing.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class CardSetsController : ControllerBase
|
||||
{
|
||||
private readonly DramaLingDbContext _context;
|
||||
|
||||
public CardSetsController(DramaLingDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private Guid GetUserId()
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
private async Task EnsureDefaultCardSetAsync(Guid userId)
|
||||
{
|
||||
// 檢查用戶是否已有預設卡組
|
||||
var hasDefaultCardSet = await _context.CardSets
|
||||
.AnyAsync(cs => cs.UserId == userId && cs.IsDefault);
|
||||
|
||||
if (!hasDefaultCardSet)
|
||||
{
|
||||
// 創建預設「未分類」卡組
|
||||
var defaultCardSet = new CardSet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = "未分類",
|
||||
Description = "系統預設卡組,用於存放尚未分類的詞卡",
|
||||
Color = "bg-slate-700",
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
_context.CardSets.Add(defaultCardSet);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetCardSets()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
// 確保用戶有預設卡組
|
||||
await EnsureDefaultCardSetAsync(userId);
|
||||
|
||||
var cardSets = await _context.CardSets
|
||||
.Where(cs => cs.UserId == userId)
|
||||
.OrderBy(cs => cs.IsDefault ? 0 : 1) // 預設卡組排在最前面
|
||||
.ThenByDescending(cs => cs.CreatedAt)
|
||||
.Select(cs => new
|
||||
{
|
||||
cs.Id,
|
||||
cs.Name,
|
||||
cs.Description,
|
||||
cs.Color,
|
||||
cs.CardCount,
|
||||
cs.CreatedAt,
|
||||
cs.UpdatedAt,
|
||||
cs.IsDefault,
|
||||
// 計算進度 (簡化版)
|
||||
Progress = cs.CardCount > 0 ?
|
||||
_context.Flashcards
|
||||
.Where(f => f.CardSetId == cs.Id)
|
||||
.Average(f => (double?)f.MasteryLevel) ?? 0 : 0,
|
||||
LastStudied = cs.UpdatedAt,
|
||||
Tags = new string[] { } // Phase 1 簡化
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = new { Sets = cardSets }
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to fetch card sets",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateCardSet([FromBody] CreateCardSetRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest(new { Success = false, Error = "Name is required" });
|
||||
|
||||
if (request.Name.Length > 255)
|
||||
return BadRequest(new { Success = false, Error = "Name must be less than 255 characters" });
|
||||
|
||||
var cardSet = new CardSet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description?.Trim(),
|
||||
Color = request.Color ?? "bg-blue-500"
|
||||
};
|
||||
|
||||
_context.CardSets.Add(cardSet);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = cardSet,
|
||||
Message = "Card set created successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to create card set",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult> UpdateCardSet(Guid id, [FromBody] UpdateCardSetRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
var cardSet = await _context.CardSets
|
||||
.FirstOrDefaultAsync(cs => cs.Id == id && cs.UserId == userId);
|
||||
|
||||
if (cardSet == null)
|
||||
return NotFound(new { Success = false, Error = "Card set not found" });
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Name))
|
||||
cardSet.Name = request.Name.Trim();
|
||||
if (request.Description != null)
|
||||
cardSet.Description = request.Description?.Trim();
|
||||
if (!string.IsNullOrEmpty(request.Color))
|
||||
cardSet.Color = request.Color;
|
||||
|
||||
cardSet.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = cardSet,
|
||||
Message = "Card set updated successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to update card set",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteCardSet(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
var cardSet = await _context.CardSets
|
||||
.Include(cs => cs.Flashcards)
|
||||
.FirstOrDefaultAsync(cs => cs.Id == id && cs.UserId == userId);
|
||||
|
||||
if (cardSet == null)
|
||||
return NotFound(new { Success = false, Error = "Card set not found" });
|
||||
|
||||
// 防止刪除預設卡組
|
||||
if (cardSet.IsDefault)
|
||||
return BadRequest(new { Success = false, Error = "Cannot delete default card set" });
|
||||
|
||||
_context.CardSets.Remove(cardSet);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Message = "Card set deleted successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to delete card set",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("ensure-default")]
|
||||
public async Task<ActionResult> EnsureDefaultCardSet()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
await EnsureDefaultCardSetAsync(userId);
|
||||
|
||||
// 返回預設卡組
|
||||
var defaultCardSet = await _context.CardSets
|
||||
.FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault);
|
||||
|
||||
if (defaultCardSet == null)
|
||||
return StatusCode(500, new { Success = false, Error = "Failed to create default card set" });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = defaultCardSet,
|
||||
Message = "Default card set ensured"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to ensure default card set",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request DTOs
|
||||
public class CreateCardSetRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateCardSetRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DramaLing.Api.Data;
|
||||
using DramaLing.Api.Models.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DramaLing.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class FlashcardsController : ControllerBase
|
||||
{
|
||||
private readonly DramaLingDbContext _context;
|
||||
|
||||
public FlashcardsController(DramaLingDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private Guid GetUserId()
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
private async Task<Guid> GetOrCreateDefaultCardSetAsync(Guid userId)
|
||||
{
|
||||
// 嘗試找到預設卡組
|
||||
var defaultCardSet = await _context.CardSets
|
||||
.FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault);
|
||||
|
||||
if (defaultCardSet != null)
|
||||
return defaultCardSet.Id;
|
||||
|
||||
// 如果沒有預設卡組,創建一個
|
||||
var newDefaultCardSet = new CardSet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = "未分類",
|
||||
Description = "系統預設卡組,用於存放尚未分類的詞卡",
|
||||
Color = "bg-slate-700",
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
_context.CardSets.Add(newDefaultCardSet);
|
||||
await _context.SaveChangesAsync();
|
||||
return newDefaultCardSet.Id;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetFlashcards(
|
||||
[FromQuery] Guid? setId,
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] bool favoritesOnly = false,
|
||||
[FromQuery] int limit = 50,
|
||||
[FromQuery] int offset = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
var query = _context.Flashcards
|
||||
.Include(f => f.CardSet)
|
||||
.Where(f => f.UserId == userId);
|
||||
|
||||
if (setId.HasValue)
|
||||
query = query.Where(f => f.CardSetId == setId);
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
query = query.Where(f => f.Word.Contains(search) || f.Translation.Contains(search));
|
||||
|
||||
if (favoritesOnly)
|
||||
query = query.Where(f => f.IsFavorite);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var flashcards = await query
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Skip(offset)
|
||||
.Take(Math.Min(limit, 100))
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id,
|
||||
f.Word,
|
||||
f.Translation,
|
||||
f.Definition,
|
||||
f.PartOfSpeech,
|
||||
f.Pronunciation,
|
||||
f.Example,
|
||||
f.ExampleTranslation,
|
||||
f.MasteryLevel,
|
||||
f.TimesReviewed,
|
||||
f.IsFavorite,
|
||||
f.NextReviewDate,
|
||||
f.CreatedAt,
|
||||
CardSet = new
|
||||
{
|
||||
f.CardSet.Name,
|
||||
f.CardSet.Color
|
||||
}
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = new
|
||||
{
|
||||
Flashcards = flashcards,
|
||||
Total = total,
|
||||
HasMore = offset + limit < total
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to fetch flashcards",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
// 確定要使用的卡組ID
|
||||
Guid cardSetId;
|
||||
if (request.CardSetId.HasValue)
|
||||
{
|
||||
// 如果指定了卡組,驗證是否屬於用戶
|
||||
var cardSet = await _context.CardSets
|
||||
.FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId);
|
||||
|
||||
if (cardSet == null)
|
||||
return NotFound(new { Success = false, Error = "Card set not found" });
|
||||
|
||||
cardSetId = request.CardSetId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果沒有指定卡組,使用或創建預設卡組
|
||||
cardSetId = await GetOrCreateDefaultCardSetAsync(userId);
|
||||
}
|
||||
|
||||
var flashcard = new Flashcard
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
CardSetId = cardSetId,
|
||||
Word = request.Word.Trim(),
|
||||
Translation = request.Translation.Trim(),
|
||||
Definition = request.Definition.Trim(),
|
||||
PartOfSpeech = request.PartOfSpeech?.Trim(),
|
||||
Pronunciation = request.Pronunciation?.Trim(),
|
||||
Example = request.Example?.Trim(),
|
||||
ExampleTranslation = request.ExampleTranslation?.Trim()
|
||||
};
|
||||
|
||||
_context.Flashcards.Add(flashcard);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = flashcard,
|
||||
Message = "Flashcard created successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to create flashcard",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult> GetFlashcard(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
var flashcard = await _context.Flashcards
|
||||
.Include(f => f.CardSet)
|
||||
.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 = flashcard });
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to fetch flashcard",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] UpdateFlashcardRequest 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" });
|
||||
|
||||
// 更新欄位
|
||||
if (!string.IsNullOrEmpty(request.Word))
|
||||
flashcard.Word = request.Word.Trim();
|
||||
if (!string.IsNullOrEmpty(request.Translation))
|
||||
flashcard.Translation = request.Translation.Trim();
|
||||
if (!string.IsNullOrEmpty(request.Definition))
|
||||
flashcard.Definition = request.Definition.Trim();
|
||||
if (request.PartOfSpeech != null)
|
||||
flashcard.PartOfSpeech = request.PartOfSpeech?.Trim();
|
||||
if (request.Pronunciation != null)
|
||||
flashcard.Pronunciation = request.Pronunciation?.Trim();
|
||||
if (request.Example != null)
|
||||
flashcard.Example = request.Example?.Trim();
|
||||
if (request.ExampleTranslation != null)
|
||||
flashcard.ExampleTranslation = request.ExampleTranslation?.Trim();
|
||||
if (request.IsFavorite.HasValue)
|
||||
flashcard.IsFavorite = request.IsFavorite.Value;
|
||||
|
||||
flashcard.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = flashcard,
|
||||
Message = "Flashcard updated successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to update flashcard",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[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 = "Flashcard deleted successfully"
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to delete flashcard",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("batch")]
|
||||
public async Task<ActionResult> BatchCreateFlashcards([FromBody] BatchCreateFlashcardsRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
|
||||
if (request.Cards == null || !request.Cards.Any())
|
||||
return BadRequest(new { Success = false, Error = "No cards provided" });
|
||||
|
||||
if (request.Cards.Count > 50)
|
||||
return BadRequest(new { Success = false, Error = "Maximum 50 cards per batch" });
|
||||
|
||||
// 確定要使用的卡組ID
|
||||
Guid cardSetId;
|
||||
if (request.CardSetId.HasValue)
|
||||
{
|
||||
var cardSet = await _context.CardSets
|
||||
.FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId);
|
||||
|
||||
if (cardSet == null)
|
||||
return NotFound(new { Success = false, Error = "Card set not found" });
|
||||
|
||||
cardSetId = request.CardSetId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
cardSetId = await GetOrCreateDefaultCardSetAsync(userId);
|
||||
}
|
||||
|
||||
var savedCards = new List<object>();
|
||||
var errors = new List<string>();
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var cardRequest in request.Cards)
|
||||
{
|
||||
try
|
||||
{
|
||||
var flashcard = new Flashcard
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
CardSetId = cardSetId,
|
||||
Word = cardRequest.Word.Trim(),
|
||||
Translation = cardRequest.Translation.Trim(),
|
||||
Definition = cardRequest.Definition.Trim(),
|
||||
PartOfSpeech = cardRequest.PartOfSpeech?.Trim(),
|
||||
Pronunciation = cardRequest.Pronunciation?.Trim(),
|
||||
Example = cardRequest.Example?.Trim(),
|
||||
ExampleTranslation = cardRequest.ExampleTranslation?.Trim()
|
||||
};
|
||||
|
||||
_context.Flashcards.Add(flashcard);
|
||||
savedCards.Add(new
|
||||
{
|
||||
Id = flashcard.Id,
|
||||
Word = flashcard.Word,
|
||||
Translation = flashcard.Translation
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add($"Failed to save card '{cardRequest.Word}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = true,
|
||||
Data = new
|
||||
{
|
||||
SavedCards = savedCards,
|
||||
SavedCount = savedCards.Count,
|
||||
ErrorCount = errors.Count,
|
||||
Errors = errors
|
||||
},
|
||||
Message = $"Successfully saved {savedCards.Count} flashcards"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Unauthorized(new { Success = false, Error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to create flashcards",
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs
|
||||
public class CreateFlashcardRequest
|
||||
{
|
||||
public Guid? CardSetId { get; set; }
|
||||
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; }
|
||||
public string? Pronunciation { get; set; }
|
||||
public string? Example { get; set; }
|
||||
public string? ExampleTranslation { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateFlashcardRequest
|
||||
{
|
||||
public string? Word { get; set; }
|
||||
public string? Translation { get; set; }
|
||||
public string? Definition { get; set; }
|
||||
public string? PartOfSpeech { get; set; }
|
||||
public string? Pronunciation { get; set; }
|
||||
public string? Example { get; set; }
|
||||
public string? ExampleTranslation { get; set; }
|
||||
public bool? IsFavorite { get; set; }
|
||||
}
|
||||
|
||||
public class BatchCreateFlashcardsRequest
|
||||
{
|
||||
public Guid? CardSetId { get; set; }
|
||||
public List<CreateFlashcardRequest> Cards { get; set; } = new();
|
||||
}
|
||||
|
|
@ -204,6 +204,100 @@ public class SimplifiedFlashcardsController : ControllerBase
|
|||
}
|
||||
}
|
||||
|
||||
[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] CreateSimpleFlashcardRequest 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using DramaLing.Api.Data;
|
||||
using DramaLing.Api.Models.Entities;
|
||||
|
||||
namespace DramaLing.Api.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Flashcard Repository 實作,包含所有與詞卡相關的數據存取邏輯
|
||||
/// </summary>
|
||||
public class FlashcardRepository : BaseRepository<Flashcard>, IFlashcardRepository
|
||||
{
|
||||
public FlashcardRepository(DramaLingDbContext context, ILogger<FlashcardRepository> logger)
|
||||
: base(context, logger)
|
||||
{
|
||||
}
|
||||
|
||||
#region 用戶相關查詢
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetFlashcardsByUserIdAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetFlashcardsByCardSetIdAsync(Guid cardSetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.CardSetId == cardSetId && !f.IsArchived)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards for card set: {CardSetId}", cardSetId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 學習相關查詢
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetDueFlashcardsAsync(Guid userId, DateTime dueDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId
|
||||
&& !f.IsArchived
|
||||
&& f.NextReviewDate <= dueDate
|
||||
&& f.MasteryLevel < 5) // 未完全掌握的卡片
|
||||
.OrderBy(f => f.NextReviewDate)
|
||||
.ThenBy(f => f.EasinessFactor) // 難度較高的優先
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting due flashcards for user: {UserId}, date: {DueDate}", userId, dueDate);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId
|
||||
&& !f.IsArchived
|
||||
&& f.DifficultyLevel == difficultyLevel)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards by difficulty for user: {UserId}, level: {DifficultyLevel}",
|
||||
userId, difficultyLevel);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetRecentlyAddedAsync(Guid userId, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting recently added flashcards for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetMostReviewedAsync(Guid userId, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.OrderByDescending(f => f.TimesReviewed)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting most reviewed flashcards for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 統計查詢
|
||||
|
||||
public async Task<int> GetTotalFlashcardsCountAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.CountAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting total flashcards count for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetMasteredFlashcardsCountAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(f => f.UserId == userId && !f.IsArchived && f.MasteryLevel >= 5)
|
||||
.CountAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting mastered flashcards count for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, int>> GetFlashcardsByDifficultyStatsAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.GroupBy(f => f.DifficultyLevel)
|
||||
.Select(g => new { Level = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Level ?? "Unknown", x => x.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards difficulty stats for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 搜尋功能
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> SearchFlashcardsAsync(Guid userId, string searchTerm)
|
||||
{
|
||||
try
|
||||
{
|
||||
var term = searchTerm.ToLower();
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId
|
||||
&& !f.IsArchived
|
||||
&& (f.Word.ToLower().Contains(term)
|
||||
|| f.Translation.ToLower().Contains(term)
|
||||
|| (f.Definition != null && f.Definition.ToLower().Contains(term))
|
||||
|| (f.Example != null && f.Example.ToLower().Contains(term))))
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching flashcards for user: {UserId}, term: {SearchTerm}", userId, searchTerm);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetFavoriteFlashcardsAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && f.IsFavorite && !f.IsArchived)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting favorite flashcards for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetArchivedFlashcardsAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _dbSet
|
||||
.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && f.IsArchived)
|
||||
.OrderByDescending(f => f.UpdatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting archived flashcards for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 批次操作
|
||||
|
||||
public async Task<bool> BulkUpdateMasteryLevelAsync(IEnumerable<Guid> flashcardIds, int newMasteryLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var idList = flashcardIds.ToList();
|
||||
var flashcards = await _dbSet
|
||||
.Where(f => idList.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var flashcard in flashcards)
|
||||
{
|
||||
flashcard.MasteryLevel = newMasteryLevel;
|
||||
flashcard.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_dbSet.UpdateRange(flashcards);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error bulk updating mastery level for flashcards: {FlashcardIds}",
|
||||
string.Join(",", flashcardIds));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> BulkUpdateNextReviewDateAsync(IEnumerable<Guid> flashcardIds, DateTime newDate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var idList = flashcardIds.ToList();
|
||||
var flashcards = await _dbSet
|
||||
.Where(f => idList.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var flashcard in flashcards)
|
||||
{
|
||||
flashcard.NextReviewDate = newDate;
|
||||
flashcard.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_dbSet.UpdateRange(flashcards);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error bulk updating next review date for flashcards: {FlashcardIds}",
|
||||
string.Join(",", flashcardIds));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 性能優化查詢
|
||||
|
||||
public async Task<IEnumerable<Flashcard>> GetFlashcardsWithIncludesAsync(Guid userId,
|
||||
bool includeTags = false,
|
||||
bool includeStudyRecords = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = _dbSet.AsNoTracking()
|
||||
.Where(f => f.UserId == userId && !f.IsArchived);
|
||||
|
||||
if (includeTags)
|
||||
{
|
||||
query = query.Include(f => f.FlashcardTags!)
|
||||
.ThenInclude(ft => ft.Tag);
|
||||
}
|
||||
|
||||
if (includeStudyRecords)
|
||||
{
|
||||
query = query.Include(f => f.StudyRecords!.OrderByDescending(sr => sr.StudiedAt).Take(10));
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards with includes for user: {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using DramaLing.Api.Models.Entities;
|
||||
|
||||
namespace DramaLing.Api.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Flashcard 專門的 Repository 介面,包含業務特定的查詢方法
|
||||
/// </summary>
|
||||
public interface IFlashcardRepository : IRepository<Flashcard>
|
||||
{
|
||||
// 用戶相關查詢
|
||||
Task<IEnumerable<Flashcard>> GetFlashcardsByUserIdAsync(Guid userId);
|
||||
Task<IEnumerable<Flashcard>> GetFlashcardsByCardSetIdAsync(Guid cardSetId);
|
||||
|
||||
// 學習相關查詢
|
||||
Task<IEnumerable<Flashcard>> GetDueFlashcardsAsync(Guid userId, DateTime dueDate);
|
||||
Task<IEnumerable<Flashcard>> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel);
|
||||
Task<IEnumerable<Flashcard>> GetRecentlyAddedAsync(Guid userId, int count);
|
||||
Task<IEnumerable<Flashcard>> GetMostReviewedAsync(Guid userId, int count);
|
||||
|
||||
// 統計查詢
|
||||
Task<int> GetTotalFlashcardsCountAsync(Guid userId);
|
||||
Task<int> GetMasteredFlashcardsCountAsync(Guid userId);
|
||||
Task<Dictionary<string, int>> GetFlashcardsByDifficultyStatsAsync(Guid userId);
|
||||
|
||||
// 搜尋功能
|
||||
Task<IEnumerable<Flashcard>> SearchFlashcardsAsync(Guid userId, string searchTerm);
|
||||
Task<IEnumerable<Flashcard>> GetFavoriteFlashcardsAsync(Guid userId);
|
||||
Task<IEnumerable<Flashcard>> GetArchivedFlashcardsAsync(Guid userId);
|
||||
|
||||
// 批次操作
|
||||
Task<bool> BulkUpdateMasteryLevelAsync(IEnumerable<Guid> flashcardIds, int newMasteryLevel);
|
||||
Task<bool> BulkUpdateNextReviewDateAsync(IEnumerable<Guid> flashcardIds, DateTime newDate);
|
||||
|
||||
// 性能優化查詢
|
||||
Task<IEnumerable<Flashcard>> GetFlashcardsWithIncludesAsync(Guid userId,
|
||||
bool includeTags = false,
|
||||
bool includeStudyRecords = false);
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
using DramaLing.Api.Models.Entities;
|
||||
using DramaLing.Api.Models.DTOs;
|
||||
|
||||
namespace DramaLing.Api.Services.Domain.Learning;
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡服務介面,封裝詞卡相關的業務邏輯
|
||||
/// </summary>
|
||||
public interface IFlashcardService
|
||||
{
|
||||
// 基本 CRUD 操作
|
||||
Task<FlashcardDto> CreateFlashcardAsync(CreateFlashcardRequest request);
|
||||
Task<FlashcardDto?> GetFlashcardAsync(Guid flashcardId, Guid userId);
|
||||
Task<FlashcardDto> UpdateFlashcardAsync(Guid flashcardId, UpdateFlashcardRequest request);
|
||||
Task<bool> DeleteFlashcardAsync(Guid flashcardId, Guid userId);
|
||||
|
||||
// 查詢操作
|
||||
Task<IEnumerable<FlashcardDto>> GetUserFlashcardsAsync(Guid userId, FlashcardQueryOptions? options = null);
|
||||
Task<IEnumerable<FlashcardDto>> GetDueFlashcardsAsync(Guid userId, int limit = 20);
|
||||
Task<IEnumerable<FlashcardDto>> GetFlashcardsByDifficultyAsync(Guid userId, string difficultyLevel);
|
||||
Task<IEnumerable<FlashcardDto>> SearchFlashcardsAsync(Guid userId, string searchTerm);
|
||||
|
||||
// 學習相關操作
|
||||
Task<StudyRecommendations> GetStudyRecommendationsAsync(Guid userId);
|
||||
Task<bool> UpdateMasteryLevelAsync(Guid flashcardId, int masteryLevel, Guid userId);
|
||||
Task<bool> MarkAsReviewedAsync(Guid flashcardId, StudyResult result, Guid userId);
|
||||
|
||||
// 批次操作
|
||||
Task<IEnumerable<FlashcardDto>> CreateFlashcardsFromAnalysisAsync(SentenceAnalysisData analysis, Guid userId);
|
||||
Task<bool> BulkUpdateMasteryAsync(IEnumerable<Guid> flashcardIds, int masteryLevel, Guid userId);
|
||||
|
||||
// 統計功能
|
||||
Task<FlashcardStats> GetFlashcardStatsAsync(Guid userId);
|
||||
Task<LearningProgress> GetLearningProgressAsync(Guid userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡查詢選項
|
||||
/// </summary>
|
||||
public class FlashcardQueryOptions
|
||||
{
|
||||
public int? Limit { get; set; }
|
||||
public int? Offset { get; set; }
|
||||
public string? SortBy { get; set; } = "CreatedAt";
|
||||
public bool SortDescending { get; set; } = true;
|
||||
public bool? IsFavorite { get; set; }
|
||||
public bool? IsArchived { get; set; }
|
||||
public string? DifficultyLevel { get; set; }
|
||||
public Guid? CardSetId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 學習推薦
|
||||
/// </summary>
|
||||
public class StudyRecommendations
|
||||
{
|
||||
public IEnumerable<FlashcardDto> DueCards { get; set; } = new List<FlashcardDto>();
|
||||
public IEnumerable<FlashcardDto> NewCards { get; set; } = new List<FlashcardDto>();
|
||||
public IEnumerable<FlashcardDto> ReviewCards { get; set; } = new List<FlashcardDto>();
|
||||
public IEnumerable<FlashcardDto> ChallengingCards { get; set; } = new List<FlashcardDto>();
|
||||
public int RecommendedStudyTimeMinutes { get; set; }
|
||||
public string RecommendationReason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 學習結果
|
||||
/// </summary>
|
||||
public class StudyResult
|
||||
{
|
||||
public int QualityRating { get; set; } // 1-5 SM2 算法評分
|
||||
public int ResponseTimeMs { get; set; }
|
||||
public bool IsCorrect { get; set; }
|
||||
public string? UserAnswer { get; set; }
|
||||
public DateTime StudiedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 詞卡統計
|
||||
/// </summary>
|
||||
public class FlashcardStats
|
||||
{
|
||||
public int TotalCards { get; set; }
|
||||
public int MasteredCards { get; set; }
|
||||
public int DueCards { get; set; }
|
||||
public int NewCards { get; set; }
|
||||
public double MasteryRate => TotalCards > 0 ? (double)MasteredCards / TotalCards : 0;
|
||||
public Dictionary<string, int> DifficultyDistribution { get; set; } = new();
|
||||
public TimeSpan AverageStudyTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 學習進度
|
||||
/// </summary>
|
||||
public class LearningProgress
|
||||
{
|
||||
public int ConsecutiveDays { get; set; }
|
||||
public int TotalStudyDays { get; set; }
|
||||
public int WordsLearned { get; set; }
|
||||
public int WordsMastered { get; set; }
|
||||
public string CurrentLevel { get; set; } = "A2";
|
||||
public double ProgressToNextLevel { get; set; }
|
||||
public DateTime LastStudyDate { get; set; }
|
||||
public IEnumerable<DailyProgress> RecentProgress { get; set; } = new List<DailyProgress>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每日進度
|
||||
/// </summary>
|
||||
public class DailyProgress
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int CardsStudied { get; set; }
|
||||
public int CorrectAnswers { get; set; }
|
||||
public TimeSpan StudyTime { get; set; }
|
||||
public double AccuracyRate => CardsStudied > 0 ? (double)CorrectAnswers / CardsStudied : 0;
|
||||
}
|
||||
Loading…
Reference in New Issue