feat: 配置分离

This commit is contained in:
tbphp
2025-07-03 00:33:57 +08:00
parent 7b372de6d8
commit 1e58c3d504
15 changed files with 709 additions and 394 deletions

View File

@@ -3,7 +3,6 @@ package config
import (
"fmt"
"net/url"
"os"
"strconv"
"strings"
@@ -70,18 +69,23 @@ func (m *Manager) ReloadConfig() error {
config := &Config{
Server: types.ServerConfig{
Port: parseInteger(os.Getenv("PORT"), 3000),
Host: getEnvOrDefault("HOST", "0.0.0.0"),
ReadTimeout: parseInteger(os.Getenv("SERVER_READ_TIMEOUT"), 120),
WriteTimeout: parseInteger(os.Getenv("SERVER_WRITE_TIMEOUT"), 1800),
IdleTimeout: parseInteger(os.Getenv("SERVER_IDLE_TIMEOUT"), 120),
GracefulShutdownTimeout: parseInteger(os.Getenv("SERVER_GRACEFUL_SHUTDOWN_TIMEOUT"), 60),
Port: parseInteger(os.Getenv("PORT"), 3000),
Host: getEnvOrDefault("HOST", "0.0.0.0"),
// Server timeout configs now come from system settings, not environment
// Using defaults here, will be overridden by system settings
ReadTimeout: 120,
WriteTimeout: 1800,
IdleTimeout: 120,
GracefulShutdownTimeout: 60,
},
OpenAI: types.OpenAIConfig{
BaseURLs: parseArray(os.Getenv("OPENAI_BASE_URL"), []string{"https://api.openai.com"}),
RequestTimeout: parseInteger(os.Getenv("REQUEST_TIMEOUT"), DefaultConstants.DefaultTimeout),
ResponseTimeout: parseInteger(os.Getenv("RESPONSE_TIMEOUT"), 30),
IdleConnTimeout: parseInteger(os.Getenv("IDLE_CONN_TIMEOUT"), 120),
// OPENAI_BASE_URL is removed from environment config
// Base URLs will be configured per group
BaseURLs: []string{}, // Will be set per group
// Timeout configs now come from system settings
RequestTimeout: 30,
ResponseTimeout: 30,
IdleConnTimeout: 120,
},
Auth: types.AuthConfig{
Key: os.Getenv("AUTH_KEY"),
@@ -113,29 +117,28 @@ func (m *Manager) ReloadConfig() error {
return err
}
logrus.Info("Configuration reloaded successfully")
m.DisplayConfig()
logrus.Info("Environment configuration reloaded successfully")
return nil
}
// GetServerConfig returns server configuration
func (m *Manager) GetServerConfig() types.ServerConfig {
return m.config.Server
}
// func (m *Manager) GetServerConfig() types.ServerConfig {
// return m.config.Server
// }
// GetOpenAIConfig returns OpenAI configuration
func (m *Manager) GetOpenAIConfig() types.OpenAIConfig {
config := m.config.OpenAI
if len(config.BaseURLs) > 1 {
// Use atomic counter for thread-safe round-robin
index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1
config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))]
} else if len(config.BaseURLs) == 1 {
config.BaseURL = config.BaseURLs[0]
}
return config
}
// func (m *Manager) GetOpenAIConfig() types.OpenAIConfig {
// config := m.config.OpenAI
// if len(config.BaseURLs) > 1 {
// // Use atomic counter for thread-safe round-robin
// index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1
// config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))]
// } else if len(config.BaseURLs) == 1 {
// config.BaseURL = config.BaseURLs[0]
// }
// return config
// }
// GetAuthConfig returns authentication configuration
func (m *Manager) GetAuthConfig() types.AuthConfig {
@@ -157,6 +160,51 @@ func (m *Manager) GetLogConfig() types.LogConfig {
return m.config.Log
}
// GetEffectiveServerConfig returns server configuration merged with system settings
func (m *Manager) GetEffectiveServerConfig() types.ServerConfig {
config := m.config.Server
// Merge with system settings
settingsManager := GetSystemSettingsManager()
systemSettings := settingsManager.GetSettings()
config.ReadTimeout = systemSettings.ServerReadTimeout
config.WriteTimeout = systemSettings.ServerWriteTimeout
config.IdleTimeout = systemSettings.ServerIdleTimeout
config.GracefulShutdownTimeout = systemSettings.ServerGracefulShutdownTimeout
return config
}
// GetEffectiveOpenAIConfig returns OpenAI configuration merged with system settings and group config
func (m *Manager) GetEffectiveOpenAIConfig(groupConfig map[string]any) types.OpenAIConfig {
config := m.config.OpenAI
// Merge with system settings
settingsManager := GetSystemSettingsManager()
effectiveSettings := settingsManager.GetEffectiveConfig(groupConfig)
config.RequestTimeout = effectiveSettings.RequestTimeout
config.ResponseTimeout = effectiveSettings.ResponseTimeout
config.IdleConnTimeout = effectiveSettings.IdleConnTimeout
// Apply round-robin for multiple URLs if configured
if len(config.BaseURLs) > 1 {
index := atomic.AddUint64(&m.roundRobinCounter, 1) - 1
config.BaseURL = config.BaseURLs[index%uint64(len(config.BaseURLs))]
} else if len(config.BaseURLs) == 1 {
config.BaseURL = config.BaseURLs[0]
}
return config
}
// GetEffectiveLogConfig returns log configuration (now uses environment config only)
// func (m *Manager) GetEffectiveLogConfig() types.LogConfig {
// // Log configuration is now managed via environment variables only
// return m.config.Log
// }
// Validate validates the configuration
func (m *Manager) Validate() error {
var validationErrors []string
@@ -166,22 +214,6 @@ func (m *Manager) Validate() error {
validationErrors = append(validationErrors, fmt.Sprintf("port must be between %d-%d", DefaultConstants.MinPort, DefaultConstants.MaxPort))
}
// Validate timeout
if m.config.OpenAI.RequestTimeout < DefaultConstants.MinTimeout {
validationErrors = append(validationErrors, fmt.Sprintf("request timeout cannot be less than %ds", DefaultConstants.MinTimeout))
}
// Validate upstream URL format
if len(m.config.OpenAI.BaseURLs) == 0 {
validationErrors = append(validationErrors, "at least one upstream API URL is required")
}
for _, baseURL := range m.config.OpenAI.BaseURLs {
if _, err := url.Parse(baseURL); err != nil {
validationErrors = append(validationErrors, fmt.Sprintf("invalid upstream API URL format: %s", baseURL))
}
}
// Validate performance configuration
if m.config.Performance.MaxConcurrentRequests < 1 {
validationErrors = append(validationErrors, "max concurrent requests cannot be less than 1")
}
@@ -199,34 +231,37 @@ func (m *Manager) Validate() error {
// DisplayConfig displays current configuration information
func (m *Manager) DisplayConfig() {
serverConfig := m.GetEffectiveServerConfig()
// openaiConfig := m.GetOpenAIConfig()
authConfig := m.GetAuthConfig()
corsConfig := m.GetCORSConfig()
perfConfig := m.GetPerformanceConfig()
logConfig := m.GetLogConfig()
logrus.Info("Current Configuration:")
logrus.Infof(" Server: %s:%d", m.config.Server.Host, m.config.Server.Port)
logrus.Infof(" Upstream URLs: %s", strings.Join(m.config.OpenAI.BaseURLs, ", "))
logrus.Infof(" Request timeout: %ds", m.config.OpenAI.RequestTimeout)
logrus.Infof(" Response timeout: %ds", m.config.OpenAI.ResponseTimeout)
logrus.Infof(" Idle connection timeout: %ds", m.config.OpenAI.IdleConnTimeout)
logrus.Infof(" Server: %s:%d", serverConfig.Host, serverConfig.Port)
authStatus := "disabled"
if m.config.Auth.Enabled {
if authConfig.Enabled {
authStatus = "enabled"
}
logrus.Infof(" Authentication: %s", authStatus)
corsStatus := "disabled"
if m.config.CORS.Enabled {
if corsConfig.Enabled {
corsStatus = "enabled"
}
logrus.Infof(" CORS: %s", corsStatus)
logrus.Infof(" Max concurrent requests: %d", m.config.Performance.MaxConcurrentRequests)
logrus.Infof(" Max concurrent requests: %d", perfConfig.MaxConcurrentRequests)
gzipStatus := "disabled"
if m.config.Performance.EnableGzip {
if perfConfig.EnableGzip {
gzipStatus = "enabled"
}
logrus.Infof(" Gzip compression: %s", gzipStatus)
requestLogStatus := "enabled"
if !m.config.Log.EnableRequest {
if !logConfig.EnableRequest {
requestLogStatus = "disabled"
}
logrus.Infof(" Request logging: %s", requestLogStatus)

View File

@@ -0,0 +1,362 @@
package config
import (
"fmt"
"gpt-load/internal/db"
"gpt-load/internal/models"
"reflect"
"strconv"
"strings"
"sync"
"github.com/sirupsen/logrus"
"gorm.io/gorm/clause"
)
// SystemSettings 定义所有系统配置项
// 使用结构体标签作为唯一事实来源
type SystemSettings struct {
// 负载均衡和重试配置
BlacklistThreshold int `json:"blacklist_threshold" default:"1" desc:"Error count before blacklisting a key" validate:"min=0"`
MaxRetries int `json:"max_retries" default:"3" desc:"Maximum retry attempts with different keys" validate:"min=0"`
// 服务器超时配置 (秒)
ServerReadTimeout int `json:"server_read_timeout" default:"120" desc:"HTTP server read timeout in seconds" validate:"min=1"`
ServerWriteTimeout int `json:"server_write_timeout" default:"1800" desc:"HTTP server write timeout in seconds" validate:"min=1"`
ServerIdleTimeout int `json:"server_idle_timeout" default:"120" desc:"HTTP server idle timeout in seconds" validate:"min=1"`
ServerGracefulShutdownTimeout int `json:"server_graceful_shutdown_timeout" default:"60" desc:"Graceful shutdown timeout in seconds" validate:"min=1"`
// 请求超时配置 (秒)
RequestTimeout int `json:"request_timeout" default:"30" desc:"Request timeout in seconds" validate:"min=1"`
ResponseTimeout int `json:"response_timeout" default:"30" desc:"Response timeout in seconds (TLS handshake & response header)" validate:"min=1"`
IdleConnTimeout int `json:"idle_conn_timeout" default:"120" desc:"Idle connection timeout in seconds" validate:"min=1"`
// 性能配置
// MaxConcurrentRequests int `json:"max_concurrent_requests" default:"100" desc:"Maximum number of concurrent requests" validate:"min=1"`
// 请求日志配置(数据库日志)
RequestLogRetentionDays int `json:"request_log_retention_days" default:"30" desc:"Number of days to retain request logs in database" validate:"min=1"`
}
// GenerateSettingsMetadata 使用反射从 SystemSettings 结构体动态生成元数据
func GenerateSettingsMetadata(s *SystemSettings) []models.SystemSettingInfo {
var settingsInfo []models.SystemSettingInfo
v := reflect.ValueOf(s).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
continue
}
descTag := field.Tag.Get("desc")
defaultTag := field.Tag.Get("default")
validateTag := field.Tag.Get("validate")
var minValue *int
if strings.HasPrefix(validateTag, "min=") {
valStr := strings.TrimPrefix(validateTag, "min=")
if val, err := strconv.Atoi(valStr); err == nil {
minValue = &val
}
}
info := models.SystemSettingInfo{
Key: jsonTag,
Value: fieldValue.Interface(),
Type: field.Type.String(),
DefaultValue: defaultTag,
Description: descTag,
MinValue: minValue,
}
settingsInfo = append(settingsInfo, info)
}
return settingsInfo
}
// DefaultSystemSettings 返回默认的系统配置
func DefaultSystemSettings() SystemSettings {
s := SystemSettings{}
v := reflect.ValueOf(&s).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
defaultTag := field.Tag.Get("default")
if defaultTag == "" {
continue
}
fieldValue := v.Field(i)
if fieldValue.CanSet() {
switch fieldValue.Kind() {
case reflect.Int:
if val, err := strconv.ParseInt(defaultTag, 10, 64); err == nil {
fieldValue.SetInt(val)
}
// Add cases for other types like string, bool if needed
}
}
}
return s
}
// SystemSettingsManager 管理系统配置
type SystemSettingsManager struct {
settings SystemSettings
mu sync.RWMutex
}
var globalSystemSettings *SystemSettingsManager
var once sync.Once
// GetSystemSettingsManager 获取全局系统配置管理器单例
func GetSystemSettingsManager() *SystemSettingsManager {
once.Do(func() {
globalSystemSettings = &SystemSettingsManager{}
})
return globalSystemSettings
}
// InitializeSystemSettings 初始化系统配置到数据库
func (sm *SystemSettingsManager) InitializeSystemSettings() error {
if db.DB == nil {
return fmt.Errorf("database not initialized")
}
defaultSettings := DefaultSystemSettings()
metadata := GenerateSettingsMetadata(&defaultSettings)
for _, meta := range metadata {
var existing models.SystemSetting
err := db.DB.Where("setting_key = ?", meta.Key).First(&existing).Error
if err != nil { // Not found
setting := models.SystemSetting{
SettingKey: meta.Key,
SettingValue: fmt.Sprintf("%v", meta.DefaultValue),
Description: meta.Description,
}
if err := db.DB.Create(&setting).Error; err != nil {
logrus.Errorf("Failed to initialize setting %s: %v", setting.SettingKey, err)
return err
}
logrus.Infof("Initialized system setting: %s = %s", setting.SettingKey, setting.SettingValue)
}
}
// 加载配置到内存
return sm.LoadFromDatabase()
}
// LoadFromDatabase 从数据库加载系统配置到内存
func (sm *SystemSettingsManager) LoadFromDatabase() error {
if db.DB == nil {
return fmt.Errorf("database not initialized")
}
var settings []models.SystemSetting
if err := db.DB.Find(&settings).Error; err != nil {
return fmt.Errorf("failed to load system settings: %w", err)
}
settingsMap := make(map[string]string)
for _, setting := range settings {
settingsMap[setting.SettingKey] = setting.SettingValue
}
sm.mu.Lock()
defer sm.mu.Unlock()
// 使用默认值,然后用数据库中的值覆盖
sm.settings = DefaultSystemSettings()
sm.mapToStruct(settingsMap, &sm.settings)
logrus.Info("System settings loaded from database")
return nil
}
// GetSettings 获取当前系统配置
func (sm *SystemSettingsManager) GetSettings() SystemSettings {
sm.mu.RLock()
defer sm.mu.RUnlock()
return sm.settings
}
// UpdateSettings 更新系统配置
func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]string) error {
if db.DB == nil {
return fmt.Errorf("database not initialized")
}
// 验证配置项
if err := sm.ValidateSettings(settingsMap); err != nil {
return err
}
// 更新数据库
var settingsToUpdate []models.SystemSetting
for key, value := range settingsMap {
settingsToUpdate = append(settingsToUpdate, models.SystemSetting{
SettingKey: key,
SettingValue: value,
})
}
if len(settingsToUpdate) > 0 {
if err := db.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "setting_key"}},
DoUpdates: clause.AssignmentColumns([]string{"setting_value", "updated_at"}),
}).Create(&settingsToUpdate).Error; err != nil {
return fmt.Errorf("failed to update system settings: %w", err)
}
}
// 重新加载配置到内存
return sm.LoadFromDatabase()
}
// GetEffectiveConfig 获取有效配置 (系统配置 + 分组覆盖)
func (sm *SystemSettingsManager) GetEffectiveConfig(groupConfig map[string]any) SystemSettings {
sm.mu.RLock()
defer sm.mu.RUnlock()
// 从系统配置开始
effectiveConfig := sm.settings
v := reflect.ValueOf(&effectiveConfig).Elem()
t := v.Type()
// 创建一个从 json 标签到字段名的映射
jsonToField := make(map[string]string)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
if jsonTag != "" {
jsonToField[jsonTag] = field.Name
}
}
// 应用分组配置覆盖
for key, val := range groupConfig {
if fieldName, ok := jsonToField[key]; ok {
fieldValue := v.FieldByName(fieldName)
if fieldValue.IsValid() && fieldValue.CanSet() {
if intVal, err := interfaceToInt(val); err == nil {
fieldValue.SetInt(int64(intVal))
}
}
}
}
return effectiveConfig
}
// ValidateSettings 验证系统配置的有效性
func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]string) error {
tempSettings := DefaultSystemSettings()
v := reflect.ValueOf(&tempSettings).Elem()
t := v.Type()
jsonToField := make(map[string]reflect.StructField)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
jsonToField[jsonTag] = field
}
}
for key, value := range settingsMap {
field, ok := jsonToField[key]
if !ok {
return fmt.Errorf("invalid setting key: %s", key)
}
validateTag := field.Tag.Get("validate")
if validateTag == "" {
continue
}
switch field.Type.Kind() {
case reflect.Int:
intVal, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid integer value for %s: %s", key, value)
}
if strings.HasPrefix(validateTag, "min=") {
minValStr := strings.TrimPrefix(validateTag, "min=")
minVal, _ := strconv.Atoi(minValStr)
if intVal < minVal {
return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal)
}
}
default:
return fmt.Errorf("unsupported type for setting key validation: %s", key)
}
}
return nil
}
// DisplayCurrentSettings 显示当前系统配置信息
func (sm *SystemSettingsManager) DisplayCurrentSettings() {
sm.mu.RLock()
defer sm.mu.RUnlock()
logrus.Info("Current System Settings:")
logrus.Infof(" Blacklist threshold: %d", sm.settings.BlacklistThreshold)
logrus.Infof(" Max retries: %d", sm.settings.MaxRetries)
logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds",
sm.settings.ServerReadTimeout, sm.settings.ServerWriteTimeout,
sm.settings.ServerIdleTimeout, sm.settings.ServerGracefulShutdownTimeout)
logrus.Infof(" Request timeouts: request=%ds, response=%ds, idle_conn=%ds",
sm.settings.RequestTimeout, sm.settings.ResponseTimeout, sm.settings.IdleConnTimeout)
// logrus.Infof(" Performance: max_concurrent=%d", sm.settings.MaxConcurrentRequests)
logrus.Infof(" Request log retention: %d days", sm.settings.RequestLogRetentionDays)
}
// 辅助方法
func (sm *SystemSettingsManager) mapToStruct(m map[string]string, s *SystemSettings) {
v := reflect.ValueOf(s).Elem()
t := v.Type()
// 创建一个从 json 标签到字段名的映射
jsonToField := make(map[string]string)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
if jsonTag != "" {
jsonToField[jsonTag] = field.Name
}
}
for key, valStr := range m {
if fieldName, ok := jsonToField[key]; ok {
fieldValue := v.FieldByName(fieldName)
if fieldValue.IsValid() && fieldValue.CanSet() {
// 假设所有字段都是 int 类型
if intVal, err := strconv.Atoi(valStr); err == nil {
fieldValue.SetInt(int64(intVal))
}
}
}
}
}
// 工具函数
func interfaceToInt(val interface{}) (int, error) {
switch v := val.(type) {
case int:
return v, nil
case float64:
return int(v), nil
case string:
return strconv.Atoi(v)
default:
return 0, fmt.Errorf("cannot convert to int: %v", val)
}
}

