256 lines
7.8 KiB
Go
256 lines
7.8 KiB
Go
// 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
|
||
}
|