package app import ( "context" "fmt" "net/http" "time" "mengstack/internal/app/cache" "mengstack/internal/app/database" "mengstack/internal/app/health" "mengstack/internal/app/middleware" appmigrate "mengstack/internal/app/migrate" "mengstack/internal/app/upgrade" "mengstack/internal/config" "mengstack/internal/kernel/i18n" "mengstack/internal/kernel/plugin" "mengstack/internal/kernel/jobs" "mengstack/internal/kernel/tenant" kws "mengstack/internal/kernel/websocket" "mengstack/internal/logger" "mengstack/migrations" authinterfaces "mengstack/internal/modules/auth/interfaces" rbacinterfaces "mengstack/internal/modules/rbac/interfaces" orginterfaces "mengstack/internal/modules/org/interfaces" auditinterfaces "mengstack/internal/modules/audit/interfaces" settingsinterfaces "mengstack/internal/modules/settings/interfaces" notificationinterfaces "mengstack/internal/modules/notification/interfaces" dashboardinterfaces "mengstack/internal/modules/dashboard/interfaces" exampleinterfaces "mengstack/internal/modules/example/interfaces" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/fx" "go.uber.org/zap" "gorm.io/gorm" ) var Module = fx.Module("app", fx.Provide( newEngine, newServer, func(log *zap.Logger) *kws.Hub { return kws.NewHub(log) }, func(hub *kws.Hub, log *zap.Logger) *kws.Handler { return kws.NewHandler(hub, log) }, func(log *zap.Logger) (*jobs.Scheduler, error) { return jobs.NewScheduler(log) }, ), fx.Invoke(registerLifecycle), ) func newEngine( cfg *config.Config, log *zap.Logger, db *gorm.DB, rdb *redis.Client, authHandler *authinterfaces.Handler, authMW gin.HandlerFunc, rbacHandler *rbacinterfaces.Handler, orgHandler *orginterfaces.Handler, auditHandler *auditinterfaces.Handler, settingsHandler *settingsinterfaces.Handler, notificationHandler *notificationinterfaces.Handler, dashboardHandler *dashboardinterfaces.Handler, examplePlugin *exampleinterfaces.ExamplePlugin, wsHandler *kws.Handler, ) *gin.Engine { ginMode := "release" if cfg.Server.Mode == "debug" || cfg.Server.Mode == "dev" { ginMode = "debug" } gin.SetMode(ginMode) r := gin.New() r.Use(middleware.RequestID()) r.Use(middleware.RequestLogger(log)) r.Use(middleware.SecurityHeaders()) r.Use(middleware.CORS()) r.Use(middleware.AcceptLanguage()) r.Use(middleware.RateLimitRedis(rdb)) r.Use(middleware.BodyLimit(10 << 20)) r.Use(middleware.Recovery(log)) healthHandler := health.NewHandler(db, rdb, log) r.GET("/health", healthHandler.Handle) r.GET("/healthz", healthHandler.Healthz) r.GET("/readyz", healthHandler.Readyz) r.GET("/api/version", healthHandler.Version) upgradeHandler := upgrade.NewHandler() r.GET("/api/upgrade/check", upgradeHandler.Check) r.GET("/swagger/*any", middleware.SwaggerHandler()) tenantResolver := tenant.NewResolver(cfg.Tenant) authinterfaces.SetupRoutes(r, authHandler, authMW, tenantResolver) rbacinterfaces.SetupRoutes(r, rbacHandler, authMW, tenantResolver) orginterfaces.SetupRoutes(r, orgHandler, authMW, tenantResolver) auditinterfaces.SetupRoutes(r, auditHandler, authMW, tenantResolver) settingsinterfaces.SetupRoutes(r, settingsHandler, authMW, tenantResolver) notificationinterfaces.SetupRoutes(r, notificationHandler, authMW, tenantResolver) dashboardinterfaces.SetupRoutes(r, dashboardHandler, authMW, tenantResolver) r.GET("/api/v1/ws", authMW, wsHandler.ServeWS) examplePlugin.SetupRoutes(r, authMW, tenantResolver) return r } func newServer(cfg *config.Config, engine *gin.Engine) *http.Server { return &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Server.Port), Handler: engine, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } } func registerLifecycle(lc fx.Lifecycle, srv *http.Server, db *gorm.DB, sched *jobs.Scheduler, log *zap.Logger) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { if err := i18n.Init(i18n.Files, "zh"); err != nil { return fmt.Errorf("init i18n: %w", err) } log.Info("i18n initialized") if err := appmigrate.Run(db, migrations.Files); err != nil { return fmt.Errorf("run migrations: %w", err) } log.Info("database migrations applied") for _, p := range plugin.All() { meta := p.Metadata() pfs := p.MigrationsFS() if pfs == nil { continue } if err := appmigrate.RunPlugin(db, meta.Name, pfs); err != nil { return fmt.Errorf("plugin %s migrations: %w", meta.Name, err) } log.Info("plugin migrations applied", zap.String("plugin", meta.Name)) } sandbox := plugin.NewSandbox(log) if err := plugin.InitAll(log, sandbox); err != nil { return err } sched.Start() log.Info("server starting", zap.String("addr", srv.Addr)) go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error("server error", zap.Error(err)) } }() return nil }, OnStop: func(ctx context.Context) error { shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() log.Info("server shutting down") if err := sched.Stop(); err != nil { log.Error("scheduler stop error", zap.Error(err)) } return srv.Shutdown(shutdownCtx) }, }) } func NewApp() *fx.App { return fx.New( fx.Provide( func() (*config.Config, error) { return config.Load() }, func(cfg *config.Config) (*zap.Logger, error) { return logger.New(cfg.Log) }, ), database.Module, cache.Module, authinterfaces.Module, rbacinterfaces.Module, orginterfaces.Module, auditinterfaces.Module, settingsinterfaces.Module, notificationinterfaces.Module, dashboardinterfaces.Module, exampleinterfaces.Module, Module, ) }