P0(高优先级): - P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过 - P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min - P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式 P1(中优先级): - P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖) - P1-2: 权限缓存 TTL 30min→5min - P1-3: Argon2id 启动自适应校准(CalibrateArgon2id) 历史优化(含本次提交): - L1Cache O(n)→O(1) LRU 重构 - Auth 中间件 DB 查询合并 + 5s L1 缓存 - Logger 异步化(4096 缓冲通道) 验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
272 lines
9.8 KiB
Go
272 lines
9.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"github.com/user-management-system/internal/api/handler"
|
||
"github.com/user-management-system/internal/api/middleware"
|
||
"github.com/user-management-system/internal/api/router"
|
||
"github.com/user-management-system/internal/auth"
|
||
"github.com/user-management-system/internal/cache"
|
||
"github.com/user-management-system/internal/config"
|
||
"github.com/user-management-system/internal/database"
|
||
"github.com/user-management-system/internal/monitoring"
|
||
"github.com/user-management-system/internal/repository"
|
||
"github.com/user-management-system/internal/security"
|
||
"github.com/user-management-system/internal/service"
|
||
)
|
||
|
||
func main() {
|
||
// 加载配置
|
||
cfg, err := config.Load()
|
||
if err != nil {
|
||
log.Fatalf("load config failed: %v", err)
|
||
}
|
||
|
||
// 设置 Gin 模式
|
||
gin.SetMode(resolveGinMode(cfg.Server.Mode))
|
||
|
||
// 初始化数据库
|
||
db, err := database.NewDB(cfg)
|
||
if err != nil {
|
||
log.Fatalf("connect database failed: %v", err)
|
||
}
|
||
|
||
// 执行数据库迁移
|
||
if err := db.AutoMigrate(cfg); err != nil {
|
||
log.Fatalf("auto migrate failed: %v", err)
|
||
}
|
||
|
||
// P1-3:Argon2id 启动时自适应校准
|
||
// 在当前机器上测量哈希耗时,超出 500ms 预算则自动降低参数,确保登录接口 P99 < 1000ms。
|
||
// 此操作仅在启动阶段执行一次,耗时约 1-3s(正常情况下与默认参数一致则跳过)。
|
||
auth.CalibrateArgon2id(500 * time.Millisecond)
|
||
|
||
// 初始化 JWT 管理器
|
||
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
|
||
HS256Secret: cfg.JWT.Secret,
|
||
AccessTokenExpire: time.Duration(cfg.JWT.AccessTokenExpireMinutes) * time.Minute,
|
||
RefreshTokenExpire: time.Duration(cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour,
|
||
})
|
||
if err != nil {
|
||
log.Fatalf("create jwt manager failed: %v", err)
|
||
}
|
||
|
||
// 初始化缓存
|
||
// Redis 智能探测:有 Redis 则启用 L2 分布式缓存,无 Redis 则降级到纯 L1 内存缓存。
|
||
// 两种模式下系统功能完全等价,区别仅在于多实例场景的缓存共享能力。
|
||
// 如需禁用 Redis 探测(即使 Redis 可达也不启用),可将配置中 redis.host 留空。
|
||
l1Cache := cache.NewL1Cache()
|
||
redisAddr := fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port)
|
||
redisEnabled := cfg.Redis.Host != "" && cache.ProbeRedis(redisAddr, cfg.Redis.Password, cfg.Redis.DB)
|
||
if !redisEnabled {
|
||
log.Printf("cache: running in memory-only mode (Redis unreachable or not configured)")
|
||
}
|
||
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
|
||
Enabled: redisEnabled,
|
||
Addr: redisAddr,
|
||
Password: cfg.Redis.Password,
|
||
DB: cfg.Redis.DB,
|
||
})
|
||
defer l2Cache.Close()
|
||
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
|
||
|
||
// 初始化 Repository
|
||
userRepo := repository.NewUserRepository(db.DB)
|
||
roleRepo := repository.NewRoleRepository(db.DB)
|
||
permissionRepo := repository.NewPermissionRepository(db.DB)
|
||
userRoleRepo := repository.NewUserRoleRepository(db.DB)
|
||
rolePermissionRepo := repository.NewRolePermissionRepository(db.DB)
|
||
deviceRepo := repository.NewDeviceRepository(db.DB)
|
||
loginLogRepo := repository.NewLoginLogRepository(db.DB)
|
||
operationLogRepo := repository.NewOperationLogRepository(db.DB)
|
||
customFieldRepo := repository.NewCustomFieldRepository(db.DB)
|
||
userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db.DB)
|
||
themeRepo := repository.NewThemeConfigRepository(db.DB)
|
||
socialRepo, err := repository.NewSocialAccountRepository(db.DB)
|
||
if err != nil {
|
||
log.Fatalf("initialize social account repository failed: %v", err)
|
||
}
|
||
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
|
||
|
||
// 初始化 Service
|
||
deviceService := service.NewDeviceService(deviceRepo, userRepo)
|
||
authService := service.NewAuthService(
|
||
userRepo,
|
||
socialRepo,
|
||
jwtManager,
|
||
cacheManager,
|
||
8, // passwordMinLength
|
||
5, // maxLoginAttempts
|
||
15*time.Minute, // loginLockDuration
|
||
)
|
||
authService.SetRoleRepositories(userRoleRepo, roleRepo)
|
||
authService.SetLoginLogRepository(loginLogRepo)
|
||
authService.SetDeviceService(deviceService)
|
||
|
||
// IP 过滤中间件
|
||
var ipFilterMiddleware *middleware.IPFilterMiddleware
|
||
ipFilter := security.NewIPFilter()
|
||
if ipFilter != nil {
|
||
ipFilterMiddleware = middleware.NewIPFilterMiddleware(ipFilter, middleware.IPFilterConfig{
|
||
TrustProxy: cfg.CORS.AllowCredentials,
|
||
})
|
||
}
|
||
|
||
// 初始化异常检测器并注入
|
||
anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter)
|
||
authService.SetAnomalyDetector(anomalyDetector)
|
||
log.Println("anomaly detector initialized")
|
||
|
||
userService := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
|
||
roleService := service.NewRoleService(roleRepo, rolePermissionRepo)
|
||
permissionService := service.NewPermissionService(permissionRepo)
|
||
loginLogService := service.NewLoginLogService(loginLogRepo)
|
||
operationLogService := service.NewOperationLogService(operationLogRepo)
|
||
captchaService := service.NewCaptchaService(cacheManager)
|
||
totpService := service.NewTOTPService(userRepo)
|
||
|
||
passwordResetConfig := service.DefaultPasswordResetConfig()
|
||
passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig).
|
||
WithPasswordHistoryRepo(passwordHistoryRepo)
|
||
|
||
webhookService := service.NewWebhookService(db.DB, service.WebhookServiceConfig{
|
||
Enabled: false,
|
||
})
|
||
exportService := service.NewExportService(userRepo, roleRepo)
|
||
statsService := service.NewStatsService(userRepo, loginLogRepo)
|
||
customFieldService := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo)
|
||
themeService := service.NewThemeService(themeRepo)
|
||
|
||
// 设置 CORS 配置
|
||
middleware.SetCORSConfig(cfg.CORS)
|
||
|
||
// 初始化中间件
|
||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(cfg.RateLimit)
|
||
authMiddleware := middleware.NewAuthMiddleware(
|
||
jwtManager,
|
||
userRepo,
|
||
userRoleRepo,
|
||
l1Cache,
|
||
)
|
||
authMiddleware.SetCacheManager(cacheManager)
|
||
|
||
opLogMiddleware := middleware.NewOperationLogMiddleware(operationLogRepo)
|
||
|
||
// 初始化 Handler
|
||
authHandler := handler.NewAuthHandler(authService)
|
||
userHandler := handler.NewUserHandler(userService)
|
||
roleHandler := handler.NewRoleHandler(roleService)
|
||
permissionHandler := handler.NewPermissionHandler(permissionService)
|
||
deviceHandler := handler.NewDeviceHandler(deviceService)
|
||
logHandler := handler.NewLogHandler(loginLogService, operationLogService)
|
||
captchaHandler := handler.NewCaptchaHandler(captchaService)
|
||
totpHandler := handler.NewTOTPHandler(authService, totpService)
|
||
webhookHandler := handler.NewWebhookHandler(webhookService)
|
||
exportHandler := handler.NewExportHandler(exportService)
|
||
statsHandler := handler.NewStatsHandler(statsService)
|
||
passwordResetHandler := handler.NewPasswordResetHandler(passwordResetService)
|
||
smsHandler := handler.NewSMSHandler(authService, nil)
|
||
avatarHandler := handler.NewAvatarHandler(userRepo)
|
||
customFieldHandler := handler.NewCustomFieldHandler(customFieldService)
|
||
themeHandler := handler.NewThemeHandler(themeService)
|
||
|
||
// 初始化 SSO 管理器
|
||
ssoManager := auth.NewSSOManager()
|
||
ssoClientsStore := auth.NewDefaultSSOClientsStore()
|
||
ssoHandler := handler.NewSSOHandler(ssoManager, ssoClientsStore)
|
||
|
||
// 系统设置服务
|
||
settingsService := service.NewSettingsService()
|
||
settingsHandler := handler.NewSettingsHandler(settingsService)
|
||
|
||
// SSO 会话清理 context(随服务器关闭而取消)
|
||
ssoCtx, ssoCancel := context.WithCancel(context.Background())
|
||
defer ssoCancel()
|
||
ssoManager.StartCleanup(ssoCtx)
|
||
|
||
// 初始化监控指标(CRIT-01/02 修复:确保指标被初始化并挂载)
|
||
metrics := monitoring.GetGlobalMetrics()
|
||
sloMetrics := monitoring.GetGlobalSLOMetrics()
|
||
|
||
// CRIT-03 修复:启动后台 goroutine 定期采集系统指标(runtime + DB 连接池)
|
||
metricsCtx, metricsCancel := context.WithCancel(context.Background())
|
||
defer metricsCancel()
|
||
go monitoring.StartSystemMetricsCollector(metricsCtx, metrics, sloMetrics, db.DB)
|
||
|
||
// 设置路由
|
||
r := router.NewRouter(
|
||
authHandler, userHandler, roleHandler, permissionHandler, deviceHandler,
|
||
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
|
||
passwordResetHandler, captchaHandler, totpHandler, webhookHandler,
|
||
ipFilterMiddleware, exportHandler, statsHandler, smsHandler, customFieldHandler, themeHandler, ssoHandler,
|
||
settingsHandler, metrics, avatarHandler,
|
||
)
|
||
engine := r.Setup()
|
||
|
||
// 健康检查(增强版:存活/就绪分离,检查数据库连接)
|
||
healthCheck := monitoring.NewHealthCheck(db.DB)
|
||
engine.GET("/health", healthCheck.Handler)
|
||
engine.GET("/health/live", healthCheck.LivenessHandler)
|
||
engine.GET("/health/ready", healthCheck.ReadinessHandler)
|
||
|
||
// 启动服务器
|
||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||
srv := &http.Server{
|
||
Addr: addr,
|
||
Handler: engine,
|
||
ReadTimeout: 30 * time.Second,
|
||
WriteTimeout: 30 * time.Second,
|
||
}
|
||
|
||
go func() {
|
||
log.Printf("server listening on %s", addr)
|
||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Fatalf("listen failed: %v", err)
|
||
}
|
||
}()
|
||
|
||
// 等待中断信号
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
|
||
log.Println("shutting down server...")
|
||
|
||
// 关闭 Webhook 服务,等待投递任务完成
|
||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer shutdownCancel()
|
||
if err := webhookService.Shutdown(shutdownCtx); err != nil {
|
||
log.Printf("webhook service shutdown: %v", err)
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
defer cancel()
|
||
|
||
if err := srv.Shutdown(ctx); err != nil {
|
||
log.Fatalf("server forced to shutdown: %v", err)
|
||
}
|
||
|
||
log.Println("server exited")
|
||
}
|
||
|
||
func resolveGinMode(mode string) string {
|
||
switch mode {
|
||
case "debug":
|
||
return gin.DebugMode
|
||
case "test":
|
||
return gin.TestMode
|
||
default:
|
||
return gin.ReleaseMode
|
||
}
|
||
}
|