feat: 完成Controllers架構統一優化與後端重啟修復
主要改進: - 🏗️ 新增BaseController統一響應處理架構 - 標準化SuccessResponse和ErrorResponse格式 - 統一GetCurrentUserIdAsync認證處理 - 統一HandleModelStateErrors驗證錯誤處理 - 🔧 重構FlashcardsController使用BaseController - 所有返回類型改為IActionResult統一格式 - 完整的異常處理與錯誤回應 - 移除重複的用戶ID獲取邏輯 - 🛠️ 修復依賴注入配置問題 - 使用ServiceCollectionExtensions組織服務註冊 - 修復ICacheProvider和IImageGenerationWorkflow缺失問題 - 清理重複的服務註冊,提升代碼可維護性 - 🐛 解決編譯錯誤 - 修復GeminiOptionsValidator nullable警告 - 排除測試文件避免編譯衝突 - 確保所有依賴正確註冊 後端現已成功重啟並運行在 http://localhost:5008 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2a6c130bb8
commit
923ce16f5f
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 統一的成功響應格式
|
||||
/// </summary>
|
||||
protected IActionResult SuccessResponse<T>(T data, string? message = null)
|
||||
{
|
||||
return Ok(new ApiResponse<T>
|
||||
{
|
||||
Success = true,
|
||||
Data = data,
|
||||
Message = message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 統一的錯誤響應格式
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 獲取當前用戶ID(統一處理認證)
|
||||
/// </summary>
|
||||
protected async Task<Guid> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 檢查是否為測試環境
|
||||
/// </summary>
|
||||
protected bool IsTestEnvironment()
|
||||
{
|
||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
return environment == "Development" || environment == "Testing";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 統一的模型驗證錯誤處理
|
||||
/// </summary>
|
||||
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<string>()
|
||||
);
|
||||
|
||||
return ErrorResponse("VALIDATION_ERROR", "輸入資料驗證失敗", errors, 400);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根據錯誤代碼獲取建議
|
||||
/// </summary>
|
||||
private List<string> GetSuggestionsForError(string errorCode)
|
||||
{
|
||||
return errorCode switch
|
||||
{
|
||||
"VALIDATION_ERROR" => new List<string> { "請檢查輸入格式", "確保所有必填欄位已填寫" },
|
||||
"INVALID_INPUT" => new List<string> { "請檢查輸入格式", "確保文本長度在限制內" },
|
||||
"RATE_LIMIT_EXCEEDED" => new List<string> { "升級到Premium帳戶以獲得無限使用", "明天重新嘗試" },
|
||||
"AI_SERVICE_ERROR" => new List<string> { "請稍後重試", "如果問題持續,請聯繫客服" },
|
||||
"UNAUTHORIZED" => new List<string> { "請檢查登入狀態", "確認Token是否有效" },
|
||||
"NOT_FOUND" => new List<string> { "請檢查資源ID是否正確", "確認資源是否存在" },
|
||||
_ => new List<string> { "請稍後重試", "如果問題持續,請聯繫客服" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 統一API響應格式
|
||||
/// </summary>
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; } = true;
|
||||
public T? Data { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分頁響應格式
|
||||
/// </summary>
|
||||
public class PagedApiResponse<T> : ApiResponse<List<T>>
|
||||
{
|
||||
public PaginationMetadata Pagination { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分頁元數據
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
|
@ -5,42 +5,30 @@ 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<FlashcardsController> _logger;
|
||||
|
||||
public FlashcardsController(
|
||||
IFlashcardRepository flashcardRepository,
|
||||
ILogger<FlashcardsController> logger)
|
||||
ILogger<FlashcardsController> logger) : base(logger)
|
||||
{
|
||||
_flashcardRepository = flashcardRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private Guid GetUserId()
|
||||
{
|
||||
// 暫時使用固定測試用戶 ID
|
||||
return Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetFlashcards(
|
||||
public async Task<IActionResult> 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
|
||||
{
|
||||
Success = true,
|
||||
Data = new
|
||||
var flashcardData = new
|
||||
{
|
||||
Flashcards = flashcards.Select(f => new
|
||||
{
|
||||
|
|
@ -58,22 +46,32 @@ public class FlashcardsController : ControllerBase
|
|||
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<ActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
|
||||
public async Task<IActionResult> 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
|
||||
return SuccessResponse(flashcard, "詞卡創建成功");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
Success = true,
|
||||
Data = flashcard,
|
||||
Message = "詞卡創建成功"
|
||||
});
|
||||
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<ActionResult> GetFlashcard(Guid id)
|
||||
public async Task<IActionResult> 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<ActionResult> UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
|
||||
public async Task<IActionResult> 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
|
||||
return SuccessResponse(flashcard, "詞卡更新成功");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
Success = true,
|
||||
Data = flashcard,
|
||||
Message = "詞卡更新成功"
|
||||
});
|
||||
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<ActionResult> DeleteFlashcard(Guid id)
|
||||
public async Task<IActionResult> 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<ActionResult> ToggleFavorite(Guid id)
|
||||
public async Task<IActionResult> 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", "切換收藏狀態失敗");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,8 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="DramaLing.Api.Tests/**/*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -5,7 +5,7 @@ namespace DramaLing.Api.Models.Configuration;
|
|||
|
||||
public class GeminiOptionsValidator : IValidateOptions<GeminiOptions>
|
||||
{
|
||||
public ValidateOptionsResult Validate(string name, GeminiOptions options)
|
||||
public ValidateOptionsResult Validate(string? name, GeminiOptions options)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DramaLingDbContext>(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<DramaLingDbContext>(options =>
|
||||
options.UseSqlite(connectionString));
|
||||
}
|
||||
// 配置 Repository 和 Caching 服務
|
||||
builder.Services.AddRepositoryServices();
|
||||
builder.Services.AddCachingServices();
|
||||
|
||||
// 暫時註解新的服務,等修正編譯錯誤後再啟用
|
||||
// Repository Services
|
||||
// builder.Services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
|
||||
// builder.Services.AddScoped<IFlashcardRepository, SimpleFlashcardRepository>();
|
||||
// builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
// Caching Services - now using Extension method
|
||||
// builder.Services.AddMemoryCache();
|
||||
// builder.Services.AddScoped<ICacheService, HybridCacheService>();
|
||||
|
||||
// AI Services
|
||||
// builder.Services.AddHttpClient<GeminiAIProvider>();
|
||||
// builder.Services.AddScoped<IAIProvider, GeminiAIProvider>();
|
||||
// builder.Services.AddScoped<IAIProviderManager, AIProviderManager>();
|
||||
|
||||
// Custom Services
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddHttpClient<IGeminiService, GeminiService>();
|
||||
// 新增帶快取的分析服務
|
||||
builder.Services.AddScoped<IAnalysisService, AnalysisService>();
|
||||
builder.Services.AddScoped<IUsageTrackingService, UsageTrackingService>();
|
||||
builder.Services.AddScoped<IAzureSpeechService, AzureSpeechService>();
|
||||
// 智能填空題系統服務已移除
|
||||
builder.Services.AddScoped<IAudioCacheService, AudioCacheService>();
|
||||
|
||||
// 智能複習服務已移除,準備重新實施
|
||||
|
||||
// 學習會話服務已清理移除
|
||||
// 配置 AI 和業務服務
|
||||
builder.Services.AddAIServices(builder.Configuration);
|
||||
builder.Services.AddBusinessServices();
|
||||
|
||||
// 🆕 選項詞彙庫服務註冊
|
||||
builder.Services.Configure<OptionsVocabularyOptions>(
|
||||
|
|
@ -103,15 +68,7 @@ builder.Services.AddSingleton<OptionsVocabularyMetrics>(); // 監控指標服務
|
|||
// builder.Services.AddScoped<OptionsVocabularySeeder>(); // 暫時註解,使用固定選項
|
||||
builder.Services.AddScoped<IOptionsVocabularyService, OptionsVocabularyService>();
|
||||
|
||||
// Image Generation Services
|
||||
builder.Services.AddHttpClient<IReplicateService, ReplicateService>();
|
||||
builder.Services.AddScoped<IImageGenerationOrchestrator, ImageGenerationOrchestrator>();
|
||||
|
||||
// Image Storage Services
|
||||
builder.Services.AddScoped<IImageStorageService, LocalImageStorageService>();
|
||||
|
||||
// Image Processing Services
|
||||
builder.Services.AddScoped<IImageProcessingService, ImageProcessingService>();
|
||||
// (圖片相關服務已透過 AddAIServices 和 AddBusinessServices 註冊)
|
||||
|
||||
// Background Services (快取清理服務已移除)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue