Files
user-system/cmd/server/main.go
long-agent 582ad7a069 test: add comprehensive test coverage and improve code quality
- Add new test files for auth, service, and handler modules
- Improve test organization and coverage
- Refactor code for better maintainability
- Add captcha, settings, stats, and theme handler tests
- Add auth module tests (CAS, OAuth, password, SSO, state)
- Add service layer tests for auth, export, permissions, roles
- All Go tests pass (exit code 0)
- All frontend tests pass (325 tests in 59 files)
2026-04-17 20:43:50 +08:00

258 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
// 初始化 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)
}
// 初始化缓存
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
Addr: fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port),
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
}
}