package app import ( "context" "errors" "net" "sync" "time" "google.golang.org/grpc" "hyapp/pkg/grpchealth" "hyapp/pkg/healthhttp" "hyapp/pkg/logx" "hyapp/pkg/rocketmqx" serviceapp "hyapp/pkg/servicekit/app" "hyapp/pkg/servicekit/grpcserver" servicemq "hyapp/pkg/servicekit/mq" "hyapp/services/activity-service/internal/config" broadcastservice "hyapp/services/activity-service/internal/service/broadcast" cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge" firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge" inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity" luckygiftservice "hyapp/services/activity-service/internal/service/luckygift" messageservice "hyapp/services/activity-service/internal/service/message" mysqlstorage "hyapp/services/activity-service/internal/storage/mysql" ) // App 装配 activity-service 的 gRPC 入口、健康检查、后台 worker 和外部连接。 // 具体业务模块的创建和注册都放在独立文件里,避免启动入口继续膨胀成一个难维护的大函数。 type App struct { server *grpc.Server listener net.Listener health *grpchealth.ServingChecker healthHTTP *healthhttp.Server mysqlRepo *mysqlstorage.Repository broadcast *broadcastservice.Service luckyGift *luckygiftservice.Service firstRechargeReward *firstrechargeservice.Service cumulativeRecharge *cumulativerechargeservice.Service inviteActivityReward *inviteactivityservice.Service actionConfirm *messageservice.ActionConfirmService broadcastWorkerEnabled bool luckyGiftWorkerEnabled bool messageActionWorkerEnabled bool workerNodeID string luckyGiftWorkerOptions luckygiftservice.WorkerOptions messageActionWorkerOptions config.MessageActionConfirmWorkerConfig messageActionOutboxTopic string userLeaderboardRedisClose func() error mqConsumers []*rocketmqx.Consumer messageActionProducer *rocketmqx.Producer userConn *grpc.ClientConn walletConn *grpc.ClientConn roomConn *grpc.ClientConn workers *serviceapp.BackgroundGroup closeOnce sync.Once } // New 初始化 activity-service 应用。 func New(cfg config.Config) (*App, error) { startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() repository, err := openRepository(startupCtx, cfg) if err != nil { return nil, err } var listener net.Listener var clients externalClients var mqConsumers []*rocketmqx.Consumer var messageActionProducer *rocketmqx.Producer var userLeaderboardRedisClose func() error cleanup := func() { shutdownConsumers(mqConsumers) servicemq.ShutdownProducers([]*rocketmqx.Producer{messageActionProducer}) if userLeaderboardRedisClose != nil { _ = userLeaderboardRedisClose() } clients.Close() if listener != nil { _ = listener.Close() } _ = repository.Close() } listener, err = listenGRPC(cfg.GRPCAddr) if err != nil { cleanup() return nil, err } clients, err = dialExternalClients(cfg) if err != nil { cleanup() return nil, err } server := grpcserver.New("activity-service") userLeaderboardStore, closeLeaderboardRedis, err := openUserLeaderboardStore(startupCtx, cfg.UserLeaderboardWorker) if err != nil { cleanup() return nil, err } userLeaderboardRedisClose = closeLeaderboardRedis services, err := buildServiceBundle(cfg, repository, clients, userLeaderboardStore) if err != nil { cleanup() return nil, err } registerGRPCServers(server, services) mqConsumers, err = buildMQConsumers(cfg, services) if err != nil { cleanup() return nil, err } if cfg.RocketMQ.MessageActionOutbox.Enabled { messageActionProducer, err = rocketmqx.NewProducer(messageActionProducerConfig(cfg.RocketMQ)) if err != nil { cleanup() return nil, err } } health, healthHTTP, err := newHealthServers(cfg, server, repository) if err != nil { cleanup() return nil, err } return &App{ server: server, listener: listener, health: health, healthHTTP: healthHTTP, mysqlRepo: repository, broadcast: services.broadcast, luckyGift: services.luckyGift, firstRechargeReward: services.firstRechargeReward, cumulativeRecharge: services.cumulativeRecharge, inviteActivityReward: services.inviteActivityReward, actionConfirm: services.actionConfirm, broadcastWorkerEnabled: cfg.Broadcast.Enabled, luckyGiftWorkerEnabled: cfg.LuckyGiftWorker.Enabled, messageActionWorkerEnabled: cfg.MessageActionConfirmWorker.Enabled, workerNodeID: cfg.NodeID, luckyGiftWorkerOptions: luckyGiftWorkerOptions(cfg.NodeID, cfg.LuckyGiftWorker), messageActionWorkerOptions: cfg.MessageActionConfirmWorker, messageActionOutboxTopic: cfg.RocketMQ.MessageActionOutbox.Topic, userLeaderboardRedisClose: userLeaderboardRedisClose, mqConsumers: mqConsumers, messageActionProducer: messageActionProducer, userConn: clients.userConn, walletConn: clients.walletConn, roomConn: clients.roomConn, workers: serviceapp.NewBackground(context.Background()), }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { a.runHealthHTTP() a.health.MarkServing() if err := a.startMQ(); err != nil { a.health.MarkStopped() return err } if a.broadcastWorkerEnabled && a.broadcast != nil && a.workers != nil { a.workers.Go(func(ctx context.Context) { a.broadcast.RunWorker(ctx, broadcastservice.WorkerOptions{}) }) } if a.luckyGiftWorkerEnabled && a.luckyGift != nil && a.workers != nil { a.workers.Go(func(ctx context.Context) { a.luckyGift.RunWorker(ctx, a.luckyGiftWorkerOptions) }) } if a.messageActionWorkerEnabled && a.actionConfirm != nil && a.messageActionProducer != nil && a.workers != nil { a.workers.Go(func(ctx context.Context) { a.runMessageActionOutboxWorker(ctx) }) } err := a.server.Serve(a.listener) if a.workers != nil { a.workers.StopAndWait() } a.shutdownMQ() a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { return nil } return err } // Close 关闭 gRPC 服务。 func (a *App) Close() { a.closeOnce.Do(func() { a.health.MarkDraining() if a.workers != nil { a.workers.StopAndWait() } a.shutdownMQ() grpcserver.GracefulStop(a.server, 15*time.Second) a.closeHealthHTTP() if a.userConn != nil { _ = a.userConn.Close() } if a.walletConn != nil { _ = a.walletConn.Close() } if a.roomConn != nil { _ = a.roomConn.Close() } if a.mysqlRepo != nil { // MySQL 连接池最后关闭,保证 drain 中的查询或消费提交能完成。 _ = a.mysqlRepo.Close() } if a.userLeaderboardRedisClose != nil { _ = a.userLeaderboardRedisClose() } }) } func (a *App) runHealthHTTP() { if a.healthHTTP == nil { return } go func() { if err := a.healthHTTP.Run(); err != nil { logx.Error(context.Background(), "health_http_run_failed", err) } }() } func (a *App) closeHealthHTTP() { if a.healthHTTP == nil { return } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = a.healthHTTP.Close(ctx) } func (a *App) startMQ() error { if err := servicemq.StartProducers([]*rocketmqx.Producer{a.messageActionProducer}); err != nil { return err } return servicemq.StartConsumers(a.mqConsumers) } func (a *App) shutdownMQ() { servicemq.ShutdownConsumers(a.mqConsumers) servicemq.ShutdownProducers([]*rocketmqx.Producer{a.messageActionProducer}) }