View File

@@ -49,7 +49,7 @@ func (s *Server) Login(c *gin.Context) {
}
authConfig := s.config.GetAuthConfig()
if !authConfig.Enabled {
c.JSON(http.StatusOK, LoginResponse{
Success: true,
@@ -102,70 +102,3 @@ func (s *Server) Health(c *gin.Context) {
"uptime": uptime,
})
}
// GetConfig returns configuration information (for debugging)
func (s *Server) GetConfig(c *gin.Context) {
// Only allow in development mode or with special header
if c.GetHeader("X-Debug-Config") != "true" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Access denied",
})
return
}
serverConfig := s.config.GetServerConfig()
openaiConfig := s.config.GetOpenAIConfig()
authConfig := s.config.GetAuthConfig()
corsConfig := s.config.GetCORSConfig()
perfConfig := s.config.GetPerformanceConfig()
logConfig := s.config.GetLogConfig()
// Sanitize sensitive information
sanitizedConfig := gin.H{
"server": gin.H{
"host": serverConfig.Host,
"port": serverConfig.Port,
},
"openai": gin.H{
"base_url": openaiConfig.BaseURL,
"request_timeout": openaiConfig.RequestTimeout,
"response_timeout": openaiConfig.ResponseTimeout,
"idle_conn_timeout": openaiConfig.IdleConnTimeout,
},
"auth": gin.H{
"enabled": authConfig.Enabled,
// Don't expose the actual key
},
"cors": gin.H{
"enabled": corsConfig.Enabled,
"allowed_origins": corsConfig.AllowedOrigins,
"allowed_methods": corsConfig.AllowedMethods,
"allowed_headers": corsConfig.AllowedHeaders,
"allow_credentials": corsConfig.AllowCredentials,
},
"performance": gin.H{
"max_concurrent_requests": perfConfig.MaxConcurrentRequests,
"enable_gzip": perfConfig.EnableGzip,
},
"timeout_config": gin.H{
"request_timeout_s": openaiConfig.RequestTimeout,
"response_timeout_s": openaiConfig.ResponseTimeout,
"idle_conn_timeout_s": openaiConfig.IdleConnTimeout,
"server_read_timeout_s": serverConfig.ReadTimeout,
"server_write_timeout_s": serverConfig.WriteTimeout,
"server_idle_timeout_s": serverConfig.IdleTimeout,
"graceful_shutdown_timeout_s": serverConfig.GracefulShutdownTimeout,
},
"log": gin.H{
"level": logConfig.Level,
"format": logConfig.Format,
"enable_file": logConfig.EnableFile,
"file_path": logConfig.FilePath,
"enable_request": logConfig.EnableRequest,
},
"timestamp": time.Now().UTC().Format(time.RFC3339),
}
c.JSON(http.StatusOK, sanitizedConfig)
}

View File

@@ -0,0 +1,34 @@
package handler
import (
"gpt-load/internal/response"
"gpt-load/internal/services"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// LogCleanupHandler handles log cleanup related requests
type LogCleanupHandler struct {
LogCleanupService *services.LogCleanupService
}
// NewLogCleanupHandler creates a new LogCleanupHandler
func NewLogCleanupHandler(s *services.LogCleanupService) *LogCleanupHandler {
return &LogCleanupHandler{
LogCleanupService: s,
}
}
// CleanupLogsNow handles the POST /api/logs/cleanup request.
// It triggers an asynchronous cleanup of expired request logs.
func (h *LogCleanupHandler) CleanupLogsNow(c *gin.Context) {
go func() {
logrus.Info("Asynchronous log cleanup started from API request")
h.LogCleanupService.CleanupNow()
}()
response.Success(c, gin.H{
"message": "Log cleanup process started in the background",
})
}

View File

@@ -1,26 +0,0 @@
package handler
import (
"gpt-load/internal/response"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// ReloadConfig handles the POST /api/reload request.
// It triggers a configuration reload.
func (s *Server) ReloadConfig(c *gin.Context) {
if s.config == nil {
response.InternalError(c, "Configuration manager is not initialized")
return
}
err := s.config.ReloadConfig()
if err != nil {
logrus.Errorf("Failed to reload config: %v", err)
response.InternalError(c, "Failed to reload config")
return
}
response.Success(c, gin.H{"message": "Configuration reloaded successfully"})
}

View File

@@ -1,33 +1,28 @@
package handler
import (
"gpt-load/internal/db"
"gpt-load/internal/models"
"gpt-load/internal/config"
"gpt-load/internal/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm/clause"
"github.com/sirupsen/logrus"
)
// GetSettings handles the GET /api/settings request.
// It retrieves all system settings from the database and returns them as a key-value map.
// It retrieves all system settings and returns them with detailed information.
func GetSettings(c *gin.Context) {
var settings []models.SystemSetting
if err := db.DB.Find(&settings).Error; err != nil {
response.InternalError(c, "Failed to retrieve settings")
return
}
settingsManager := config.GetSystemSettingsManager()
currentSettings := settingsManager.GetSettings()
settingsMap := make(map[string]string)
for _, s := range settings {
settingsMap[s.SettingKey] = s.SettingValue
}
// 使用新的动态元数据生成器
settingsInfo := config.GenerateSettingsMetadata(&currentSettings)
response.Success(c, settingsMap)
response.Success(c, settingsInfo)
}
// UpdateSettings handles the PUT /api/settings request.
// It receives a key-value JSON object and updates or creates settings in the database.
// It receives a key-value JSON object and updates system settings.
// After updating, it triggers a configuration reload.
func UpdateSettings(c *gin.Context) {
var settingsMap map[string]string
if err := c.ShouldBindJSON(&settingsMap); err != nil {
@@ -35,28 +30,37 @@ func UpdateSettings(c *gin.Context) {
return
}
var settingsToUpdate []models.SystemSetting
for key, value := range settingsMap {
settingsToUpdate = append(settingsToUpdate, models.SystemSetting{
SettingKey: key,
SettingValue: value,
})
}
if len(settingsToUpdate) == 0 {
if len(settingsMap) == 0 {
response.Success(c, nil)
return
}
// Using OnConflict to perform an "upsert" operation.
// If a setting with the same key exists, it will be updated. Otherwise, a new one will be created.
if err := db.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "setting_key"}},
DoUpdates: clause.AssignmentColumns([]string{"setting_value"}),
}).Create(&settingsToUpdate).Error; err != nil {
response.InternalError(c, "Failed to update settings")
settingsManager := config.GetSystemSettingsManager()
// 更新配置
if err := settingsManager.UpdateSettings(settingsMap); err != nil {
response.InternalError(c, "Failed to update settings: "+err.Error())
return
}
response.Success(c, nil)
}
// 重载系统配置
if err := settingsManager.LoadFromDatabase(); err != nil {
logrus.Errorf("Failed to reload system settings: %v", err)
response.InternalError(c, "Failed to reload system settings")
return
}
settingsManager.DisplayCurrentSettings()
logrus.Info("Configuration reloaded successfully via API")
response.Success(c, gin.H{
"message": "Configuration reloaded successfully",
"timestamp": gin.H{
"reloaded_at": "now",
},
})
response.Success(c, gin.H{
"message": "Settings updated successfully. Configuration reloaded.",
})
}

View File

@@ -0,0 +1,15 @@
package models
// SystemSettingInfo 表示系统配置的详细信息用于API返回
type SystemSettingInfo struct {
Key string `json:"key"`
Value interface{} `json:"value"`
Type string `json:"type"` // "int", "bool", "string"
DefaultValue interface{} `json:"default_value"`
Description string `json:"description"`
Category string `json:"category"` // "timeout", "performance", "logging", etc.
Required bool `json:"required"`
MinValue *int `json:"min_value,omitempty"`
MaxValue *int `json:"max_value,omitempty"`
ValidOptions []string `json:"valid_options,omitempty"`
}

View File

@@ -63,4 +63,4 @@ type DashboardStats struct {
SuccessRequests int64 `json:"success_requests"`
SuccessRate float64 `json:"success_rate"`
GroupStats []GroupRequestStat `json:"group_stats"`
}
}

View File

@@ -39,6 +39,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
func New(
serverHandler *handler.Server,
proxyServer *proxy.ProxyServer,
logCleanupHandler *handler.LogCleanupHandler,
configManager types.ConfigManager,
buildFS embed.FS,
indexPage []byte,
@@ -60,7 +61,7 @@ func New(
// 注册路由
registerSystemRoutes(router, serverHandler)
registerAPIRoutes(router, serverHandler, configManager)
registerAPIRoutes(router, serverHandler, logCleanupHandler, configManager)
registerProxyRoutes(router, proxyServer, configManager)
registerFrontendRoutes(router, buildFS, indexPage)
@@ -71,11 +72,10 @@ func New(
func registerSystemRoutes(router *gin.Engine, serverHandler *handler.Server) {
router.GET("/health", serverHandler.Health)
router.GET("/stats", serverHandler.Stats)
// router.GET("/config", serverHandler.GetConfig)
}
// registerAPIRoutes 注册API路由
func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, configManager types.ConfigManager) {
func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, logCleanupHandler *handler.LogCleanupHandler, configManager types.ConfigManager) {
api := router.Group("/api")
authConfig := configManager.GetAuthConfig()
@@ -86,9 +86,9 @@ func registerAPIRoutes(router *gin.Engine, serverHandler *handler.Server, config
if authConfig.Enabled {
protectedAPI := api.Group("")
protectedAPI.Use(middleware.Auth(authConfig))
registerProtectedAPIRoutes(protectedAPI, serverHandler)
registerProtectedAPIRoutes(protectedAPI, serverHandler, logCleanupHandler)
} else {
registerProtectedAPIRoutes(api, serverHandler)
registerProtectedAPIRoutes(api, serverHandler, logCleanupHandler)
}
}
@@ -98,7 +98,7 @@ func registerPublicAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server
}
// registerProtectedAPIRoutes 认证API路由
func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server) {
func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Server, logCleanupHandler *handler.LogCleanupHandler) {
groups := api.Group("/groups")
{
groups.POST("", serverHandler.CreateGroup)
@@ -123,7 +123,11 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
}
// 日志
api.GET("/logs", handler.GetLogs)
logs := api.Group("/logs")
{
logs.GET("", handler.GetLogs)
logs.POST("/cleanup", logCleanupHandler.CleanupLogsNow)
}
// 设置
settings := api.Group("/settings")
@@ -131,9 +135,6 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
settings.GET("", handler.GetSettings)
settings.PUT("", handler.UpdateSettings)
}
// 重载配置
api.POST("/reload", serverHandler.ReloadConfig)
}
// registerProxyRoutes 注册代理路由

