feat: 引入dig服务依赖管理

This commit is contained in:
tbphp
2025-07-06 22:44:41 +08:00
parent a992c28593
commit 3b6468af83
12 changed files with 404 additions and 266 deletions

181
internal/app/app.go Normal file
View File

@@ -0,0 +1,181 @@
// Package app provides the main application logic and lifecycle management.
package app
import (
"context"
"fmt"
"net/http"
"sync"
"time"
"gpt-load/internal/config"
"gpt-load/internal/models"
"gpt-load/internal/proxy"
"gpt-load/internal/services"
"gpt-load/internal/store"
"gpt-load/internal/types"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"go.uber.org/dig"
"gorm.io/gorm"
)
// App holds all services and manages the application lifecycle.
type App struct {
engine *gin.Engine
configManager types.ConfigManager
settingsManager *config.SystemSettingsManager
logCleanupService *services.LogCleanupService
keyCronService *services.KeyCronService
keyValidationPool *services.KeyValidationPool
proxyServer *proxy.ProxyServer
storage store.Store
db *gorm.DB
httpServer *http.Server
requestLogChan chan models.RequestLog
wg sync.WaitGroup
}
// AppParams defines the dependencies for the App.
type AppParams struct {
dig.In
Engine *gin.Engine
ConfigManager types.ConfigManager
SettingsManager *config.SystemSettingsManager
LogCleanupService *services.LogCleanupService
KeyCronService *services.KeyCronService
KeyValidationPool *services.KeyValidationPool
ProxyServer *proxy.ProxyServer
Storage store.Store
DB *gorm.DB
RequestLogChan chan models.RequestLog
}
// NewApp is the constructor for App, with dependencies injected by dig.
func NewApp(params AppParams) *App {
return &App{
engine: params.Engine,
configManager: params.ConfigManager,
settingsManager: params.SettingsManager,
logCleanupService: params.LogCleanupService,
keyCronService: params.KeyCronService,
keyValidationPool: params.KeyValidationPool,
proxyServer: params.ProxyServer,
storage: params.Storage,
db: params.DB,
requestLogChan: params.RequestLogChan,
}
}
// Start runs the application, it is a non-blocking call.
func (a *App) Start() error {
// Initialize system settings
if err := a.settingsManager.InitializeSystemSettings(); err != nil {
return fmt.Errorf("failed to initialize system settings: %w", err)
}
logrus.Info("System settings initialized")
a.settingsManager.DisplayCurrentSettings()
a.configManager.DisplayConfig()
// Start background services
a.startRequestLogger()
a.logCleanupService.Start()
a.keyValidationPool.Start()
a.keyCronService.Start()
// Create HTTP server
serverConfig := a.configManager.GetEffectiveServerConfig()
a.httpServer = &http.Server{
Addr: fmt.Sprintf("%s:%d", serverConfig.Host, serverConfig.Port),
Handler: a.engine,
ReadTimeout: time.Duration(serverConfig.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(serverConfig.WriteTimeout) * time.Second,
IdleTimeout: time.Duration(serverConfig.IdleTimeout) * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB header limit
}
// Start HTTP server in a new goroutine
go func() {
logrus.Info("GPT-Load proxy server started successfully")
logrus.Infof("Server address: http://%s:%d", serverConfig.Host, serverConfig.Port)
logrus.Infof("Statistics: http://%s:%d/stats", serverConfig.Host, serverConfig.Port)
logrus.Infof("Health check: http://%s:%d/health", serverConfig.Host, serverConfig.Port)
logrus.Info("")
if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logrus.Fatalf("Server startup failed: %v", err)
}
}()
return nil
}
// Stop gracefully shuts down the application.
func (a *App) Stop(ctx context.Context) {
logrus.Info("Shutting down server...")
// Shutdown http server
if err := a.httpServer.Shutdown(ctx); err != nil {
logrus.Errorf("Server forced to shutdown: %v", err)
}
// Stop background services
a.keyCronService.Stop()
a.keyValidationPool.Stop()
a.logCleanupService.Stop()
// Close resources
a.proxyServer.Close()
a.storage.Close()
// Wait for the logger to finish writing all logs
logrus.Info("Closing request log channel...")
close(a.requestLogChan)
a.wg.Wait()
logrus.Info("All logs have been written.")
logrus.Info("Server exited gracefully")
}
// startRequestLogger runs a background goroutine to batch-insert request logs.
func (a *App) startRequestLogger() {
a.wg.Add(1)
go func() {
defer a.wg.Done()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
logBuffer := make([]models.RequestLog, 0, 100)
for {
select {
case logEntry, ok := <-a.requestLogChan:
if !ok {
// Channel closed, flush remaining logs and exit
if len(logBuffer) > 0 {
if err := a.db.Create(&logBuffer).Error; err != nil {
logrus.Errorf("Failed to write remaining request logs: %v", err)
}
}
logrus.Info("Request logger stopped.")
return
}
logBuffer = append(logBuffer, logEntry)
if len(logBuffer) >= 100 {
if err := a.db.Create(&logBuffer).Error; err != nil {
logrus.Errorf("Failed to write request logs: %v", err)
}
logBuffer = make([]models.RequestLog, 0, 100) // Reset buffer
}
case <-ticker.C:
// Flush logs periodically
if len(logBuffer) > 0 {
if err := a.db.Create(&logBuffer).Error; err != nil {
logrus.Errorf("Failed to write request logs on tick: %v", err)
}
logBuffer = make([]models.RequestLog, 0, 100) // Reset buffer
}
}
}
}()
}

