topfans/backend/services/notificationService/main.go

256 lines
7.8 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 是 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 ServerTriple 协议)
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
}