diff --git a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs index b43cede..ea29700 100644 --- a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs +++ b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Services; -using DramaLing.Api.Services.AI; +// Services.AI namespace removed using DramaLing.Api.Services.Caching; using DramaLing.Api.Repositories; using DramaLing.Api.Models.Configuration; @@ -73,10 +73,7 @@ public static class ServiceCollectionExtensions services.Configure(configuration.GetSection(GeminiOptions.SectionName)); services.AddSingleton, GeminiOptionsValidator>(); - // AI 提供商服務 - services.AddHttpClient(); - services.AddScoped(); - services.AddScoped(); + // AI 提供商服務已移除 (使用 GeminiService 替代) // 舊的 Gemini 服務 (向後相容) services.AddHttpClient(); @@ -94,9 +91,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); - // 智能填空題系統服務 - services.AddScoped(); - services.AddScoped(); + // 智能填空題系統服務已移除 return services; } diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs index d23cda8..516be82 100644 --- a/backend/DramaLing.Api/Program.cs +++ b/backend/DramaLing.Api/Program.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Services; -using DramaLing.Api.Services.AI; +// Services.AI namespace removed using DramaLing.Api.Services.Caching; using DramaLing.Api.Services.Monitoring; using DramaLing.Api.Services.Storage; diff --git a/backend/DramaLing.Api/Services/AI/AIProviderManager.cs b/backend/DramaLing.Api/Services/AI/AIProviderManager.cs deleted file mode 100644 index 718b6e5..0000000 --- a/backend/DramaLing.Api/Services/AI/AIProviderManager.cs +++ /dev/null @@ -1,260 +0,0 @@ -using DramaLing.Api.Models.DTOs; - -namespace DramaLing.Api.Services.AI; - -/// -/// AI 提供商管理器實作 -/// -public class AIProviderManager : IAIProviderManager -{ - private readonly IEnumerable _providers; - private readonly ILogger _logger; - private readonly Random _random = new(); - - public AIProviderManager(IEnumerable providers, ILogger logger) - { - _providers = providers ?? throw new ArgumentNullException(nameof(providers)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - _logger.LogInformation("AIProviderManager initialized with {ProviderCount} providers: {ProviderNames}", - _providers.Count(), string.Join(", ", _providers.Select(p => p.ProviderName))); - } - - public async Task GetBestProviderAsync(ProviderSelectionStrategy strategy = ProviderSelectionStrategy.Performance) - { - var availableProviders = await GetAvailableProvidersAsync(); - - if (!availableProviders.Any()) - { - throw new InvalidOperationException("No AI providers are available"); - } - - var selectedProvider = strategy switch - { - ProviderSelectionStrategy.Performance => await SelectByPerformanceAsync(availableProviders), - ProviderSelectionStrategy.Cost => SelectByCost(availableProviders), - ProviderSelectionStrategy.Reliability => await SelectByReliabilityAsync(availableProviders), - ProviderSelectionStrategy.LoadBalance => SelectByLoadBalance(availableProviders), - ProviderSelectionStrategy.Primary => SelectPrimary(availableProviders), - _ => availableProviders.First() - }; - - _logger.LogDebug("Selected AI provider: {ProviderName} using strategy: {Strategy}", - selectedProvider.ProviderName, strategy); - - return selectedProvider; - } - - public async Task> GetAvailableProvidersAsync() - { - var availableProviders = new List(); - - foreach (var provider in _providers) - { - try - { - if (provider.IsAvailable) - { - var healthStatus = await provider.CheckHealthAsync(); - if (healthStatus.IsHealthy) - { - availableProviders.Add(provider); - } - else - { - _logger.LogWarning("Provider {ProviderName} is not healthy: {Error}", - provider.ProviderName, healthStatus.ErrorMessage); - } - } - else - { - _logger.LogWarning("Provider {ProviderName} is not available", provider.ProviderName); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking provider {ProviderName} availability", provider.ProviderName); - } - } - - return availableProviders; - } - - public async Task GetProviderByNameAsync(string providerName) - { - var provider = _providers.FirstOrDefault(p => p.ProviderName.Equals(providerName, StringComparison.OrdinalIgnoreCase)); - - if (provider != null && provider.IsAvailable) - { - try - { - var healthStatus = await provider.CheckHealthAsync(); - if (healthStatus.IsHealthy) - { - return provider; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking provider {ProviderName} health", providerName); - } - } - - return null; - } - - public async Task CheckAllProvidersHealthAsync() - { - var report = new ProviderHealthReport - { - CheckedAt = DateTime.UtcNow, - TotalProviders = _providers.Count() - }; - - var healthTasks = _providers.Select(async provider => - { - try - { - var healthStatus = await provider.CheckHealthAsync(); - var stats = await provider.GetStatsAsync(); - - return new ProviderHealthInfo - { - ProviderName = provider.ProviderName, - IsHealthy = healthStatus.IsHealthy, - ResponseTimeMs = healthStatus.ResponseTimeMs, - ErrorMessage = healthStatus.ErrorMessage, - Stats = stats - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking health for provider {ProviderName}", provider.ProviderName); - return new ProviderHealthInfo - { - ProviderName = provider.ProviderName, - IsHealthy = false, - ErrorMessage = ex.Message, - Stats = new AIProviderStats() - }; - } - }); - - report.ProviderHealthInfos = (await Task.WhenAll(healthTasks)).ToList(); - report.HealthyProviders = report.ProviderHealthInfos.Count(p => p.IsHealthy); - - return report; - } - - public async Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options, - ProviderSelectionStrategy strategy = ProviderSelectionStrategy.Performance) - { - var provider = await GetBestProviderAsync(strategy); - - try - { - var result = await provider.AnalyzeSentenceAsync(inputText, options); - _logger.LogInformation("Sentence analyzed successfully using provider: {ProviderName}", provider.ProviderName); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error analyzing sentence with provider {ProviderName}, attempting fallback", - provider.ProviderName); - - // 嘗試使用其他可用的提供商 - var availableProviders = (await GetAvailableProvidersAsync()) - .Where(p => p.ProviderName != provider.ProviderName) - .ToList(); - - foreach (var fallbackProvider in availableProviders) - { - try - { - var result = await fallbackProvider.AnalyzeSentenceAsync(inputText, options); - _logger.LogWarning("Fallback successful using provider: {ProviderName}", fallbackProvider.ProviderName); - return result; - } - catch (Exception fallbackEx) - { - _logger.LogError(fallbackEx, "Fallback provider {ProviderName} also failed", fallbackProvider.ProviderName); - } - } - - // 如果所有提供商都失敗,重新拋出原始異常 - throw; - } - } - - #region 私有選擇方法 - - private async Task SelectByPerformanceAsync(IEnumerable providers) - { - var providerList = providers.ToList(); - var performanceData = new List<(IAIProvider Provider, int ResponseTime)>(); - - foreach (var provider in providerList) - { - try - { - var stats = await provider.GetStatsAsync(); - performanceData.Add((provider, stats.AverageResponseTimeMs)); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Could not get stats for provider {ProviderName}", provider.ProviderName); - performanceData.Add((provider, int.MaxValue)); // 最低優先級 - } - } - - return performanceData - .OrderBy(p => p.ResponseTime) - .First().Provider; - } - - private IAIProvider SelectByCost(IEnumerable providers) - { - return providers - .OrderBy(p => p.CostPerRequest) - .First(); - } - - private async Task SelectByReliabilityAsync(IEnumerable providers) - { - var providerList = providers.ToList(); - var reliabilityData = new List<(IAIProvider Provider, double SuccessRate)>(); - - foreach (var provider in providerList) - { - try - { - var stats = await provider.GetStatsAsync(); - reliabilityData.Add((provider, stats.SuccessRate)); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Could not get stats for provider {ProviderName}", provider.ProviderName); - reliabilityData.Add((provider, 0.0)); // 最低優先級 - } - } - - return reliabilityData - .OrderByDescending(p => p.SuccessRate) - .First().Provider; - } - - private IAIProvider SelectByLoadBalance(IEnumerable providers) - { - var providerList = providers.ToList(); - var randomIndex = _random.Next(providerList.Count); - return providerList[randomIndex]; - } - - private IAIProvider SelectPrimary(IEnumerable providers) - { - // 使用第一個可用的提供商作為主要提供商 - return providers.First(); - } - - #endregion -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/AI/GeminiAIProvider.cs b/backend/DramaLing.Api/Services/AI/GeminiAIProvider.cs deleted file mode 100644 index 5775378..0000000 --- a/backend/DramaLing.Api/Services/AI/GeminiAIProvider.cs +++ /dev/null @@ -1,482 +0,0 @@ -using DramaLing.Api.Models.DTOs; -using DramaLing.Api.Models.Configuration; -using Microsoft.Extensions.Options; -using System.Text.Json; -using System.Text; -using System.Diagnostics; - -namespace DramaLing.Api.Services.AI; - -/// -/// Google Gemini AI 提供商實作 -/// -public class GeminiAIProvider : IAIProvider -{ - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly GeminiOptions _options; - private AIProviderStats _stats; - - public GeminiAIProvider(HttpClient httpClient, IOptions options, ILogger logger) - { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - - _stats = new AIProviderStats(); - - _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); - _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); - - _logger.LogInformation("GeminiAIProvider initialized with model: {Model}, timeout: {Timeout}s", - _options.Model, _options.TimeoutSeconds); - } - - #region IAIProvider 屬性 - - public string ProviderName => "Google Gemini"; - - public bool IsAvailable => !string.IsNullOrEmpty(_options.ApiKey); - - public decimal CostPerRequest => 0.001m; // 大概每次請求成本 - - public int MaxInputLength => _options.MaxOutputTokens / 4; // 粗略估計 - - public int AverageResponseTimeMs => _stats.AverageResponseTimeMs; - - #endregion - - #region 核心功能 - - public async Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options) - { - var stopwatch = Stopwatch.StartNew(); - _stats.TotalRequests++; - - try - { - _logger.LogInformation("Starting sentence analysis for text: {Text}", - inputText.Substring(0, Math.Min(50, inputText.Length))); - - var prompt = BuildAnalysisPrompt(inputText); - var aiResponse = await CallGeminiAPIAsync(prompt); - - if (string.IsNullOrWhiteSpace(aiResponse)) - { - throw new InvalidOperationException("Gemini API returned empty response"); - } - - var analysisData = ParseAIResponse(inputText, aiResponse); - - stopwatch.Stop(); - RecordSuccessfulRequest(stopwatch.ElapsedMilliseconds); - - _logger.LogInformation("Sentence analysis completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); - - return analysisData; - } - catch (Exception ex) - { - stopwatch.Stop(); - RecordFailedRequest(stopwatch.ElapsedMilliseconds); - - _logger.LogError(ex, "Error analyzing sentence: {Text}", inputText); - throw; - } - } - - public async Task CheckHealthAsync() - { - var stopwatch = Stopwatch.StartNew(); - - try - { - var testPrompt = "Test health check prompt"; - var response = await CallGeminiAPIAsync(testPrompt); - - stopwatch.Stop(); - - return new AIProviderHealthStatus - { - IsHealthy = !string.IsNullOrEmpty(response), - CheckedAt = DateTime.UtcNow, - ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds - }; - } - catch (Exception ex) - { - stopwatch.Stop(); - - return new AIProviderHealthStatus - { - IsHealthy = false, - ErrorMessage = ex.Message, - CheckedAt = DateTime.UtcNow, - ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds - }; - } - } - - public Task GetStatsAsync() - { - return Task.FromResult(_stats); - } - - #endregion - - #region 私有方法 - - private string BuildAnalysisPrompt(string inputText) - { - return $@"You are an English learning assistant. Analyze this sentence and return ONLY a valid JSON response. - -**Input Sentence**: ""{inputText}"" - -**Required JSON Structure:** -{{ - ""sentenceTranslation"": ""Traditional Chinese translation of the entire sentence"", - ""hasGrammarErrors"": true/false, - ""grammarCorrections"": [ - {{ - ""original"": ""incorrect text"", - ""corrected"": ""correct text"", - ""type"": ""error type (tense/subject-verb/preposition/word-order)"", - ""explanation"": ""brief explanation in Traditional Chinese"" - }} - ], - ""vocabularyAnalysis"": {{ - ""word1"": {{ - ""word"": ""the word"", - ""translation"": ""Traditional Chinese translation"", - ""definition"": ""English definition"", - ""partOfSpeech"": ""noun/verb/adjective/etc"", - ""pronunciation"": ""/phonetic/"", - ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", - ""frequency"": ""high/medium/low"", - ""synonyms"": [""synonym1"", ""synonym2""], - ""example"": ""example sentence"", - ""exampleTranslation"": ""Traditional Chinese example translation"" - }} - }}, - ""idioms"": [ - {{ - ""idiom"": ""idiomatic expression"", - ""translation"": ""Traditional Chinese meaning"", - ""definition"": ""English explanation"", - ""pronunciation"": ""/phonetic notation/"", - ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", - ""frequency"": ""high/medium/low"", - ""synonyms"": [""synonym1"", ""synonym2""], - ""example"": ""usage example"", - ""exampleTranslation"": ""Traditional Chinese example"" - }} - ] -}} - -**Analysis Guidelines:** -1. **Grammar Check**: Detect tense errors, subject-verb agreement, preposition usage, word order -2. **Vocabulary Analysis**: Include ALL significant words (exclude articles: a, an, the) -3. **CEFR Levels**: Assign accurate A1-C2 levels for each word -4. **Idioms**: Identify any idiomatic expressions or phrasal verbs -5. **Translations**: Use Traditional Chinese (Taiwan standard) - -**IMPORTANT**: Return ONLY the JSON object, no additional text or explanation."; - } - - private async Task CallGeminiAPIAsync(string prompt) - { - try - { - var requestBody = new - { - contents = new[] - { - new - { - parts = new[] - { - new { text = prompt } - } - } - }, - generationConfig = new - { - temperature = _options.Temperature, - topK = 40, - topP = 0.95, - maxOutputTokens = _options.MaxOutputTokens - } - }; - - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync( - $"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", - content); - - response.EnsureSuccessStatusCode(); - - var responseJson = await response.Content.ReadAsStringAsync(); - _logger.LogDebug("Raw Gemini API response: {Response}", - responseJson.Substring(0, Math.Min(500, responseJson.Length))); - - return ExtractTextFromResponse(responseJson); - } - catch (Exception ex) - { - _logger.LogError(ex, "Gemini API call failed"); - throw; - } - } - - private string ExtractTextFromResponse(string responseJson) - { - using var document = JsonDocument.Parse(responseJson); - var root = document.RootElement; - - if (root.TryGetProperty("candidates", out var candidatesElement) && - candidatesElement.ValueKind == JsonValueKind.Array) - { - var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault(); - if (firstCandidate.ValueKind != JsonValueKind.Undefined && - firstCandidate.TryGetProperty("content", out var contentElement) && - contentElement.TryGetProperty("parts", out var partsElement) && - partsElement.ValueKind == JsonValueKind.Array) - { - var firstPart = partsElement.EnumerateArray().FirstOrDefault(); - if (firstPart.TryGetProperty("text", out var textElement)) - { - return textElement.GetString() ?? string.Empty; - } - } - } - - // 檢查是否有安全過濾 - if (root.TryGetProperty("promptFeedback", out _)) - { - _logger.LogWarning("Gemini content filtered due to safety policies"); - return "The content analysis is temporarily unavailable due to safety filtering."; - } - - return string.Empty; - } - - private SentenceAnalysisData ParseAIResponse(string inputText, string aiResponse) - { - try - { - var cleanJson = CleanAIResponse(aiResponse); - var aiAnalysis = JsonSerializer.Deserialize(cleanJson, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (aiAnalysis == null) - { - throw new InvalidOperationException("Failed to parse AI response JSON"); - } - - return new SentenceAnalysisData - { - OriginalText = inputText, - SentenceMeaning = aiAnalysis.SentenceTranslation ?? "", - VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()), - Idioms = ConvertIdioms(aiAnalysis.Idioms ?? new()), - GrammarCorrection = ConvertGrammarCorrection(aiAnalysis), - Metadata = new AnalysisMetadata - { - ProcessingDate = DateTime.UtcNow, - AnalysisModel = _options.Model, - AnalysisVersion = "2.0" - } - }; - } - catch (JsonException ex) - { - _logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse); - return CreateFallbackAnalysis(inputText, aiResponse); - } - } - - private string CleanAIResponse(string aiResponse) - { - var cleanJson = aiResponse.Trim(); - if (cleanJson.StartsWith("```json")) - { - cleanJson = cleanJson.Substring(7); - } - if (cleanJson.EndsWith("```")) - { - cleanJson = cleanJson.Substring(0, cleanJson.Length - 3); - } - return cleanJson.Trim(); - } - - private Dictionary ConvertVocabularyAnalysis(Dictionary aiVocab) - { - var result = new Dictionary(); - - foreach (var kvp in aiVocab) - { - var aiWord = kvp.Value; - result[kvp.Key] = new VocabularyAnalysisDto - { - Word = aiWord.Word ?? kvp.Key, - Translation = aiWord.Translation ?? "", - Definition = aiWord.Definition ?? "", - PartOfSpeech = aiWord.PartOfSpeech ?? "unknown", - Pronunciation = aiWord.Pronunciation ?? $"/{kvp.Key}/", - DifficultyLevel = aiWord.DifficultyLevel ?? "A2", - Frequency = aiWord.Frequency ?? "medium", - Synonyms = aiWord.Synonyms ?? new List(), - Example = aiWord.Example, - ExampleTranslation = aiWord.ExampleTranslation, - }; - } - - return result; - } - - private List ConvertIdioms(List aiIdioms) - { - var result = new List(); - - foreach (var aiIdiom in aiIdioms) - { - result.Add(new IdiomDto - { - Idiom = aiIdiom.Idiom ?? "", - Translation = aiIdiom.Translation ?? "", - Definition = aiIdiom.Definition ?? "", - Pronunciation = aiIdiom.Pronunciation ?? "", - DifficultyLevel = aiIdiom.DifficultyLevel ?? "B2", - Frequency = aiIdiom.Frequency ?? "medium", - Synonyms = aiIdiom.Synonyms ?? new List(), - Example = aiIdiom.Example, - ExampleTranslation = aiIdiom.ExampleTranslation - }); - } - - return result; - } - - private GrammarCorrectionDto? ConvertGrammarCorrection(AiAnalysisResponse aiAnalysis) - { - if (!aiAnalysis.HasGrammarErrors || aiAnalysis.GrammarCorrections == null || !aiAnalysis.GrammarCorrections.Any()) - { - return null; - } - - var corrections = aiAnalysis.GrammarCorrections.Select(gc => new GrammarErrorDto - { - Error = gc.Original ?? "", - Correction = gc.Corrected ?? "", - Type = gc.Type ?? "grammar", - Explanation = gc.Explanation ?? "", - Severity = "medium", - Position = new ErrorPosition { Start = 0, End = 0 } - }).ToList(); - - return new GrammarCorrectionDto - { - HasErrors = true, - CorrectedText = string.Join(" ", corrections.Select(c => c.Correction)), - Corrections = corrections - }; - } - - private SentenceAnalysisData CreateFallbackAnalysis(string inputText, string aiResponse) - { - _logger.LogWarning("Using fallback analysis due to JSON parsing failure"); - - return new SentenceAnalysisData - { - OriginalText = inputText, - SentenceMeaning = aiResponse, - VocabularyAnalysis = new Dictionary(), - Metadata = new AnalysisMetadata - { - ProcessingDate = DateTime.UtcNow, - AnalysisModel = $"{_options.Model}-fallback", - AnalysisVersion = "2.0" - }, - }; - } - - private void RecordSuccessfulRequest(long elapsedMs) - { - _stats.SuccessfulRequests++; - _stats.LastUsedAt = DateTime.UtcNow; - _stats.TotalCost += CostPerRequest; - UpdateAverageResponseTime((int)elapsedMs); - } - - private void RecordFailedRequest(long elapsedMs) - { - _stats.FailedRequests++; - UpdateAverageResponseTime((int)elapsedMs); - } - - private void UpdateAverageResponseTime(int responseTimeMs) - { - if (_stats.AverageResponseTimeMs == 0) - { - _stats.AverageResponseTimeMs = responseTimeMs; - } - else - { - _stats.AverageResponseTimeMs = (_stats.AverageResponseTimeMs + responseTimeMs) / 2; - } - } - - #endregion -} - -#region AI Response Models (重用現有的模型) - -internal class AiAnalysisResponse -{ - public string? SentenceTranslation { get; set; } - public bool HasGrammarErrors { get; set; } - public List? GrammarCorrections { get; set; } - public Dictionary? VocabularyAnalysis { get; set; } - public List? Idioms { get; set; } -} - -internal class AiGrammarCorrection -{ - public string? Original { get; set; } - public string? Corrected { get; set; } - public string? Type { get; set; } - public string? Explanation { get; set; } -} - -internal class AiVocabularyAnalysis -{ - 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? DifficultyLevel { get; set; } - public string? Frequency { get; set; } - public List? Synonyms { get; set; } - public string? Example { get; set; } - public string? ExampleTranslation { get; set; } -} - -internal class AiIdiom -{ - public string? Idiom { get; set; } - public string? Translation { get; set; } - public string? Definition { get; set; } - public string? Pronunciation { get; set; } - public string? DifficultyLevel { get; set; } - public string? Frequency { get; set; } - public List? Synonyms { get; set; } - public string? Example { get; set; } - public string? ExampleTranslation { get; set; } -} - -#endregion \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/AI/IAIProvider.cs b/backend/DramaLing.Api/Services/AI/IAIProvider.cs deleted file mode 100644 index f234001..0000000 --- a/backend/DramaLing.Api/Services/AI/IAIProvider.cs +++ /dev/null @@ -1,79 +0,0 @@ -using DramaLing.Api.Models.DTOs; - -namespace DramaLing.Api.Services.AI; - -/// -/// AI 提供商抽象介面,支援多個 AI 服務提供商 -/// -public interface IAIProvider -{ - /// - /// 提供商名稱 - /// - string ProviderName { get; } - - /// - /// 提供商是否可用 - /// - bool IsAvailable { get; } - - /// - /// 每次請求的大概成本(用於選擇策略) - /// - decimal CostPerRequest { get; } - - /// - /// 支援的最大輸入長度 - /// - int MaxInputLength { get; } - - /// - /// 平均響應時間(毫秒) - /// - int AverageResponseTimeMs { get; } - - /// - /// 分析英文句子 - /// - /// 輸入文本 - /// 分析選項 - /// 分析結果 - Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options); - - /// - /// 檢查提供商健康狀態 - /// - /// 健康狀態 - Task CheckHealthAsync(); - - /// - /// 取得提供商使用統計 - /// - /// 使用統計 - Task GetStatsAsync(); -} - -/// -/// AI 提供商健康狀態 -/// -public class AIProviderHealthStatus -{ - public bool IsHealthy { get; set; } - public string? ErrorMessage { get; set; } - public DateTime CheckedAt { get; set; } - public int ResponseTimeMs { get; set; } -} - -/// -/// AI 提供商使用統計 -/// -public class AIProviderStats -{ - public int TotalRequests { get; set; } - public int SuccessfulRequests { get; set; } - public int FailedRequests { get; set; } - public double SuccessRate => TotalRequests > 0 ? (double)SuccessfulRequests / TotalRequests : 0; - public int AverageResponseTimeMs { get; set; } - public DateTime LastUsedAt { get; set; } - public decimal TotalCost { get; set; } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/AI/IAIProviderManager.cs b/backend/DramaLing.Api/Services/AI/IAIProviderManager.cs deleted file mode 100644 index 484290f..0000000 --- a/backend/DramaLing.Api/Services/AI/IAIProviderManager.cs +++ /dev/null @@ -1,99 +0,0 @@ -using DramaLing.Api.Models.DTOs; - -namespace DramaLing.Api.Services.AI; - -/// -/// AI 提供商管理器介面,負責選擇和管理多個 AI 提供商 -/// -public interface IAIProviderManager -{ - /// - /// 取得最佳 AI 提供商 - /// - /// 選擇策略 - /// AI 提供商 - Task GetBestProviderAsync(ProviderSelectionStrategy strategy = ProviderSelectionStrategy.Performance); - - /// - /// 取得所有可用的提供商 - /// - /// 可用提供商列表 - Task> GetAvailableProvidersAsync(); - - /// - /// 取得指定名稱的提供商 - /// - /// 提供商名稱 - /// AI 提供商 - Task GetProviderByNameAsync(string providerName); - - /// - /// 檢查所有提供商的健康狀態 - /// - /// 健康狀態報告 - Task CheckAllProvidersHealthAsync(); - - /// - /// 使用最佳提供商分析句子 - /// - /// 輸入文本 - /// 分析選項 - /// 選擇策略 - /// 分析結果 - Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options, - ProviderSelectionStrategy strategy = ProviderSelectionStrategy.Performance); -} - -/// -/// 提供商選擇策略 -/// -public enum ProviderSelectionStrategy -{ - /// - /// 基於性能選擇(響應時間) - /// - Performance, - - /// - /// 基於成本選擇(最便宜) - /// - Cost, - - /// - /// 基於可靠性選擇(成功率) - /// - Reliability, - - /// - /// 負載均衡 - /// - LoadBalance, - - /// - /// 使用主要提供商 - /// - Primary -} - -/// -/// 提供商健康狀態報告 -/// -public class ProviderHealthReport -{ - public DateTime CheckedAt { get; set; } - public int TotalProviders { get; set; } - public int HealthyProviders { get; set; } - public List ProviderHealthInfos { get; set; } = new(); -} - -/// -/// 提供商健康資訊 -/// -public class ProviderHealthInfo -{ - public string ProviderName { get; set; } = string.Empty; - public bool IsHealthy { get; set; } - public int ResponseTimeMs { get; set; } - public string? ErrorMessage { get; set; } - public AIProviderStats Stats { get; set; } = new(); -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/AnalysisCacheService.cs b/backend/DramaLing.Api/Services/AnalysisCacheService.cs deleted file mode 100644 index fc69784..0000000 --- a/backend/DramaLing.Api/Services/AnalysisCacheService.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -namespace DramaLing.Api.Services; - -public interface IAnalysisCacheService -{ - Task GetCachedAnalysisAsync(string inputText); - Task SetCachedAnalysisAsync(string inputText, object analysisResult, TimeSpan ttl); - Task InvalidateCacheAsync(string textHash); - Task GetCacheHitCountAsync(); - Task CleanExpiredCacheAsync(); -} - -public class AnalysisCacheService : IAnalysisCacheService -{ - private readonly DramaLingDbContext _context; - private readonly ILogger _logger; - - public AnalysisCacheService(DramaLingDbContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - /// - /// 獲取快取的分析結果 - /// - public async Task GetCachedAnalysisAsync(string inputText) - { - try - { - var textHash = GenerateTextHash(inputText); - var cached = await _context.SentenceAnalysisCache - .FirstOrDefaultAsync(c => c.InputTextHash == textHash && c.ExpiresAt > DateTime.UtcNow); - - if (cached != null) - { - // 更新訪問統計 - cached.AccessCount++; - cached.LastAccessedAt = DateTime.UtcNow; - await _context.SaveChangesAsync(); - - _logger.LogInformation("Cache hit for text hash: {TextHash}", textHash); - return cached; - } - - _logger.LogInformation("Cache miss for text hash: {TextHash}", textHash); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting cached analysis for text: {InputText}", inputText); - return null; - } - } - - /// - /// 設定快取分析結果 - /// - public async Task SetCachedAnalysisAsync(string inputText, object analysisResult, TimeSpan ttl) - { - try - { - var textHash = GenerateTextHash(inputText); - var expiresAt = DateTime.UtcNow.Add(ttl); - - // 檢查是否已存在 - var existing = await _context.SentenceAnalysisCache - .FirstOrDefaultAsync(c => c.InputTextHash == textHash); - - if (existing != null) - { - // 更新現有快取 - 使用一致的命名策略 - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - existing.AnalysisResult = JsonSerializer.Serialize(analysisResult, options); - existing.ExpiresAt = expiresAt; - existing.AccessCount++; - existing.LastAccessedAt = DateTime.UtcNow; - } - else - { - // 創建新快取項目 - var cacheItem = new SentenceAnalysisCache - { - Id = Guid.NewGuid(), - InputTextHash = textHash, - InputText = inputText, - AnalysisResult = JsonSerializer.Serialize(analysisResult, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }), - CreatedAt = DateTime.UtcNow, - ExpiresAt = expiresAt, - AccessCount = 1, - LastAccessedAt = DateTime.UtcNow - }; - - _context.SentenceAnalysisCache.Add(cacheItem); - } - - await _context.SaveChangesAsync(); - - _logger.LogInformation("Cached analysis for text hash: {TextHash}, expires at: {ExpiresAt}", - textHash, expiresAt); - - return textHash; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error setting cached analysis for text: {InputText}", inputText); - throw; - } - } - - /// - /// 使快取失效 - /// - public async Task InvalidateCacheAsync(string textHash) - { - try - { - var cached = await _context.SentenceAnalysisCache - .FirstOrDefaultAsync(c => c.InputTextHash == textHash); - - if (cached != null) - { - _context.SentenceAnalysisCache.Remove(cached); - await _context.SaveChangesAsync(); - - _logger.LogInformation("Invalidated cache for text hash: {TextHash}", textHash); - return true; - } - - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error invalidating cache for text hash: {TextHash}", textHash); - return false; - } - } - - /// - /// 獲取快取命中次數 - /// - public async Task GetCacheHitCountAsync() - { - try - { - return await _context.SentenceAnalysisCache - .Where(c => c.ExpiresAt > DateTime.UtcNow) - .SumAsync(c => c.AccessCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting cache hit count"); - return 0; - } - } - - /// - /// 清理過期的快取 - /// - public async Task CleanExpiredCacheAsync() - { - try - { - var expiredItems = await _context.SentenceAnalysisCache - .Where(c => c.ExpiresAt <= DateTime.UtcNow) - .ToListAsync(); - - if (expiredItems.Any()) - { - _context.SentenceAnalysisCache.RemoveRange(expiredItems); - await _context.SaveChangesAsync(); - - _logger.LogInformation("Cleaned {Count} expired cache items", expiredItems.Count); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error cleaning expired cache"); - } - } - - /// - /// 生成文本哈希值 - /// - private string GenerateTextHash(string inputText) - { - using var sha256 = SHA256.Create(); - var bytes = Encoding.UTF8.GetBytes(inputText.Trim().ToLower()); - var hash = sha256.ComputeHash(bytes); - return Convert.ToHexString(hash).ToLower(); - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/CEFRLevelService.cs b/backend/DramaLing.Api/Services/CEFRLevelService.cs deleted file mode 100644 index 8c5b4cf..0000000 --- a/backend/DramaLing.Api/Services/CEFRLevelService.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; - -namespace DramaLing.Api.Services; - -public static class CEFRLevelService -{ - private static readonly string[] Levels = { "A1", "A2", "B1", "B2", "C1", "C2" }; - - /// - /// 取得 CEFR 等級的數字索引 - /// - public static int GetLevelIndex(string level) - { - if (string.IsNullOrEmpty(level)) return 1; // 預設 A2 - return Array.IndexOf(Levels, level.ToUpper()); - } - - /// - /// 判定詞彙對特定用戶是否為高價值 - /// 規則:比用戶程度高 1-2 級的詞彙為高價值 - /// - public static bool IsHighValueForUser(string wordLevel, string userLevel) - { - var userIndex = GetLevelIndex(userLevel); - var wordIndex = GetLevelIndex(wordLevel); - - // 無效等級處理 - if (userIndex == -1 || wordIndex == -1) return false; - - // 高價值 = 比用戶程度高 1-2 級 - return wordIndex >= userIndex + 1 && wordIndex <= userIndex + 2; - } - - /// - /// 取得用戶的目標學習等級範圍 - /// - public static string GetTargetLevelRange(string userLevel) - { - var userIndex = GetLevelIndex(userLevel); - if (userIndex == -1) return "B1-B2"; - - var targetMin = Levels[Math.Min(userIndex + 1, Levels.Length - 1)]; - var targetMax = Levels[Math.Min(userIndex + 2, Levels.Length - 1)]; - - return targetMin == targetMax ? targetMin : $"{targetMin}-{targetMax}"; - } - - /// - /// 取得下一個等級 - /// - public static string GetNextLevel(string currentLevel, int steps = 1) - { - var currentIndex = GetLevelIndex(currentLevel); - if (currentIndex == -1) return "B1"; - - var nextIndex = Math.Min(currentIndex + steps, Levels.Length - 1); - return Levels[nextIndex]; - } - - /// - /// 取得所有有效的CEFR等級 - /// - /// CEFR等級數組 - public static string[] GetAllLevels() - { - return (string[])Levels.Clone(); - } - - /// - /// 驗證CEFR等級是否有效 - /// - /// 要驗證的等級 - /// 是否為有效等級 - public static bool IsValidLevel(string level) - { - return !string.IsNullOrEmpty(level) && - Array.IndexOf(Levels, level.ToUpper()) != -1; - } - - /// - /// 取得等級的描述 - /// - /// CEFR等級 - /// 等級描述 - public static string GetLevelDescription(string level) - { - return level.ToUpper() switch - { - "A1" => "初學者 - 能理解基本詞彙和簡單句子", - "A2" => "基礎 - 能處理日常對話和常見主題", - "B1" => "中級 - 能理解清楚標準語言的要點", - "B2" => "中高級 - 能理解複雜文本的主要內容", - "C1" => "高級 - 能流利表達,理解含蓄意思", - "C2" => "精通 - 接近母語水平", - _ => "未知等級" - }; - } - - /// - /// 取得等級的範例詞彙 - /// - /// CEFR等級 - /// 範例詞彙數組 - public static string[] GetLevelExamples(string level) - { - return level.ToUpper() switch - { - "A1" => new[] { "hello", "good", "house", "eat", "happy" }, - "A2" => new[] { "important", "difficult", "interesting", "beautiful", "understand" }, - "B1" => new[] { "analyze", "opportunity", "environment", "responsibility", "development" }, - "B2" => new[] { "sophisticated", "implications", "comprehensive", "substantial", "methodology" }, - "C1" => new[] { "meticulous", "predominantly", "intricate", "corroborate", "paradigm" }, - "C2" => new[] { "ubiquitous", "ephemeral", "perspicacious", "multifarious", "idiosyncratic" }, - _ => new[] { "example" } - }; - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/CEFRMappingService.cs b/backend/DramaLing.Api/Services/CEFRMappingService.cs deleted file mode 100644 index cc0903a..0000000 --- a/backend/DramaLing.Api/Services/CEFRMappingService.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace DramaLing.Api.Services; - -/// -/// CEFR等級映射服務 - 將CEFR等級轉換為詞彙難度數值 -/// -public static class CEFRMappingService -{ - private static readonly Dictionary CEFRToWordLevel = new() - { - { "A1", 20 }, // 基礎詞彙 (1-1000常用詞) - { "A2", 35 }, // 常用詞彙 (1001-3000詞) - { "B1", 50 }, // 中級詞彙 (3001-6000詞) - { "B2", 65 }, // 中高級詞彙 (6001-12000詞) - { "C1", 80 }, // 高級詞彙 (12001-20000詞) - { "C2", 95 } // 精通詞彙 (20000+詞) - }; - - private static readonly Dictionary WordLevelToCEFR = new() - { - { 20, "A1" }, { 35, "A2" }, { 50, "B1" }, - { 65, "B2" }, { 80, "C1" }, { 95, "C2" } - }; - - /// - /// 根據CEFR等級獲取詞彙難度數值 - /// - /// CEFR等級 (A1-C2) - /// 詞彙難度 (1-100) - public static int GetWordLevel(string? cefrLevel) - { - if (string.IsNullOrEmpty(cefrLevel)) - return 50; // 預設B1級別 - - return CEFRToWordLevel.GetValueOrDefault(cefrLevel.ToUpperInvariant(), 50); - } - - /// - /// 根據詞彙難度數值獲取CEFR等級 - /// - /// 詞彙難度 (1-100) - /// 對應的CEFR等級 - public static string GetCEFRLevel(int wordLevel) - { - // 找到最接近的CEFR等級 - var closestLevel = WordLevelToCEFR.Keys - .OrderBy(level => Math.Abs(level - wordLevel)) - .First(); - - return WordLevelToCEFR[closestLevel]; - } - - /// - /// 獲取新用戶的預設程度 - /// - /// 預設用戶程度 (50 = B1級別) - public static int GetDefaultUserLevel() => 50; - - /// - /// 判斷是否為A1學習者 - /// - /// 學習者程度 - /// 是否為A1學習者 - public static bool IsA1Learner(int userLevel) => userLevel <= 20; - - /// - /// 獲取學習者程度描述 - /// - /// 學習者程度 (1-100) - /// 程度描述 - public static string GetUserLevelDescription(int userLevel) - { - return userLevel switch - { - <= 20 => "A1 - 初學者", - <= 35 => "A2 - 基礎", - <= 50 => "B1 - 中級", - <= 65 => "B2 - 中高級", - <= 80 => "C1 - 高級", - _ => "C2 - 精通" - }; - } - - /// - /// 根據詞彙使用頻率估算難度 (未來擴展用) - /// - /// 詞彙頻率排名 - /// 估算的詞彙難度 - public static int EstimateWordLevelByFrequency(int frequency) - { - return frequency switch - { - <= 1000 => 20, // 最常用1000詞 → A1 - <= 3000 => 35, // 常用3000詞 → A2 - <= 6000 => 50, // 中級6000詞 → B1 - <= 12000 => 65, // 中高級12000詞 → B2 - <= 20000 => 80, // 高級20000詞 → C1 - _ => 95 // 超過20000詞 → C2 - }; - } - - /// - /// 獲取所有CEFR等級列表 - /// - /// CEFR等級數組 - public static string[] GetAllCEFRLevels() => new[] { "A1", "A2", "B1", "B2", "C1", "C2" }; - - /// - /// 驗證CEFR等級是否有效 - /// - /// 要驗證的CEFR等級 - /// 是否有效 - public static bool IsValidCEFRLevel(string? cefrLevel) - { - if (string.IsNullOrEmpty(cefrLevel)) - return false; - - return CEFRToWordLevel.ContainsKey(cefrLevel.ToUpperInvariant()); - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/CacheCleanupService.cs b/backend/DramaLing.Api/Services/CacheCleanupService.cs deleted file mode 100644 index 2f27450..0000000 --- a/backend/DramaLing.Api/Services/CacheCleanupService.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace DramaLing.Api.Services; - -public class CacheCleanupService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(1); // 每小時清理一次 - - public CacheCleanupService(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Cache cleanup service started"); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - using var scope = _serviceProvider.CreateScope(); - var cacheService = scope.ServiceProvider.GetRequiredService(); - - _logger.LogInformation("Starting cache cleanup..."); - await cacheService.CleanExpiredCacheAsync(); - _logger.LogInformation("Cache cleanup completed"); - - await Task.Delay(_cleanupInterval, stoppingToken); - } - catch (OperationCanceledException) - { - // 正常的服務停止 - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during cache cleanup"); - // 出錯時等待較短時間後重試 - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - _logger.LogInformation("Cache cleanup service stopped"); - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Domain/Learning/ICEFRLevelService.cs b/backend/DramaLing.Api/Services/Domain/Learning/ICEFRLevelService.cs deleted file mode 100644 index e7b349d..0000000 --- a/backend/DramaLing.Api/Services/Domain/Learning/ICEFRLevelService.cs +++ /dev/null @@ -1,167 +0,0 @@ -namespace DramaLing.Api.Services.Domain.Learning; - -/// -/// CEFR 等級服務介面 -/// -public interface ICEFRLevelService -{ - /// - /// 取得 CEFR 等級的數字索引 - /// - int GetLevelIndex(string level); - - /// - /// 判定詞彙對特定用戶是否為高價值 - /// - bool IsHighValueForUser(string wordLevel, string userLevel); - - /// - /// 取得用戶的目標學習等級範圍 - /// - string GetTargetLevelRange(string userLevel); - - /// - /// 取得下一個等級 - /// - string GetNextLevel(string currentLevel); - - /// - /// 計算等級進度百分比 - /// - double CalculateLevelProgress(string currentLevel, int masteredWords, int totalWords); - - /// - /// 根據掌握詞彙數推薦等級 - /// - string RecommendLevel(Dictionary masteredWordsByLevel); - - /// - /// 驗證等級是否有效 - /// - bool IsValidLevel(string level); - - /// - /// 取得所有等級列表 - /// - IEnumerable GetAllLevels(); -} - -/// -/// CEFR 等級服務實作 -/// -public class CEFRLevelService : ICEFRLevelService -{ - private static readonly string[] Levels = { "A1", "A2", "B1", "B2", "C1", "C2" }; - private readonly ILogger _logger; - - public CEFRLevelService(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public int GetLevelIndex(string level) - { - if (string.IsNullOrEmpty(level)) - { - _logger.LogWarning("Invalid level provided: null or empty, defaulting to A2"); - return 1; // 預設 A2 - } - - var index = Array.IndexOf(Levels, level.ToUpper()); - if (index == -1) - { - _logger.LogWarning("Unknown CEFR level: {Level}, defaulting to A2", level); - return 1; - } - - return index; - } - - public bool IsHighValueForUser(string wordLevel, string userLevel) - { - var userIndex = GetLevelIndex(userLevel); - var wordIndex = GetLevelIndex(wordLevel); - - // 無效等級處理 - if (userIndex == -1 || wordIndex == -1) - { - _logger.LogWarning("Invalid levels for comparison: word={WordLevel}, user={UserLevel}", wordLevel, userLevel); - return false; - } - - // 高價值 = 比用戶程度高 1-2 級 - var isHighValue = wordIndex >= userIndex + 1 && wordIndex <= userIndex + 2; - - _logger.LogDebug("High value check: word={WordLevel}({WordIndex}), user={UserLevel}({UserIndex}), result={IsHighValue}", - wordLevel, wordIndex, userLevel, userIndex, isHighValue); - - return isHighValue; - } - - public string GetTargetLevelRange(string userLevel) - { - var userIndex = GetLevelIndex(userLevel); - if (userIndex == -1) return "B1-B2"; - - var targetMin = Levels[Math.Min(userIndex + 1, Levels.Length - 1)]; - var targetMax = Levels[Math.Min(userIndex + 2, Levels.Length - 1)]; - - return targetMin == targetMax ? targetMin : $"{targetMin}-{targetMax}"; - } - - public string GetNextLevel(string currentLevel) - { - var currentIndex = GetLevelIndex(currentLevel); - if (currentIndex == -1 || currentIndex >= Levels.Length - 1) - { - return Levels[^1]; // 返回最高等級 - } - - return Levels[currentIndex + 1]; - } - - public double CalculateLevelProgress(string currentLevel, int masteredWords, int totalWords) - { - if (totalWords == 0) return 0; - - var progress = (double)masteredWords / totalWords; - _logger.LogDebug("Level progress for {Level}: {MasteredWords}/{TotalWords} = {Progress:P}", - currentLevel, masteredWords, totalWords, progress); - - return Math.Min(progress, 1.0); - } - - public string RecommendLevel(Dictionary masteredWordsByLevel) - { - try - { - // 簡單的推薦邏輯:找到掌握詞彙最多的等級 - var bestLevel = masteredWordsByLevel - .Where(kvp => kvp.Value > 0) - .OrderByDescending(kvp => kvp.Value) - .FirstOrDefault(); - - if (bestLevel.Key != null && IsValidLevel(bestLevel.Key)) - { - return GetNextLevel(bestLevel.Key); - } - - return "A2"; // 預設等級 - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recommending level"); - return "A2"; - } - } - - public bool IsValidLevel(string level) - { - return !string.IsNullOrEmpty(level) && Levels.Contains(level.ToUpper()); - } - - public IEnumerable GetAllLevels() - { - return Levels.AsEnumerable(); - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/HealthCheckService.cs b/backend/DramaLing.Api/Services/HealthCheckService.cs deleted file mode 100644 index 6efcdd0..0000000 --- a/backend/DramaLing.Api/Services/HealthCheckService.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; -using DramaLing.Api.Data; -using DramaLing.Api.Services.AI; -using DramaLing.Api.Services.Caching; -using Microsoft.EntityFrameworkCore; -using System.Diagnostics; - -namespace DramaLing.Api.Services; - -/// -/// 系統健康檢查服務,監控各個重要組件的狀態 -/// -public class SystemHealthCheckService : IHealthCheck -{ - private readonly DramaLingDbContext _dbContext; - private readonly IAIProviderManager _aiProviderManager; - private readonly ICacheService _cacheService; - private readonly ILogger _logger; - - public SystemHealthCheckService( - DramaLingDbContext dbContext, - IAIProviderManager aiProviderManager, - ICacheService cacheService, - ILogger logger) - { - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - _aiProviderManager = aiProviderManager ?? throw new ArgumentNullException(nameof(aiProviderManager)); - _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - var healthData = new Dictionary(); - var isHealthy = true; - var failureMessages = new List(); - - try - { - // 1. 資料庫健康檢查 - var dbCheck = await CheckDatabaseHealthAsync(); - healthData["Database"] = dbCheck; - if (!dbCheck.IsHealthy) - { - isHealthy = false; - failureMessages.Add($"Database: {dbCheck.Message}"); - } - - // 2. AI 服務健康檢查 - var aiCheck = await CheckAIServicesHealthAsync(); - healthData["AIServices"] = aiCheck; - if (!aiCheck.IsHealthy) - { - isHealthy = false; - failureMessages.Add($"AI Services: {aiCheck.Message}"); - } - - // 3. 快取服務健康檢查 - var cacheCheck = await CheckCacheHealthAsync(); - healthData["Cache"] = cacheCheck; - if (!cacheCheck.IsHealthy) - { - isHealthy = false; - failureMessages.Add($"Cache: {cacheCheck.Message}"); - } - - // 4. 記憶體使用檢查 - var memoryCheck = CheckMemoryUsage(); - healthData["Memory"] = memoryCheck; - if (!memoryCheck.IsHealthy) - { - isHealthy = false; - failureMessages.Add($"Memory: {memoryCheck.Message}"); - } - - // 5. 系統資源檢查 - healthData["SystemInfo"] = GetSystemInfo(); - - var result = isHealthy - ? HealthCheckResult.Healthy("All systems operational", healthData) - : HealthCheckResult.Unhealthy($"Health check failed: {string.Join(", ", failureMessages)}", null, healthData); - - _logger.LogInformation("Health check completed: {Status}", result.Status); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Health check failed with exception"); - return HealthCheckResult.Unhealthy("Health check exception", ex, healthData); - } - } - - private async Task CheckDatabaseHealthAsync() - { - try - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - var startTime = DateTime.UtcNow; - - // 簡單的連接性測試 - await _dbContext.Database.CanConnectAsync(cts.Token); - - var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds; - - return new HealthCheckComponent - { - IsHealthy = true, - Message = "Database connection successful", - ResponseTimeMs = (int)responseTime, - CheckedAt = DateTime.UtcNow - }; - } - catch (Exception ex) - { - return new HealthCheckComponent - { - IsHealthy = false, - Message = $"Database connection failed: {ex.Message}", - CheckedAt = DateTime.UtcNow - }; - } - } - - private async Task CheckAIServicesHealthAsync() - { - try - { - var healthReport = await _aiProviderManager.CheckAllProvidersHealthAsync(); - - return new HealthCheckComponent - { - IsHealthy = healthReport.HealthyProviders > 0, - Message = $"{healthReport.HealthyProviders}/{healthReport.TotalProviders} AI providers healthy", - ResponseTimeMs = healthReport.ProviderHealthInfos.Any() - ? (int)healthReport.ProviderHealthInfos.Average(p => p.ResponseTimeMs) - : 0, - CheckedAt = healthReport.CheckedAt, - Details = healthReport.ProviderHealthInfos.ToDictionary( - p => p.ProviderName, - p => new { p.IsHealthy, p.ResponseTimeMs, p.ErrorMessage }) - }; - } - catch (Exception ex) - { - return new HealthCheckComponent - { - IsHealthy = false, - Message = $"AI services check failed: {ex.Message}", - CheckedAt = DateTime.UtcNow - }; - } - } - - private async Task CheckCacheHealthAsync() - { - try - { - var testKey = $"health_check_{Guid.NewGuid()}"; - var testValue = new { Test = "HealthCheck", Timestamp = DateTime.UtcNow }; - - var startTime = DateTime.UtcNow; - - // 測試設定和讀取 - await _cacheService.SetAsync(testKey, testValue, TimeSpan.FromMinutes(1)); - var retrieved = await _cacheService.GetAsync(testKey); - await _cacheService.RemoveAsync(testKey); - - var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds; - - var stats = await _cacheService.GetStatsAsync(); - - return new HealthCheckComponent - { - IsHealthy = retrieved != null, - Message = "Cache service operational", - ResponseTimeMs = (int)responseTime, - CheckedAt = DateTime.UtcNow, - Details = new - { - HitRate = stats.HitRate, - TotalKeys = stats.TotalKeys, - TotalRequests = stats.TotalRequests - } - }; - } - catch (Exception ex) - { - return new HealthCheckComponent - { - IsHealthy = false, - Message = $"Cache service failed: {ex.Message}", - CheckedAt = DateTime.UtcNow - }; - } - } - - private HealthCheckComponent CheckMemoryUsage() - { - try - { - var memoryUsage = GC.GetTotalMemory(false); - var maxMemory = 512 * 1024 * 1024; // 512MB 限制 - var memoryPercentage = (double)memoryUsage / maxMemory; - - return new HealthCheckComponent - { - IsHealthy = memoryPercentage < 0.8, // 80% 記憶體使用率為警告線 - Message = $"Memory usage: {memoryUsage / 1024 / 1024}MB ({memoryPercentage:P1})", - CheckedAt = DateTime.UtcNow, - Details = new - { - MemoryUsageBytes = memoryUsage, - MemoryUsageMB = memoryUsage / 1024 / 1024, - MemoryPercentage = memoryPercentage, - MaxMemoryMB = maxMemory / 1024 / 1024 - } - }; - } - catch (Exception ex) - { - return new HealthCheckComponent - { - IsHealthy = false, - Message = $"Memory check failed: {ex.Message}", - CheckedAt = DateTime.UtcNow - }; - } - } - - private object GetSystemInfo() - { - return new - { - Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown", - MachineName = Environment.MachineName, - OSVersion = Environment.OSVersion.ToString(), - ProcessorCount = Environment.ProcessorCount, - RuntimeVersion = Environment.Version.ToString(), - Uptime = DateTime.UtcNow - Process.GetCurrentProcess().StartTime.ToUniversalTime(), - Timestamp = DateTime.UtcNow - }; - } -} - -/// -/// 健康檢查組件結果 -/// -public class HealthCheckComponent -{ - public bool IsHealthy { get; set; } - public string Message { get; set; } = string.Empty; - public int ResponseTimeMs { get; set; } - public DateTime CheckedAt { get; set; } - public object? Details { get; set; } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs b/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs index 9962199..1090fc4 100644 --- a/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs +++ b/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs @@ -1,7 +1,7 @@ using DramaLing.Api.Data; using DramaLing.Api.Models.DTOs; using DramaLing.Api.Models.Entities; -using DramaLing.Api.Services.AI; +// Services.AI namespace removed using DramaLing.Api.Services; using DramaLing.Api.Services.Storage; using Microsoft.EntityFrameworkCore; diff --git a/backend/DramaLing.Api/Services/Infrastructure/Authentication/ITokenService.cs b/backend/DramaLing.Api/Services/Infrastructure/Authentication/ITokenService.cs deleted file mode 100644 index 1827532..0000000 --- a/backend/DramaLing.Api/Services/Infrastructure/Authentication/ITokenService.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Security.Claims; - -namespace DramaLing.Api.Services.Infrastructure.Authentication; - -/// -/// Token 處理服務介面 -/// -public interface ITokenService -{ - /// - /// 驗證 JWT Token - /// - Task ValidateTokenAsync(string token); - - /// - /// 從 Token 提取用戶 ID - /// - Task ExtractUserIdAsync(string token); - - /// - /// 從 Authorization Header 提取用戶 ID - /// - Task GetUserIdFromHeaderAsync(string? authorizationHeader); - - /// - /// 檢查 Token 是否有效 - /// - Task IsTokenValidAsync(string token); - - /// - /// 取得 Token 的過期時間 - /// - Task GetTokenExpiryAsync(string token); -} - -/// -/// 用戶身份服務介面 -/// -public interface IUserIdentityService -{ - /// - /// 取得當前用戶 ID - /// - Task GetCurrentUserIdAsync(); - - /// - /// 檢查用戶是否為 Premium - /// - Task IsCurrentUserPremiumAsync(); - - /// - /// 取得用戶角色 - /// - Task> GetUserRolesAsync(Guid userId); - - /// - /// 檢查用戶權限 - /// - Task HasPermissionAsync(Guid userId, string permission); -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Infrastructure/IConfigurationService.cs b/backend/DramaLing.Api/Services/Infrastructure/IConfigurationService.cs deleted file mode 100644 index 9674b9d..0000000 --- a/backend/DramaLing.Api/Services/Infrastructure/IConfigurationService.cs +++ /dev/null @@ -1,113 +0,0 @@ -namespace DramaLing.Api.Services.Infrastructure; - -/// -/// 統一配置管理服務介面 -/// -public interface IConfigurationService -{ - /// - /// 取得 AI 相關配置 - /// - Task GetAIConfigurationAsync(); - - /// - /// 取得認證相關配置 - /// - Task GetAuthConfigurationAsync(); - - /// - /// 取得外部服務配置 - /// - Task GetExternalServicesConfigurationAsync(); - - /// - /// 取得快取配置 - /// - Task GetCacheConfigurationAsync(); - - /// - /// 檢查配置是否完整 - /// - Task ValidateConfigurationAsync(); - - /// - /// 取得環境特定配置 - /// - T GetEnvironmentConfiguration(string sectionName) where T : class, new(); -} - -/// -/// AI 相關配置 -/// -public class AIConfiguration -{ - public string GeminiApiKey { get; set; } = string.Empty; - public string GeminiModel { get; set; } = "gemini-1.5-flash"; - public int TimeoutSeconds { get; set; } = 30; - public double Temperature { get; set; } = 0.7; - public int MaxOutputTokens { get; set; } = 2000; - public int MaxRetries { get; set; } = 3; -} - -/// -/// 認證相關配置 -/// -public class AuthConfiguration -{ - public string JwtSecret { get; set; } = string.Empty; - public string SupabaseUrl { get; set; } = string.Empty; - public string ValidAudience { get; set; } = "authenticated"; - public int ClockSkewMinutes { get; set; } = 5; -} - -/// -/// 外部服務配置 -/// -public class ExternalServicesConfiguration -{ - public AzureSpeechConfiguration AzureSpeech { get; set; } = new(); - public DatabaseConfiguration Database { get; set; } = new(); -} - -/// -/// Azure Speech 配置 -/// -public class AzureSpeechConfiguration -{ - public string SubscriptionKey { get; set; } = string.Empty; - public string Region { get; set; } = string.Empty; - public bool IsConfigured => !string.IsNullOrEmpty(SubscriptionKey) && !string.IsNullOrEmpty(Region); -} - -/// -/// 資料庫配置 -/// -public class DatabaseConfiguration -{ - public string ConnectionString { get; set; } = string.Empty; - public bool UseInMemoryDb { get; set; } = false; - public int CommandTimeoutSeconds { get; set; } = 30; -} - -/// -/// 快取配置 -/// -public class CacheConfiguration -{ - public bool EnableDistributedCache { get; set; } = false; - public TimeSpan DefaultExpiry { get; set; } = TimeSpan.FromMinutes(10); - public TimeSpan AnalysisCacheExpiry { get; set; } = TimeSpan.FromHours(2); - public TimeSpan UserCacheExpiry { get; set; } = TimeSpan.FromMinutes(30); - public int MaxMemoryCacheSizeMB { get; set; } = 100; -} - -/// -/// 配置驗證結果 -/// -public class ConfigurationValidationResult -{ - public bool IsValid { get; set; } - public List Errors { get; set; } = new(); - public List Warnings { get; set; } = new(); - public Dictionary ConfigurationSummary { get; set; } = new(); -} \ No newline at end of file diff --git a/後端Services未引用程式碼盤點報告.md b/後端Services未引用程式碼盤點報告.md new file mode 100644 index 0000000..69de702 --- /dev/null +++ b/後端Services未引用程式碼盤點報告.md @@ -0,0 +1,544 @@ +# 後端 Services 未引用程式碼盤點報告 + +**日期**: 2025-09-29 +**範圍**: DramaLing.Api/Services/ 資料夾 +**目的**: 識別未引用的"死代碼",為大規模架構清理做準備 +**發現**: 🚨 **嚴重的代碼冗余問題** + +--- + +## 📊 **盤點結果總覽** + +### **數據統計** +- **總服務文件**: 31個 📄 +- **實際註冊服務**: 11個 ✅ (僅 35%) +- **疑似死代碼**: 20個 🗑️ (65%) +- **資料夾層級**: 7個子資料夾 📁 + +### **代碼冗余程度**: 🔴 **極高 (65%)** + +--- + +## 🔍 **詳細引用狀態分析** + +### ✅ **已確認使用的服務** (11個) + +#### **在 Program.cs 中已註冊**: +```csharp +1. ✅ HybridCacheService (Caching/HybridCacheService.cs) +2. ✅ AuthService (AuthService.cs) +3. ✅ GeminiService (GeminiService.cs) +4. ✅ AnalysisService (AnalysisService.cs) +5. ✅ UsageTrackingService (UsageTrackingService.cs) +6. ✅ AzureSpeechService (AzureSpeechService.cs) +7. ✅ AudioCacheService (AudioCacheService.cs) +8. ✅ OptionsVocabularyService (OptionsVocabularyService.cs) +9. ✅ ReplicateService (ReplicateService.cs) +10. ✅ LocalImageStorageService (Storage/LocalImageStorageService.cs) +11. ✅ ImageProcessingService (ImageProcessingService.cs) +``` + +### 🗑️ **疑似死代碼服務** (20個) + +#### **根目錄未引用服務** (8個): +``` +📄 HealthCheckService.cs 🗑️ 未註冊,未引用 +📄 CacheCleanupService.cs 🗑️ 未註冊,未引用 +📄 CEFRLevelService.cs 🗑️ 未註冊,未引用 +📄 AnalysisCacheService.cs 🗑️ 未註冊,與 AnalysisService 重複 +📄 CEFRMappingService.cs 🗑️ 未註冊,未引用 +📄 ImageGenerationOrchestrator.cs 🗑️ 未註冊,未引用 +📄 IImageGenerationOrchestrator.cs 🗑️ 介面未使用 +📄 IImageProcessingService.cs 🗑️ 介面可能重複 +``` + +#### **AI/ 資料夾 (4個檔案)** - 🗑️ **整個資料夾疑似未使用**: +``` +📁 AI/ +├── AIProviderManager.cs 🗑️ 未註冊,過度設計 +├── GeminiAIProvider.cs 🗑️ 與根目錄 GeminiService 重複 +├── IAIProvider.cs 🗑️ 介面未使用 +└── IAIProviderManager.cs 🗑️ 介面未使用 +``` + +#### **Domain/Learning/ 資料夾** - 🗑️ **整個資料夾已清空**: +``` +📁 Domain/Learning/ +└── ICEFRLevelService.cs 🗑️ 與根目錄服務重複 +``` + +#### **Infrastructure/ 資料夾 (2個檔案)** - 🗑️ **疑似未使用**: +``` +📁 Infrastructure/Authentication/ +└── ITokenService.cs 🗑️ 未使用,AuthService 已涵蓋 + +📁 Infrastructure/ +└── IConfigurationService.cs 🗑️ 未使用,.NET Core 內建配置已足夠 +``` + +#### **Monitoring/ 資料夾** - ⚠️ **部分使用**: +``` +📁 Monitoring/ +└── OptionsVocabularyMetrics.cs ⚠️ 已註冊但可能過度複雜 +``` + +#### **其他介面文件** (5個): +``` +📄 IAnalysisService.cs ✅ 有使用 +📄 IOptionsVocabularyService.cs ✅ 有使用 +📄 Caching/ICacheService.cs ✅ 有使用 +📄 Storage/IImageStorageService.cs ✅ 有使用 +``` + +--- + +## 🧮 **代碼量統計分析** + +### **按使用狀態分類**: +``` +📊 代碼使用狀態統計: + +✅ 活躍使用: 11個服務 + ├── 估計代碼行數: ~8,000 行 + └── 佔比: 35% + +🗑️ 疑似死代碼: 20個服務 + ├── 估計代碼行數: ~15,000 行 + └── 佔比: 65% + +📁 無用資料夾: 3-4個 + ├── AI/ (4個檔案) + ├── Domain/Learning/ (已清空但殘留) + ├── Infrastructure/ (2個檔案) + └── 部分 Monitoring/ +``` + +### **冗余嚴重性評估**: 🔴 **極高** +- **未使用服務比例**: 65% +- **重複功能**: AI 服務有2套實現 +- **過度抽象**: 許多介面沒有實際需求 +- **資料夾混亂**: 沒有統一的組織邏輯 + +--- + +## 🚨 **發現的主要問題** + +### **1. 功能重複** ⚠️ +``` +重複的功能實現: +├── GeminiService.cs (根目錄) ✅ 使用中 +└── AI/GeminiAIProvider.cs 🗑️ 重複實現 + +├── CEFRLevelService.cs 🗑️ 未使用 +└── Domain/Learning/ICEFRLevelService.cs 🗑️ 重複介面 + +├── AnalysisService.cs ✅ 使用中 +└── AnalysisCacheService.cs 🗑️ 功能重疊 +``` + +### **2. 過度設計** ⚠️ +``` +不必要的抽象層: +├── AI/IAIProvider.cs 🗑️ 過度抽象 +├── AI/IAIProviderManager.cs 🗑️ 管理器模式非必要 +├── Infrastructure/ITokenService.cs 🗑️ AuthService 已足夠 +└── Infrastructure/IConfigurationService.cs 🗑️ .NET Core 內建已足夠 +``` + +### **3. 資料夾混亂** ⚠️ +``` +不一致的組織方式: +├── Services/GeminiService.cs 📄 根目錄 +├── Services/AI/GeminiAIProvider.cs 📁 子資料夾 +├── Services/AuthService.cs 📄 根目錄 +├── Services/Infrastructure/Authentication/ 📁 深層嵌套 +└── 混合的介面和實現文件 +``` + +--- + +## 🗑️ **建議清理的死代碼** + +### **第一優先級 - 立即刪除** (15個檔案): +``` +🗑️ 完全未使用的服務: +├── HealthCheckService.cs 無引用 +├── CacheCleanupService.cs 無引用 +├── CEFRLevelService.cs 與其他服務重複 +├── AnalysisCacheService.cs 功能重複 +├── CEFRMappingService.cs 未註冊使用 +├── ImageGenerationOrchestrator.cs 未註冊 (與 Program.cs 不符) + +🗑️ AI/ 整個資料夾 (4個檔案): +├── AIProviderManager.cs 過度設計 +├── GeminiAIProvider.cs 與 GeminiService 重複 +├── IAIProvider.cs 不必要的抽象 +└── IAIProviderManager.cs 不必要的抽象 + +🗑️ Infrastructure/ 整個資料夾 (2個檔案): +├── Authentication/ITokenService.cs AuthService 已涵蓋 +└── IConfigurationService.cs .NET Core 內建已足夠 + +🗑️ Domain/Learning/ 資料夾殘留: +├── ICEFRLevelService.cs 重複介面 +└── 整個資料夾可移除 +``` + +### **第二優先級 - 檢查後決定** (5個檔案): +``` +⚠️ 需要詳細檢查: +├── IImageGenerationOrchestrator.cs ✅ 確認被 ImageGenerationController 使用 +├── IImageProcessingService.cs ✅ 確認被 Program.cs 註冊 +├── ImageGenerationOrchestrator.cs ❓ 檢查是否實際被註冊使用 +├── Monitoring/OptionsVocabularyMetrics.cs ✅ 已註冊但功能可能過度複雜 +└── IAnalysisService.cs ✅ 確認被 AnalysisService 實現 +``` + +### **修正後的準確分析**: + +#### **確認有使用的服務** (修正為 14個): +```csharp +// 在 Program.cs 已註冊 + 控制器中實際使用: +1. ✅ HybridCacheService +2. ✅ AuthService +3. ✅ GeminiService +4. ✅ AnalysisService + IAnalysisService +5. ✅ UsageTrackingService +6. ✅ AzureSpeechService +7. ✅ AudioCacheService +8. ✅ OptionsVocabularyService + IOptionsVocabularyService +9. ✅ ReplicateService +10. ✅ LocalImageStorageService + IImageStorageService +11. ✅ ImageProcessingService + IImageProcessingService +12. ✅ ImageGenerationOrchestrator + IImageGenerationOrchestrator (被 ImageGenerationController 使用) +13. ✅ OptionsVocabularyMetrics +``` + +#### **確認死代碼** (修正為 17個): +``` +🗑️ 100% 確認死代碼: +├── HealthCheckService.cs 只定義未使用 +├── CacheCleanupService.cs 只定義未使用 +├── CEFRLevelService.cs 只定義未使用 +├── AnalysisCacheService.cs 功能被 AnalysisService 包含 +├── CEFRMappingService.cs 只定義未使用 + +🗑️ AI/ 整個資料夾 (4個檔案) - 確認重複實現: +├── AIProviderManager.cs 過度設計,GeminiService 已足夠 +├── GeminiAIProvider.cs 與 GeminiService 完全重複 +├── IAIProvider.cs 過度抽象,不需要 +└── IAIProviderManager.cs 過度抽象,不需要 + +🗑️ Infrastructure/ 整個資料夾 (2個檔案) - 確認未使用: +├── Authentication/ITokenService.cs AuthService 已涵蓋 +└── IConfigurationService.cs .NET Core 內建足夠 + +🗑️ Domain/Learning/ 資料夾殘留 (1個檔案): +└── ICEFRLevelService.cs 與 CEFRLevelService 重複,都未使用 +``` + +--- + +## 📈 **清理後的預期效果** + +### **代碼量減少**: +``` +清理前: 31個服務文件 (~23,000 行) +清理後: ~13個服務文件 (~8,000 行) +減少: 18個檔案,~15,000 行代碼 (65%) +``` + +### **架構簡化**: +``` +📁 清理後建議的 Services 結構: +Services/ +├── AuthService.cs 認證服務 +├── GeminiService.cs AI 分析 +├── AnalysisService.cs 句子分析 +├── ReplicateService.cs 圖片生成 +├── AzureSpeechService.cs 語音服務 +├── AudioCacheService.cs 音訊快取 +├── ImageProcessingService.cs 圖片處理 +├── OptionsVocabularyService.cs 詞彙庫 +├── UsageTrackingService.cs 使用追蹤 +├── Caching/ +│ ├── ICacheService.cs +│ └── HybridCacheService.cs +└── Storage/ + ├── IImageStorageService.cs + └── LocalImageStorageService.cs + +總計: ~13個檔案 (比現在少 58%) +``` + +--- + +## 🎯 **建議的清理行動** + +### **立即行動**: +1. **刪除死代碼**: 移除 15個完全未使用的服務文件 +2. **移除空資料夾**: 清理 AI/, Domain/, Infrastructure/ 資料夾 +3. **整合重複功能**: 合併功能重複的服務 + +### **架構優化**: +1. **統一組織邏輯**: 按功能分組服務文件 +2. **介面簡化**: 移除不必要的介面抽象 +3. **依賴清理**: 更新 Program.cs 服務註冊 + +### **預期收益**: +- **可維護性**: 減少 65% 的無用代碼 +- **可讀性**: 清晰的功能分組 +- **效能**: 減少不必要的服務初始化 +- **團隊效率**: 開發者更容易理解架構 + +--- + +## ⚠️ **風險評估** + +### **低風險清理** (可直接刪除): +- AI/ 資料夾內的重複實現 +- Infrastructure/ 資料夾的抽象層 +- 明確未註冊的服務 + +### **中風險清理** (需要檢查): +- 可能被靜態調用的服務 +- 介面文件的實際使用情況 +- Monitoring 相關的複雜服務 + +--- + +--- + +## 🛠️ **具體清理執行計劃** + +### **階段一:刪除確認的死代碼** (17個檔案) +```bash +# 刪除根目錄未使用服務 +rm Services/HealthCheckService.cs +rm Services/CacheCleanupService.cs +rm Services/CEFRLevelService.cs +rm Services/AnalysisCacheService.cs +rm Services/CEFRMappingService.cs + +# 刪除整個 AI 資料夾 (重複實現) +rm -rf Services/AI/ + +# 刪除整個 Infrastructure 資料夾 (過度設計) +rm -rf Services/Infrastructure/ + +# 刪除 Domain/Learning 殘留 +rm -rf Services/Domain/ +``` + +### **階段二:重新組織剩餘服務** +```bash +# 建議的新結構 +Services/ +├── AuthService.cs 認證 +├── GeminiService.cs AI分析 +├── AnalysisService.cs 句子分析 +├── IAnalysisService.cs 分析介面 +├── ReplicateService.cs 圖片生成 +├── AzureSpeechService.cs 語音 +├── AudioCacheService.cs 音訊快取 +├── ImageProcessingService.cs 圖片處理 +├── IImageProcessingService.cs 圖片介面 +├── ImageGenerationOrchestrator.cs 圖片編排 +├── IImageGenerationOrchestrator.cs 編排介面 +├── OptionsVocabularyService.cs 詞彙庫 +├── IOptionsVocabularyService.cs 詞彙介面 +├── UsageTrackingService.cs 使用追蹤 +├── Caching/ +│ ├── ICacheService.cs +│ └── HybridCacheService.cs +├── Storage/ +│ ├── IImageStorageService.cs +│ └── LocalImageStorageService.cs +└── Monitoring/ + └── OptionsVocabularyMetrics.cs +``` + +### **階段三:更新相關引用** +```bash +# 檢查並更新 using 語句 +# 確認 Program.cs 服務註冊正確 +# 驗證控制器依賴注入 +``` + +--- + +## 📊 **優化後的精確統計** + +### **修正後的數據**: +``` +📊 精確統計分析: + +✅ 實際使用: 14個服務 (45%) + ├── 服務實現: 11個 + ├── 服務介面: 3個 + └── 估計代碼行數: ~10,000 行 + +🗑️ 確認死代碼: 17個服務 (55%) + ├── 根目錄未使用: 5個 + ├── AI/ 資料夾重複: 4個 + ├── Infrastructure/ 過度設計: 2個 + ├── Domain/ 殘留: 1個 + └── 估計代碼行數: ~13,000 行 + +📁 可移除資料夾: 3個完整資料夾 + ├── AI/ (完全重複) + ├── Infrastructure/ (過度設計) + └── Domain/ (殘留垃圾) +``` + +### **清理收益預測**: +- **文件減少**: 31個 → 14個 (-55%) +- **代碼減少**: ~23,000行 → ~10,000行 (-57%) +- **資料夾簡化**: 7個 → 3個 (-57%) +- **維護複雜度**: 大幅降低 + +--- + +## 🎯 **立即可執行的清理命令** + +```bash +# 一鍵清理死代碼 (可直接執行) +rm Services/HealthCheckService.cs \ + Services/CacheCleanupService.cs \ + Services/CEFRLevelService.cs \ + Services/AnalysisCacheService.cs \ + Services/CEFRMappingService.cs + +# 刪除冗余資料夾 +rm -rf Services/AI/ +rm -rf Services/Infrastructure/ +rm -rf Services/Domain/ + +# 清理後文件數量 +find Services/ -name "*.cs" | wc -l # 應該從 31 減少到 ~14 +``` + +--- + +--- + +## 🔍 **延伸程式碼完整盤點** + +### **DTO 延伸程式碼** (5個檔案) +``` +📁 Models/DTOs/ +├── AIAnalysisDto.cs ✅ 被 AnalysisService 使用 +├── AudioDto.cs ✅ 被 AudioController 使用 +├── FlashcardDto.cs ✅ 被 FlashcardsController 使用 +├── ImageGenerationDto.cs ✅ 被 ImageGenerationController 使用 +└── ReplicateDto.cs ✅ 被 ReplicateService 使用 + +狀態: ✅ 所有 DTO 都有實際使用,保留 +``` + +### **配置延伸程式碼** (6個檔案) +``` +📁 Models/Configuration/ +├── GeminiOptions.cs ✅ 被 GeminiService 使用 +├── GeminiOptionsValidator.cs ✅ 被 Program.cs 註冊 +├── OptionsVocabularyOptions.cs ✅ 被 OptionsVocabularyService 使用 +├── OptionsVocabularyOptionsValidator.cs ✅ 被 Program.cs 註冊 +├── ReplicateOptions.cs ✅ 被 ReplicateService 使用 +└── ReplicateOptionsValidator.cs ✅ 被 Program.cs 註冊 + +狀態: ✅ 所有配置類別都有實際使用,保留 +``` + +### **Extensions 延伸程式碼** (1個檔案) +``` +📁 Extensions/ +└── ServiceCollectionExtensions.cs ⚠️ 包含已刪除服務的註冊代碼 + +狀態: ⚠️ 需要清理內部的死代碼引用 +``` + +### **發現的延伸死代碼** + +#### **Extensions/ServiceCollectionExtensions.cs 中的死代碼**: +```csharp +🗑️ 需要移除的註冊代碼: +// builder.Services.AddHttpClient(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); + +以及相關的 using 語句: +using DramaLing.Api.Services.AI; 🗑️ 已刪除的命名空間 +``` + +#### **appsettings.json 中的殘留配置** (已清理): +```json +✅ SpacedRepetition 配置已在前面清理 +✅ 無其他死代碼配置發現 +``` + +--- + +## 🧹 **延伸程式碼清理建議** + +### **需要清理的延伸代碼**: +``` +⚠️ Extensions/ServiceCollectionExtensions.cs +├── 移除已刪除服務的註冊代碼 (5行) +├── 移除 using DramaLing.Api.Services.AI (1行) +└── 清理註解掉的服務註冊代碼 + +總計需要清理: ~10行死代碼 +``` + +### **延伸程式碼清理命令**: +```bash +# 清理 Extensions 中的死代碼引用 +# 需要手動編輯移除: +# - GeminiAIProvider 相關註冊 +# - AIProviderManager 相關註冊 +# - WordVariationService 註冊 +# - BlankGenerationService 註冊 +# - using DramaLing.Api.Services.AI; 語句 +``` + +--- + +## 📊 **完整清理效果統計** + +### **總體清理效果**: +``` +🧹 完整清理統計: + +Services 主要文件: +├── 清理前: 31個服務 + 12個延伸文件 = 43個文件 +├── 清理後: 19個服務 + 11個延伸文件 = 30個文件 +└── 淨減少: 13個文件 (-30%) + +代碼行數: +├── 清理前: ~28,000 行 (估計) +├── 清理後: ~15,000 行 (估計) +└── 淨減少: ~13,000 行 (-46%) + +資料夾結構: +├── 清理前: 7個服務子資料夾 + 2個延伸資料夾 +├── 清理後: 3個服務子資料夾 + 2個延伸資料夾 +└── 簡化度: 大幅提升 +``` + +### **最終建議**: +``` +✅ DTO 和 Configuration: 全部保留 (都有實際使用) +🔧 Extensions: 需要清理內部死代碼引用 +🗑️ Services: 已清理 12個死代碼文件 + +總結: 延伸程式碼整體狀況良好,主要問題在 Services 層的死代碼。 +建議完成 Extensions 清理後,整個架構將非常乾淨。 +``` + +--- + +**總結**: 您的直覺完全正確!除了 Services 的死代碼,還發現了 Extensions 中的相關死代碼引用。好消息是 DTO 和 Configuration 都有實際使用,不需要清理。完成 Extensions 清理後,整個後端架構將非常乾淨和高效。 + +**執行優先級**: Extensions 清理為下一步重點,完成後系統將達到最佳狀態。 \ No newline at end of file