View File

@@ -0,0 +1,102 @@
package services
import (
"gpt-load/internal/config"
"gpt-load/internal/db"
"gpt-load/internal/models"
"time"
"github.com/sirupsen/logrus"
)
// LogCleanupService 负责清理过期的请求日志
type LogCleanupService struct {
stopCh chan struct{}
}
// NewLogCleanupService 创建新的日志清理服务
func NewLogCleanupService() *LogCleanupService {
return &LogCleanupService{
stopCh: make(chan struct{}),
}
}
// Start 启动日志清理服务
func (s *LogCleanupService) Start() {
go s.run()
logrus.Info("Log cleanup service started")
}
// Stop 停止日志清理服务
func (s *LogCleanupService) Stop() {
close(s.stopCh)
logrus.Info("Log cleanup service stopped")
}
// run 运行日志清理的主循环
func (s *LogCleanupService) run() {
// 每天凌晨2点执行清理任务
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
// 启动时先执行一次清理
s.cleanupExpiredLogs()
for {
select {
case <-ticker.C:
s.cleanupExpiredLogs()
case <-s.stopCh:
return
}
}
}
// cleanupExpiredLogs 清理过期的请求日志
func (s *LogCleanupService) cleanupExpiredLogs() {
if db.DB == nil {
logrus.Error("Database connection is not available for log cleanup")
return
}
// 获取日志保留天数配置
settingsManager := config.GetSystemSettingsManager()
if settingsManager == nil {
logrus.Error("System settings manager is not available for log cleanup")
return
}
settings := settingsManager.GetSettings()
retentionDays := settings.RequestLogRetentionDays
if retentionDays <= 0 {
logrus.Debug("Log retention is disabled (retention_days <= 0)")
return
}
// 计算过期时间点
cutoffTime := time.Now().AddDate(0, 0, -retentionDays).UTC()
// 执行删除操作
result := db.DB.Where("timestamp < ?", cutoffTime).Delete(&models.RequestLog{})
if result.Error != nil {
logrus.WithError(result.Error).Error("Failed to cleanup expired request logs")
return
}
if result.RowsAffected > 0 {
logrus.WithFields(logrus.Fields{
"deleted_count": result.RowsAffected,
"cutoff_time": cutoffTime.Format(time.RFC3339),
"retention_days": retentionDays,
}).Info("Successfully cleaned up expired request logs")
} else {
logrus.Debug("No expired request logs found to cleanup")
}
}
// CleanupNow 立即执行一次日志清理
func (s *LogCleanupService) CleanupNow() {
logrus.Info("Manual log cleanup triggered")
s.cleanupExpiredLogs()
}

View File

@@ -7,12 +7,19 @@ import (
// ConfigManager defines the interface for configuration management
type ConfigManager interface {
GetServerConfig() ServerConfig
GetOpenAIConfig() OpenAIConfig
// GetServerConfig() ServerConfig
// GetOpenAIConfig() OpenAIConfig
GetAuthConfig() AuthConfig
GetCORSConfig() CORSConfig
GetPerformanceConfig() PerformanceConfig
GetLogConfig() LogConfig
// Effective configuration methods that merge system settings
GetEffectiveServerConfig() ServerConfig
GetEffectiveOpenAIConfig(groupConfig map[string]any) OpenAIConfig
// GetEffectivePerformanceConfig() PerformanceConfig
// GetEffectiveLogConfig() LogConfig
Validate() error
DisplayConfig()
ReloadConfig() error