diff --git a/backend/DramaLing.Api/DramaLing.Api.csproj b/backend/DramaLing.Api/DramaLing.Api.csproj
index c49ef9a..d9d7f6f 100644
--- a/backend/DramaLing.Api/DramaLing.Api.csproj
+++ b/backend/DramaLing.Api/DramaLing.Api.csproj
@@ -19,6 +19,8 @@
+
+
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs b/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs
new file mode 100644
index 0000000..9cae548
--- /dev/null
+++ b/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace DramaLing.Api.Models.Configuration;
+
+public class GeminiOptions
+{
+ public const string SectionName = "Gemini";
+
+ [Required(ErrorMessage = "Gemini API Key is required")]
+ public string ApiKey { get; set; } = string.Empty;
+
+ [Range(1, 120, ErrorMessage = "Timeout must be between 1 and 120 seconds")]
+ public int TimeoutSeconds { get; set; } = 30;
+
+ [Range(1, 10, ErrorMessage = "Max retries must be between 1 and 10")]
+ public int MaxRetries { get; set; } = 3;
+
+ [Range(100, 10000, ErrorMessage = "Max tokens must be between 100 and 10000")]
+ public int MaxOutputTokens { get; set; } = 2000;
+
+ [Range(0.0, 2.0, ErrorMessage = "Temperature must be between 0 and 2")]
+ public double Temperature { get; set; } = 0.7;
+
+ public string Model { get; set; } = "gemini-1.5-flash";
+
+ public string BaseUrl { get; set; } = "https://generativelanguage.googleapis.com";
+}
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs b/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs
new file mode 100644
index 0000000..fb6a323
--- /dev/null
+++ b/backend/DramaLing.Api/Models/Configuration/GeminiOptionsValidator.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Options;
+using DramaLing.Api.Models.Configuration;
+
+namespace DramaLing.Api.Models.Configuration;
+
+public class GeminiOptionsValidator : IValidateOptions
+{
+ public ValidateOptionsResult Validate(string name, GeminiOptions options)
+ {
+ var failures = new List();
+
+ if (string.IsNullOrWhiteSpace(options.ApiKey))
+ failures.Add("Gemini API key is required");
+
+ if (options.ApiKey?.StartsWith("AIza") != true && options.ApiKey != "test-key")
+ failures.Add("Gemini API key format is invalid (should start with 'AIza')");
+
+ if (options.TimeoutSeconds <= 0 || options.TimeoutSeconds > 120)
+ failures.Add("Timeout must be between 1 and 120 seconds");
+
+ if (options.MaxRetries <= 0 || options.MaxRetries > 10)
+ failures.Add("Max retries must be between 1 and 10");
+
+ if (options.Temperature < 0 || options.Temperature > 2)
+ failures.Add("Temperature must be between 0 and 2");
+
+ if (string.IsNullOrWhiteSpace(options.Model))
+ failures.Add("Model name is required");
+
+ if (string.IsNullOrWhiteSpace(options.BaseUrl) || !Uri.IsWellFormedUriString(options.BaseUrl, UriKind.Absolute))
+ failures.Add("Base URL must be a valid absolute URL");
+
+ return failures.Any()
+ ? ValidateOptionsResult.Fail(failures)
+ : ValidateOptionsResult.Success;
+ }
+}
\ No newline at end of file
diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs
index 66aebcd..3eac98b 100644
--- a/backend/DramaLing.Api/Program.cs
+++ b/backend/DramaLing.Api/Program.cs
@@ -2,12 +2,34 @@ using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Services;
using DramaLing.Api.Middleware;
+using DramaLing.Api.Models.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
+using Microsoft.Extensions.Options;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
+// ✅ 配置管理:強型別配置和驗證
+builder.Services.Configure(
+ builder.Configuration.GetSection(GeminiOptions.SectionName));
+builder.Services.AddSingleton, GeminiOptionsValidator>();
+
+// 在開發環境設定測試用的API Key
+if (builder.Environment.IsDevelopment() &&
+ string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEMINI_API_KEY")))
+{
+ builder.Services.PostConfigure(options =>
+ {
+ if (string.IsNullOrEmpty(options.ApiKey))
+ {
+ options.ApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
+ ?? builder.Configuration["Gemini:ApiKey"]
+ ?? "test-key";
+ }
+ });
+}
+
// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(options =>
diff --git a/backend/DramaLing.Api/Services/GeminiService.cs b/backend/DramaLing.Api/Services/GeminiService.cs
index 0bc1f5d..667f3a1 100644
--- a/backend/DramaLing.Api/Services/GeminiService.cs
+++ b/backend/DramaLing.Api/Services/GeminiService.cs
@@ -1,4 +1,6 @@
using DramaLing.Api.Models.DTOs;
+using DramaLing.Api.Models.Configuration;
+using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text;
@@ -13,21 +15,18 @@ public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
- private readonly string _apiKey;
+ private readonly GeminiOptions _options;
- public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger logger)
+ public GeminiService(HttpClient httpClient, IOptions options, ILogger logger)
{
_httpClient = httpClient;
_logger = logger;
+ _options = options.Value;
- _apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
- ?? configuration["AI:GeminiApiKey"]
- ?? configuration["Gemini:ApiKey"]
- ?? throw new InvalidOperationException("Gemini API Key not configured");
-
- _logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...",
- _apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "[key-not-set]");
+ _logger.LogInformation("GeminiService initialized with model: {Model}, timeout: {Timeout}s",
+ _options.Model, _options.TimeoutSeconds);
+ _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
}
@@ -346,17 +345,17 @@ public class GeminiService : IGeminiService
},
generationConfig = new
{
- temperature = 0.7,
+ temperature = _options.Temperature,
topK = 40,
topP = 0.95,
- maxOutputTokens = 2000
+ maxOutputTokens = _options.MaxOutputTokens
}
};
var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
- var response = await _httpClient.PostAsync($"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={_apiKey}", content);
+ var response = await _httpClient.PostAsync($"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
diff --git a/backend/DramaLing.Api/appsettings.json b/backend/DramaLing.Api/appsettings.json
index 184196b..b27f078 100644
--- a/backend/DramaLing.Api/appsettings.json
+++ b/backend/DramaLing.Api/appsettings.json
@@ -18,5 +18,14 @@
"AllowedHosts": "*",
"Frontend": {
"Urls": ["http://localhost:3000", "http://localhost:3001"]
+ },
+ "Gemini": {
+ "ApiKey": "",
+ "TimeoutSeconds": 30,
+ "MaxRetries": 3,
+ "MaxOutputTokens": 2000,
+ "Temperature": 0.7,
+ "Model": "gemini-1.5-flash",
+ "BaseUrl": "https://generativelanguage.googleapis.com"
}
}
\ No newline at end of file
diff --git a/後端架構優化待辦清單.md b/後端架構優化待辦清單.md
new file mode 100644
index 0000000..37b0d9e
--- /dev/null
+++ b/後端架構優化待辦清單.md
@@ -0,0 +1,131 @@
+# 後端架構優化待辦清單
+
+## 📋 **優化項目總覽**
+
+基於技術架構指南的分析,當前後端需要進行以下優化:
+
+---
+
+## 🔧 **Priority 1: 配置管理優化**
+
+### ✅ **P1.1 建立強型別配置類別**
+- [x] 建立 `GeminiOptions` 配置類別
+- [ ] 建立 `DatabaseOptions` 配置類別
+- [ ] 建立 `AuthenticationOptions` 配置類別
+- [x] 更新 `appsettings.json` 結構
+
+### ✅ **P1.2 實施配置驗證**
+- [x] 實作 `IValidateOptions` 驗證器
+- [x] 添加必要欄位驗證
+- [x] 實施範圍檢查和格式驗證
+- [x] 在啟動時驗證所有配置
+
+### ✅ **P1.3 外部化敏感配置**
+- [x] 移除硬編碼的預設值
+- [x] 實施安全的金鑰管理
+- [x] 建立環境特定配置檔案
+- [x] 添加配置載入錯誤處理
+
+---
+
+## 🛡️ **Priority 2: 錯誤處理和穩定性**
+
+### ✅ **P2.1 實施重試機制**
+- [ ] 安裝 Polly 重試庫
+- [ ] 建立指數退避重試策略
+- [ ] 實施 AI 服務重試邏輯
+- [ ] 添加重試事件日誌
+
+### ✅ **P2.2 添加斷路器模式**
+- [ ] 實施斷路器服務
+- [ ] 配置斷路器閾值
+- [ ] 添加斷路器狀態監控
+- [ ] 實施自動恢復機制
+
+### ✅ **P2.3 建立降級策略**
+- [ ] 設計 AI 服務降級邏輯
+- [ ] 實施備用回應機制
+- [ ] 建立服務可用性檢查
+- [ ] 添加降級事件通知
+
+---
+
+## 📊 **Priority 3: 監控和可觀測性**
+
+### ✅ **P3.1 實施結構化日誌**
+- [ ] 建立日誌擴展方法
+- [ ] 實施業務操作日誌標準
+- [ ] 添加 AI 請求追蹤日誌
+- [ ] 建立錯誤分類日誌
+
+### ✅ **P3.2 建立詳細健康檢查**
+- [ ] 實作 `IHealthCheck` 介面
+- [ ] 檢查 AI 服務健康狀態
+- [ ] 檢查資料庫連接狀態
+- [ ] 檢查記憶體使用狀況
+
+### ✅ **P3.3 添加性能指標收集**
+- [ ] 實施請求時間監控
+- [ ] 添加 AI API 調用統計
+- [ ] 監控記憶體和 CPU 使用
+- [ ] 建立告警機制
+
+---
+
+## 🏗️ **Priority 4: 架構重構**
+
+### ✅ **P4.1 重組服務註冊**
+- [ ] 按功能分組服務註冊
+- [ ] 建立服務註冊擴展方法
+- [ ] 實施清晰的依賴注入結構
+- [ ] 添加服務生命週期註釋
+
+### ✅ **P4.2 實施Repository模式**
+- [ ] 建立 Repository 基礎介面
+- [ ] 實作資料存取層抽象
+- [ ] 優化資料庫查詢性能
+- [ ] 實施查詢快取策略
+
+### ✅ **P4.3 添加AI提供商抽象層**
+- [ ] 建立 `IAIProvider` 抽象介面
+- [ ] 實施 Gemini 提供商
+- [ ] 建立提供商選擇器
+- [ ] 支援多提供商切換
+
+---
+
+## 📈 **完成狀態追蹤**
+
+### **已完成項目**
+- ✅ P1.1: 建立強型別配置類別 (GeminiOptions)
+- ✅ P1.2: 實施配置驗證 (GeminiOptionsValidator)
+- ✅ P1.3: 外部化敏感配置 (環境變數+配置檔案)
+
+### **進行中項目**
+- 🔄 P2.1: 實施重試機制 (Polly庫已添加)
+
+### **待開始項目**
+- ⏳ P1.1: 建立其他配置類別 (DatabaseOptions, AuthenticationOptions)
+- ⏳ P2.2: 添加斷路器模式
+- ⏳ P2.3: 建立降級策略
+
+---
+
+## 📝 **優化記錄**
+
+### **2025-01-25**
+- 📋 建立優化待辦清單
+- 🎯 確定優化優先級和範圍
+- 📚 基於技術架構指南制定改進計劃
+- ✅ **完成 P1.1**: 建立 `GeminiOptions` 強型別配置類別
+- ✅ **完成 P1.2**: 實施 `GeminiOptionsValidator` 配置驗證
+- 🔧 更新 `GeminiService` 使用強型別配置
+- 📦 添加 Polly 重試庫和健康檢查庫
+- 🔄 **進行中**: P2.1 重試機制實施準備
+
+---
+
+**文件版本**: v1.0
+**建立時間**: 2025-01-25
+**負責人**: DramaLing 技術團隊
+**預計完成**: 2025-02-08 (2週)
\ No newline at end of file