// Package main 是 notification service 的入口程序。 // // 设计要点(参考 socialService/main.go 的结构): // - 通过 flag + 环境变量注入运行参数(端口、DB 等)。 // - 启动顺序:logger -> DB -> AutoMigrate -> service -> provider -> Dubbo server。 // - 使用 Dubbo Triple 协议暴露 notification.proto 中的 NotificationService。 // - 优雅关闭:监听 SIGINT/SIGTERM,关停 health server 与 DB 连接。 package main import ( "flag" "fmt" "os" "os/signal" "strconv" "strings" "syscall" "time" "dubbo.apache.org/dubbo-go/v3/protocol" "dubbo.apache.org/dubbo-go/v3/server" _ "dubbo.apache.org/dubbo-go/v3/imports" "github.com/topfans/backend/pkg/database" "github.com/topfans/backend/pkg/health" "github.com/topfans/backend/pkg/logger" notifPb "github.com/topfans/backend/pkg/proto/notification" "github.com/topfans/backend/pkg/push" "github.com/topfans/backend/services/notificationService/model" "github.com/topfans/backend/services/notificationService/provider" "github.com/topfans/backend/services/notificationService/service" ) var ( port = flag.Int("port", getEnvInt("PORT", 20010), "Dubbo service port") dbHost = flag.String("db-host", getEnv("DB_HOST", "localhost"), "Database host") dbPort = flag.Int("db-port", getEnvInt("DB_PORT", 5432), "Database port") dbUser = flag.String("db-user", getEnv("DB_USER", "postgres"), "Database user") dbPassword = flag.String("db-password", getEnv("DB_PASSWORD", ""), "Database password") dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name") pushEnabled = flag.Bool("push-enabled", getEnvBool("PUSH_ENABLED", true), "Enable mobile push (uniPush)") pushURL = flag.String("push-url", getEnv("PUSH_URL", ""), "uniCloud sendMessage URL (REQUIRED when push-enabled=true; deploy via env, do NOT commit vendor URLs)") pushTimeoutMs = flag.Int("push-timeout-ms", getEnvInt("PUSH_TIMEOUT_MS", 4000), "HTTP timeout for uniPush (ms)") healthHndl *health.Handler ) func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getEnvInt(key string, fallback int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback } // getEnvBool 把 "1"/"true"/"TRUE"/"yes"/"y" 视为 true;其它视为 false。空时回退到 fallback。 func getEnvBool(key string, fallback bool) bool { v := strings.ToLower(strings.TrimSpace(os.Getenv(key))) if v == "" { return fallback } switch v { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false } return fallback } func main() { flag.Parse() // 1) 初始化日志(必须在最前面,便于后续捕获启动错误) env := os.Getenv("ENV") if env == "" { env = "development" } if err := logger.Init(logger.Config{ ServiceName: "notification-service", Environment: env, LogLevel: os.Getenv("LOG_LEVEL"), }); err != nil { panic(fmt.Sprintf("Failed to initialize logger: %v", err)) } defer logger.Sync() logger.Sugar.Info("Starting Notification Service...") // 2) 初始化数据库 if err := initDatabase(); err != nil { logger.Sugar.Fatalf("Failed to initialize database: %v", err) } // 3) 自动迁移数据库表(notification + notification_stats) if err := autoMigrate(); err != nil { logger.Sugar.Fatalf("Failed to migrate database: %v", err) } // 4) 启动 Dubbo server + 注册 NotificationService if err := initDubboService(); err != nil { logger.Sugar.Fatalf("Failed to initialize Dubbo service: %v", err) } // 5) 等待退出信号(优雅关闭) logger.Sugar.Info("Notification service started successfully. Press Ctrl+C to exit.") gracefulShutdown() } // initDatabase 初始化数据库连接(复用 socialService 的写法)。 func initDatabase() error { config := database.Config{ Host: *dbHost, Port: *dbPort, User: *dbUser, Password: *dbPassword, DBName: *dbName, SSLMode: "disable", TimeZone: "Asia/Shanghai", } return database.Init(config) } // autoMigrate 自动迁移 notification 相关表。 func autoMigrate() error { db := database.GetDB() if db == nil { return fmt.Errorf("database is not initialized") } tables := []interface{}{ &model.Notification{}, &model.NotificationStats{}, &model.UserDevice{}, } for _, table := range tables { if err := db.AutoMigrate(table); err != nil { return fmt.Errorf("failed to migrate table: %w", err) } } logger.Sugar.Info("Database migration completed successfully") return nil } // gracefulShutdown 优雅关闭(health server + DB)。 func gracefulShutdown() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Sugar.Info("Shutting down server...") if healthHndl != nil { healthHndl.Stop() } if err := database.Close(); err != nil { logger.Sugar.Errorf("Error closing database: %v", err) } logger.Sugar.Info("Server exited") } // initDubboService 初始化 Dubbo-go 服务。 // // 启动健康检查 HTTP server(端口 = dubbo port + 1000,例如 21010), // 然后构造 service / provider,注册到 Triple 协议的 Dubbo server。 func initDubboService() error { // 健康检查 HTTP server healthPort := *port + 1000 // e.g., 20010 -> 21010 healthHndl = health.NewHandler("notification-service", healthPort) healthHndl.Start() db := database.GetDB() if db == nil { return fmt.Errorf("database is not initialized") } // 业务层:UserDeviceService 给 NotificationService 提供 cids 拉取能力。 deviceService := service.NewUserDeviceService(db) // 推送客户端(三态:disabled / enabled-with-URL / enabled-without-URL 降级为 noop) // 设计要点: // - push-enabled=true 但 url 为空 → warn 后降级为 NoopPusher,不直接 Fatal。 // 这样部署期忘了配 PUSH_URL 时业务仍能跑(只是不会真的推),比启动失败更安全。 // - 真要严格启动校验,把降级改成 fmt.Errorf 返回即可。 var pusher push.Pusher switch { case !*pushEnabled: pusher = push.NoopPusher{} logger.Sugar.Info("mobile push disabled (PUSH_ENABLED=false)") case *pushEnabled && *pushURL == "": pusher = push.NoopPusher{} logger.Sugar.Warn("mobile push enabled but PUSH_URL is empty; falling back to NoopPusher — set PUSH_URL env to actually deliver") default: pusher = push.NewUniPushClient( *pushURL, time.Duration(*pushTimeoutMs)*time.Millisecond, logger.Logger, ) logger.Sugar.Info("mobile push enabled", "url", *pushURL, "timeout_ms", *pushTimeoutMs, ) } // 通知服务(注入了 deviceService + pusher;CreateNotification 成功后异步触发推送) notifService := service.NewNotificationService(db, deviceService, pusher) // RPC Provider notifProvider := provider.NewNotificationProvider(notifService, deviceService) // Dubbo Server(Triple 协议) srv, err := server.NewServer( server.WithServerProtocol( protocol.WithPort(*port), protocol.WithTriple(), ), ) if err != nil { return fmt.Errorf("failed to create Dubbo server: %w", err) } // 注册 NotificationServiceHandler(来自 notification.triple.go) if err := notifPb.RegisterNotificationServiceHandler(srv, notifProvider); err != nil { return fmt.Errorf("failed to register NotificationService handler: %w", err) } logger.Sugar.Info("Dubbo-go notification provider registered successfully", "service", notifPb.NotificationServiceName, "port", *port, ) // 后台启动 Dubbo server(阻塞当前 goroutine 时 main 会卡在 srv.Serve 上, // 所以放 goroutine 里,由 gracefulShutdown 通过 os.Signal 触发退出)。 go func() { if err := srv.Serve(); err != nil { logger.Sugar.Fatalf("Failed to serve Dubbo: %v", err) } }() return nil }