View File

@@ -3,7 +3,9 @@ package config
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
@@ -36,7 +38,8 @@ var DefaultConstants = Constants{
// Manager implements the ConfigManager interface
type Manager struct {
config *Config
config *Config
settingsManager *SystemSettingsManager
}
// Config represents the application configuration
@@ -52,8 +55,10 @@ type Config struct {
}
// NewManager creates a new configuration manager
func NewManager() (types.ConfigManager, error) {
manager := &Manager{}
func NewManager(settingsManager *SystemSettingsManager) (types.ConfigManager, error) {
manager := &Manager{
settingsManager: settingsManager,
}
if err := manager.ReloadConfig(); err != nil {
return nil, err
}
@@ -170,8 +175,7 @@ func (m *Manager) GetEffectiveServerConfig() types.ServerConfig {
config := m.config.Server
// Merge with system settings from database
settingsManager := GetSystemSettingsManager()
systemSettings := settingsManager.GetSettings()
systemSettings := m.settingsManager.GetSettings()
config.ReadTimeout = systemSettings.ServerReadTimeout
config.WriteTimeout = systemSettings.ServerWriteTimeout
@@ -313,3 +317,45 @@ func (s *SystemSettingsManager) GetInt(key string, defaultValue int) int {
}
return defaultValue
}
// SetupLogger configures the logging system based on the provided configuration.
func SetupLogger(configManager types.ConfigManager) {
logConfig := configManager.GetLogConfig()
// Set log level
level, err := logrus.ParseLevel(logConfig.Level)
if err != nil {
logrus.Warn("Invalid log level, using info")
level = logrus.InfoLevel
}
logrus.SetLevel(level)
// Set log format
if logConfig.Format == "json" {
logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z07:00", // ISO 8601 format
})
} else {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
}
// Setup file logging if enabled
if logConfig.EnableFile {
logDir := filepath.Dir(logConfig.FilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
logrus.Warnf("Failed to create log directory: %v", err)
} else {
logFile, err := os.OpenFile(logConfig.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logrus.Warnf("Failed to open log file: %v", err)
} else {
logrus.SetOutput(io.MultiWriter(os.Stdout, logFile))
}
}
}
}

View File

@@ -117,8 +117,8 @@ type SystemSettingsManager struct {
var globalSystemSettings *SystemSettingsManager
var once sync.Once
// GetSystemSettingsManager 获取全局系统配置管理器单例
func GetSystemSettingsManager() *SystemSettingsManager {
// NewSystemSettingsManager 获取全局系统配置管理器单例
func NewSystemSettingsManager() *SystemSettingsManager {
once.Do(func() {
globalSystemSettings = &SystemSettingsManager{}
})

View File

@@ -0,0 +1,87 @@
// Package container provides a dependency injection container for the application.
package container
import (
"gpt-load/internal/app"
"gpt-load/internal/channel"
"gpt-load/internal/config"
"gpt-load/internal/db"
"gpt-load/internal/handler"
"gpt-load/internal/proxy"
"gpt-load/internal/router"
"gpt-load/internal/services"
"gpt-load/internal/store"
"go.uber.org/dig"
)
// BuildContainer creates a new dependency injection container and provides all the application's services.
func BuildContainer() (*dig.Container, error) {
container := dig.New()
// Infrastructure Services
if err := container.Provide(config.NewManager); err != nil {
return nil, err
}
if err := container.Provide(db.NewDB); err != nil {
return nil, err
}
if err := container.Provide(config.NewSystemSettingsManager); err != nil {
return nil, err
}
if err := container.Provide(store.NewStore); err != nil {
return nil, err
}
if err := container.Provide(channel.NewFactory); err != nil {
return nil, err
}
// Business Services
if err := container.Provide(services.NewTaskService); err != nil {
return nil, err
}
if err := container.Provide(services.NewKeyValidatorService); err != nil {
return nil, err
}
if err := container.Provide(services.NewKeyValidationPool); err != nil {
return nil, err
}
if err := container.Provide(services.NewKeyManualValidationService); err != nil {
return nil, err
}
if err := container.Provide(services.NewKeyCronService); err != nil {
return nil, err
}
if err := container.Provide(services.NewKeyService); err != nil {
return nil, err
}
if err := container.Provide(services.NewLogCleanupService); err != nil {
return nil, err
}
// Handlers
if err := container.Provide(handler.NewServer); err != nil {
return nil, err
}
if err := container.Provide(handler.NewLogCleanupHandler); err != nil {
return nil, err
}
if err := container.Provide(handler.NewCommonHandler); err != nil {
return nil, err
}
// Proxy & Router
if err := container.Provide(proxy.NewProxyServer); err != nil {
return nil, err
}
if err := container.Provide(router.NewRouter); err != nil {
return nil, err
}
// Application Layer
if err := container.Provide(app.NewApp); err != nil {
return nil, err
}
return container, nil
}

View File

@@ -15,7 +15,7 @@ import (
var DB *gorm.DB
func InitDB(configManager types.ConfigManager) (*gorm.DB, error) {
func NewDB(configManager types.ConfigManager) (*gorm.DB, error) {
dbConfig := configManager.GetDatabaseConfig()
if dbConfig.DSN == "" {
return nil, fmt.Errorf("DATABASE_DSN is not configured")

View File

@@ -11,6 +11,7 @@ import (
"gpt-load/internal/types"
"github.com/gin-gonic/gin"
"go.uber.org/dig"
"gorm.io/gorm"
)
@@ -26,25 +27,30 @@ type Server struct {
CommonHandler *CommonHandler
}
// NewServer creates a new handler instance
func NewServer(
db *gorm.DB,
config types.ConfigManager,
settingsManager *config.SystemSettingsManager,
keyValidatorService *services.KeyValidatorService,
keyManualValidationService *services.KeyManualValidationService,
taskService *services.TaskService,
keyService *services.KeyService,
) *Server {
// NewServerParams defines the dependencies for the NewServer constructor.
type NewServerParams struct {
dig.In
DB *gorm.DB
Config types.ConfigManager
SettingsManager *config.SystemSettingsManager
KeyValidatorService *services.KeyValidatorService
KeyManualValidationService *services.KeyManualValidationService
TaskService *services.TaskService
KeyService *services.KeyService
CommonHandler *CommonHandler
}
// NewServer creates a new handler instance with dependencies injected by dig.
func NewServer(params NewServerParams) *Server {
return &Server{
DB: db,
config: config,
SettingsManager: settingsManager,
KeyValidatorService: keyValidatorService,
KeyManualValidationService: keyManualValidationService,
TaskService: taskService,
KeyService: keyService,
CommonHandler: NewCommonHandler(),
DB: params.DB,
config: params.Config,
SettingsManager: params.SettingsManager,
KeyValidatorService: params.KeyValidatorService,
KeyManualValidationService: params.KeyManualValidationService,
TaskService: params.TaskService,
KeyService: params.KeyService,
CommonHandler: params.CommonHandler,
}
}

View File

@@ -12,9 +12,8 @@ import (
// GetSettings handles the GET /api/settings request.
// It retrieves all system settings, groups them by category, and returns them.
func GetSettings(c *gin.Context) {
settingsManager := config.GetSystemSettingsManager()
currentSettings := settingsManager.GetSettings()
func (s *Server) GetSettings(c *gin.Context) {
currentSettings := s.SettingsManager.GetSettings()
settingsInfo := config.GenerateSettingsMetadata(&currentSettings)
// Group settings by category while preserving order
@@ -42,7 +41,7 @@ func GetSettings(c *gin.Context) {
// UpdateSettings handles the PUT /api/settings request.
// It receives a key-value JSON object and updates system settings.
// After updating, it triggers a configuration reload.
func UpdateSettings(c *gin.Context) {
func (s *Server) UpdateSettings(c *gin.Context) {
var settingsMap map[string]any
if err := c.ShouldBindJSON(&settingsMap); err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
@@ -54,22 +53,20 @@ func UpdateSettings(c *gin.Context) {
return
}
settingsManager := config.GetSystemSettingsManager()
// 更新配置
if err := settingsManager.UpdateSettings(settingsMap); err != nil {
if err := s.SettingsManager.UpdateSettings(settingsMap); err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, err.Error()))
return
}
// 重载系统配置
if err := settingsManager.LoadFromDatabase(); err != nil {
if err := s.SettingsManager.LoadFromDatabase(); err != nil {
logrus.Errorf("Failed to reload system settings: %v", err)
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, "Failed to reload system settings after update"))
return
}
settingsManager.DisplayCurrentSettings()
s.SettingsManager.DisplayCurrentSettings()
logrus.Info("Configuration reloaded successfully via API")
response.Success(c, gin.H{

View File

@@ -36,7 +36,7 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
}
}
func New(
func NewRouter(
serverHandler *handler.Server,
proxyServer *proxy.ProxyServer,
logCleanupHandler *handler.LogCleanupHandler,
@@ -142,8 +142,8 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
// 设置
settings := api.Group("/settings")
{
settings.GET("", handler.GetSettings)
settings.PUT("", handler.UpdateSettings)
settings.GET("", serverHandler.GetSettings)
settings.PUT("", serverHandler.UpdateSettings)
}
}

View File

@@ -2,22 +2,26 @@ package services
import (
"gpt-load/internal/config"
"gpt-load/internal/db"
"gpt-load/internal/models"
"time"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// LogCleanupService 负责清理过期的请求日志
type LogCleanupService struct {
stopCh chan struct{}
db *gorm.DB
settingsManager *config.SystemSettingsManager
stopCh chan struct{}
}
// NewLogCleanupService 创建新的日志清理服务
func NewLogCleanupService() *LogCleanupService {
func NewLogCleanupService(db *gorm.DB, settingsManager *config.SystemSettingsManager) *LogCleanupService {
return &LogCleanupService{
stopCh: make(chan struct{}),
db: db,
settingsManager: settingsManager,
stopCh: make(chan struct{}),
}
}
@@ -54,19 +58,8 @@ func (s *LogCleanupService) run() {
// 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()
settings := s.settingsManager.GetSettings()
retentionDays := settings.RequestLogRetentionDays
if retentionDays <= 0 {
@@ -78,7 +71,7 @@ func (s *LogCleanupService) cleanupExpiredLogs() {
cutoffTime := time.Now().AddDate(0, 0, -retentionDays).UTC()
// 执行删除操作
result := db.DB.Where("timestamp < ?", cutoffTime).Delete(&models.RequestLog{})
result := s.db.Where("timestamp < ?", cutoffTime).Delete(&models.RequestLog{})
if result.Error != nil {
logrus.WithError(result.Error).Error("Failed to cleanup expired request logs")
return