diff --git a/backend/DramaLing.Api/Controllers/BaseController.cs b/backend/DramaLing.Api/Controllers/BaseController.cs
new file mode 100644
index 0000000..5b13cd3
--- /dev/null
+++ b/backend/DramaLing.Api/Controllers/BaseController.cs
@@ -0,0 +1,157 @@
+using Microsoft.AspNetCore.Mvc;
+using DramaLing.Api.Models.DTOs;
+using DramaLing.Api.Services;
+using System.Security.Claims;
+
+namespace DramaLing.Api.Controllers;
+
+[ApiController]
+public abstract class BaseController : ControllerBase
+{
+ protected readonly ILogger _logger;
+ protected readonly IAuthService? _authService;
+
+ protected BaseController(ILogger logger, IAuthService? authService = null)
+ {
+ _logger = logger;
+ _authService = authService;
+ }
+
+ ///
+ /// 統一的成功響應格式
+ ///
+ protected IActionResult SuccessResponse(T data, string? message = null)
+ {
+ return Ok(new ApiResponse
+ {
+ Success = true,
+ Data = data,
+ Message = message,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
+ ///
+ /// 統一的錯誤響應格式
+ ///
+ protected IActionResult ErrorResponse(string code, string message, object? details = null, int statusCode = 500)
+ {
+ var response = new ApiErrorResponse
+ {
+ Success = false,
+ Error = new ApiError
+ {
+ Code = code,
+ Message = message,
+ Details = details,
+ Suggestions = GetSuggestionsForError(code)
+ },
+ RequestId = Guid.NewGuid().ToString(),
+ Timestamp = DateTime.UtcNow
+ };
+
+ return StatusCode(statusCode, response);
+ }
+
+ ///
+ /// 獲取當前用戶ID(統一處理認證)
+ ///
+ protected async Task GetCurrentUserIdAsync()
+ {
+ if (_authService != null)
+ {
+ // 使用AuthService進行JWT解析(適用於已實現認證的Controller)
+ var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization);
+ if (userId.HasValue)
+ return userId.Value;
+ }
+
+ // Fallback: 從Claims直接解析
+ var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
+ User.FindFirst("sub")?.Value;
+
+ if (Guid.TryParse(userIdString, out var parsedUserId))
+ return parsedUserId;
+
+ // 開發階段:使用固定測試用戶ID
+ if (IsTestEnvironment())
+ {
+ return Guid.Parse("00000000-0000-0000-0000-000000000001");
+ }
+
+ throw new UnauthorizedAccessException("Invalid or missing user authentication");
+ }
+
+ ///
+ /// 檢查是否為測試環境
+ ///
+ protected bool IsTestEnvironment()
+ {
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ return environment == "Development" || environment == "Testing";
+ }
+
+ ///
+ /// 統一的模型驗證錯誤處理
+ ///
+ protected IActionResult HandleModelStateErrors()
+ {
+ var errors = ModelState
+ .Where(x => x.Value?.Errors.Count > 0)
+ .ToDictionary(
+ kvp => kvp.Key,
+ kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty()
+ );
+
+ return ErrorResponse("VALIDATION_ERROR", "輸入資料驗證失敗", errors, 400);
+ }
+
+ ///
+ /// 根據錯誤代碼獲取建議
+ ///
+ private List GetSuggestionsForError(string errorCode)
+ {
+ return errorCode switch
+ {
+ "VALIDATION_ERROR" => new List { "請檢查輸入格式", "確保所有必填欄位已填寫" },
+ "INVALID_INPUT" => new List { "請檢查輸入格式", "確保文本長度在限制內" },
+ "RATE_LIMIT_EXCEEDED" => new List { "升級到Premium帳戶以獲得無限使用", "明天重新嘗試" },
+ "AI_SERVICE_ERROR" => new List { "請稍後重試", "如果問題持續,請聯繫客服" },
+ "UNAUTHORIZED" => new List { "請檢查登入狀態", "確認Token是否有效" },
+ "NOT_FOUND" => new List { "請檢查資源ID是否正確", "確認資源是否存在" },
+ _ => new List { "請稍後重試", "如果問題持續,請聯繫客服" }
+ };
+ }
+}
+
+///
+/// 統一API響應格式
+///
+public class ApiResponse
+{
+ public bool Success { get; set; } = true;
+ public T? Data { get; set; }
+ public string? Message { get; set; }
+ public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+}
+
+///
+/// 分頁響應格式
+///
+public class PagedApiResponse : ApiResponse>
+{
+ public PaginationMetadata Pagination { get; set; } = new();
+}
+
+///
+/// 分頁元數據
+///
+public class PaginationMetadata
+{
+ public int CurrentPage { get; set; }
+ public int PageSize { get; set; }
+ public int TotalCount { get; set; }
+ public int TotalPages { get; set; }
+ public bool HasNext { get; set; }
+ public bool HasPrevious { get; set; }
+}
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs
index 00d2aa1..d1f8841 100644
--- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs
+++ b/backend/DramaLing.Api/Controllers/FlashcardsController.cs
@@ -5,75 +5,73 @@ using Microsoft.AspNetCore.Authorization;
namespace DramaLing.Api.Controllers;
-[ApiController]
[Route("api/flashcards")]
[AllowAnonymous]
-public class FlashcardsController : ControllerBase
+public class FlashcardsController : BaseController
{
private readonly IFlashcardRepository _flashcardRepository;
- private readonly ILogger _logger;
public FlashcardsController(
IFlashcardRepository flashcardRepository,
- ILogger logger)
+ ILogger logger) : base(logger)
{
_flashcardRepository = flashcardRepository;
- _logger = logger;
- }
-
- private Guid GetUserId()
- {
- // 暫時使用固定測試用戶 ID
- return Guid.Parse("00000000-0000-0000-0000-000000000001");
}
[HttpGet]
- public async Task GetFlashcards(
+ public async Task GetFlashcards(
[FromQuery] string? search = null,
[FromQuery] bool favoritesOnly = false)
{
try
{
- var userId = GetUserId();
+ var userId = await GetCurrentUserIdAsync();
var flashcards = await _flashcardRepository.GetByUserIdAsync(userId, search, favoritesOnly);
- return Ok(new
+ var flashcardData = new
{
- Success = true,
- Data = new
+ Flashcards = flashcards.Select(f => new
{
- Flashcards = flashcards.Select(f => new
- {
- f.Id,
- f.Word,
- f.Translation,
- f.Definition,
- f.PartOfSpeech,
- f.Pronunciation,
- f.Example,
- f.ExampleTranslation,
- f.IsFavorite,
- f.DifficultyLevel,
- f.CreatedAt,
- f.UpdatedAt
- }),
- Count = flashcards.Count()
- }
- });
+ f.Id,
+ f.Word,
+ f.Translation,
+ f.Definition,
+ f.PartOfSpeech,
+ f.Pronunciation,
+ f.Example,
+ f.ExampleTranslation,
+ f.IsFavorite,
+ f.DifficultyLevel,
+ f.CreatedAt,
+ f.UpdatedAt
+ }),
+ Count = flashcards.Count()
+ };
+
+ return SuccessResponse(flashcardData);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting flashcards");
- return StatusCode(500, new { Success = false, Error = "Failed to load flashcards" });
+ return ErrorResponse("INTERNAL_ERROR", "載入詞卡失敗");
}
}
[HttpPost]
- public async Task CreateFlashcard([FromBody] CreateFlashcardRequest request)
+ public async Task CreateFlashcard([FromBody] CreateFlashcardRequest request)
{
try
{
- var userId = GetUserId();
+ if (!ModelState.IsValid)
+ {
+ return HandleModelStateErrors();
+ }
+
+ var userId = await GetCurrentUserIdAsync();
var flashcard = new Flashcard
{
@@ -93,55 +91,63 @@ public class FlashcardsController : ControllerBase
await _flashcardRepository.AddAsync(flashcard);
- return Ok(new
- {
- Success = true,
- Data = flashcard,
- Message = "詞卡創建成功"
- });
+ return SuccessResponse(flashcard, "詞卡創建成功");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating flashcard");
- return StatusCode(500, new { Success = false, Error = "Failed to create flashcard" });
+ return ErrorResponse("INTERNAL_ERROR", "創建詞卡失敗");
}
}
[HttpGet("{id}")]
- public async Task GetFlashcard(Guid id)
+ public async Task GetFlashcard(Guid id)
{
try
{
- var userId = GetUserId();
+ var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
- return NotFound(new { Success = false, Error = "Flashcard not found" });
+ return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
- return Ok(new { Success = true, Data = flashcard });
+ return SuccessResponse(flashcard);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting flashcard {FlashcardId}", id);
- return StatusCode(500, new { Success = false, Error = "Failed to get flashcard" });
+ return ErrorResponse("INTERNAL_ERROR", "取得詞卡失敗");
}
}
[HttpPut("{id}")]
- public async Task UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
+ public async Task UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
{
try
{
- var userId = GetUserId();
+ if (!ModelState.IsValid)
+ {
+ return HandleModelStateErrors();
+ }
+
+ var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
- return NotFound(new { Success = false, Error = "Flashcard not found" });
+ return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
// 更新詞卡資訊
@@ -156,57 +162,60 @@ public class FlashcardsController : ControllerBase
await _flashcardRepository.UpdateAsync(flashcard);
- return Ok(new
- {
- Success = true,
- Data = flashcard,
- Message = "詞卡更新成功"
- });
+ return SuccessResponse(flashcard, "詞卡更新成功");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating flashcard {FlashcardId}", id);
- return StatusCode(500, new { Success = false, Error = "Failed to update flashcard" });
+ return ErrorResponse("INTERNAL_ERROR", "更新詞卡失敗");
}
}
[HttpDelete("{id}")]
- public async Task DeleteFlashcard(Guid id)
+ public async Task DeleteFlashcard(Guid id)
{
try
{
- var userId = GetUserId();
+ var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
- return NotFound(new { Success = false, Error = "Flashcard not found" });
+ return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
await _flashcardRepository.DeleteAsync(flashcard);
- return Ok(new { Success = true, Message = "詞卡已刪除" });
+ return SuccessResponse(new { Id = id }, "詞卡已刪除");
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting flashcard {FlashcardId}", id);
- return StatusCode(500, new { Success = false, Error = "Failed to delete flashcard" });
+ return ErrorResponse("INTERNAL_ERROR", "刪除詞卡失敗");
}
}
[HttpPost("{id}/favorite")]
- public async Task ToggleFavorite(Guid id)
+ public async Task ToggleFavorite(Guid id)
{
try
{
- var userId = GetUserId();
+ var userId = await GetCurrentUserIdAsync();
var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id);
if (flashcard == null)
{
- return NotFound(new { Success = false, Error = "Flashcard not found" });
+ return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404);
}
flashcard.IsFavorite = !flashcard.IsFavorite;
@@ -214,16 +223,22 @@ public class FlashcardsController : ControllerBase
await _flashcardRepository.UpdateAsync(flashcard);
- return Ok(new {
- Success = true,
- IsFavorite = flashcard.IsFavorite,
- Message = flashcard.IsFavorite ? "已加入收藏" : "已取消收藏"
- });
+ 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 StatusCode(500, new { Success = false, Error = "Failed to toggle favorite" });
+ return ErrorResponse("INTERNAL_ERROR", "切換收藏狀態失敗");
}
}
}
diff --git a/backend/DramaLing.Api/DramaLing.Api.csproj b/backend/DramaLing.Api/DramaLing.Api.csproj
index 4ed5f7e..c0d1354 100644
--- a/backend/DramaLing.Api/DramaLing.Api.csproj
+++ b/backend/DramaLing.Api/DramaLing.Api.csproj
@@ -24,4 +24,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs b/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs
index fb6a323..bd0536a 100644
--- a/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs
+++ b/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs
@@ -5,7 +5,7 @@ namespace DramaLing.Api.Models.Configuration;
public class GeminiOptionsValidator : IValidateOptions
{
- public ValidateOptionsResult Validate(string name, GeminiOptions options)
+ public ValidateOptionsResult Validate(string? name, GeminiOptions options)
{
var failures = new List();
diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs
index 37ebeeb..ac7f499 100644
--- a/backend/DramaLing.Api/Program.cs
+++ b/backend/DramaLing.Api/Program.cs
@@ -1,13 +1,13 @@
using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Services;
-// Services.AI namespace removed
using DramaLing.Api.Services.Caching;
using DramaLing.Api.Services.Monitoring;
using DramaLing.Api.Services.Storage;
using DramaLing.Api.Middleware;
using DramaLing.Api.Models.Configuration;
using DramaLing.Api.Repositories;
+using DramaLing.Api.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Options;
@@ -49,51 +49,16 @@ builder.Services.AddControllers()
options.JsonSerializerOptions.WriteIndented = true;
});
-// Entity Framework - 使用 SQLite 進行測試
-var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
-if (useInMemoryDb)
-{
- builder.Services.AddDbContext(options =>
- options.UseSqlite("Data Source=:memory:"));
-}
-else
-{
- var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION")
- ?? builder.Configuration.GetConnectionString("DefaultConnection")
- ?? "Data Source=dramaling_test.db"; // SQLite 檔案
+// 配置資料庫服務
+builder.Services.AddDatabaseServices(builder.Configuration);
- builder.Services.AddDbContext(options =>
- options.UseSqlite(connectionString));
-}
+// 配置 Repository 和 Caching 服務
+builder.Services.AddRepositoryServices();
+builder.Services.AddCachingServices();
-// 暫時註解新的服務,等修正編譯錯誤後再啟用
-// Repository Services
-// builder.Services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
-// builder.Services.AddScoped();
-// builder.Services.AddScoped();
-
-// Caching Services - now using Extension method
-// builder.Services.AddMemoryCache();
-// builder.Services.AddScoped();
-
-// AI Services
-// builder.Services.AddHttpClient();
-// builder.Services.AddScoped();
-// builder.Services.AddScoped();
-
-// Custom Services
-builder.Services.AddScoped();
-builder.Services.AddHttpClient();
-// 新增帶快取的分析服務
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-builder.Services.AddScoped();
-// 智能填空題系統服務已移除
-builder.Services.AddScoped();
-
-// 智能複習服務已移除,準備重新實施
-
-// 學習會話服務已清理移除
+// 配置 AI 和業務服務
+builder.Services.AddAIServices(builder.Configuration);
+builder.Services.AddBusinessServices();
// 🆕 選項詞彙庫服務註冊
builder.Services.Configure(
@@ -103,15 +68,7 @@ builder.Services.AddSingleton(); // 監控指標服務
// builder.Services.AddScoped(); // 暫時註解,使用固定選項
builder.Services.AddScoped();
-// Image Generation Services
-builder.Services.AddHttpClient();
-builder.Services.AddScoped();
-
-// Image Storage Services
-builder.Services.AddScoped();
-
-// Image Processing Services
-builder.Services.AddScoped();
+// (圖片相關服務已透過 AddAIServices 和 AddBusinessServices 註冊)
// Background Services (快取清理服務已移除)