package app import ( "context" "errors" "net" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" healthgrpc "google.golang.org/grpc/health/grpc_health_v1" activityv1 "hyapp.local/api/proto/activity/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/logx" "hyapp/pkg/tencentim" "hyapp/services/activity-service/internal/client" "hyapp/services/activity-service/internal/config" activityservice "hyapp/services/activity-service/internal/service/activity" broadcastservice "hyapp/services/activity-service/internal/service/broadcast" messageservice "hyapp/services/activity-service/internal/service/message" taskservice "hyapp/services/activity-service/internal/service/task" mysqlstorage "hyapp/services/activity-service/internal/storage/mysql" grpcserver "hyapp/services/activity-service/internal/transport/grpc" ) // App 装配 activity-service gRPC 入口和底座依赖。 type App struct { server *grpc.Server listener net.Listener health *grpchealth.ServingChecker mysqlRepo *mysqlstorage.Repository broadcast *broadcastservice.Service broadcastWorkerEnabled bool userConn *grpc.ClientConn walletConn *grpc.ClientConn workerCtx context.Context workerStop context.CancelFunc workerWG sync.WaitGroup closeOnce sync.Once } // New 初始化 activity-service 应用。 func New(cfg config.Config) (*App, error) { startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // activity-service 的活动状态、消费幂等和 outbox 都必须使用 MySQL。 repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) if err != nil { return nil, err } listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { _ = repository.Close() return nil, err } userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = listener.Close() _ = repository.Close() return nil, err } walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = userConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("activity-service"))) svc := activityservice.New(activityservice.Config{NodeID: cfg.NodeID}, repository) messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(client.NewGRPCUserTargetSource(userConn))) taskSvc := taskservice.New(repository, walletv1.NewWalletServiceClient(walletConn)) var broadcastPublisher broadcastservice.Publisher if cfg.TencentIM.Enabled { tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig()) if err != nil { _ = walletConn.Close() _ = userConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } broadcastPublisher = tencentClient } if cfg.Broadcast.Enabled && broadcastPublisher == nil { _ = walletConn.Close() _ = userConn.Close() _ = listener.Close() _ = repository.Close() return nil, errors.New("broadcast worker requires tencent_im.enabled") } broadcastSvc := broadcastservice.New(broadcastservice.Config{ NodeID: cfg.NodeID, SuperGiftMinValue: cfg.Broadcast.SuperGiftMinValue, WorkerBatchSize: cfg.Broadcast.WorkerBatchSize, WorkerLockTTL: cfg.Broadcast.WorkerLockTTL, WorkerMaxRetry: cfg.Broadcast.WorkerMaxRetry, WorkerPollInterval: cfg.Broadcast.WorkerPollInterval, EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup, }, repository, broadcastPublisher, client.NewGRPCRegionSource(userConn)) activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(svc)) activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc)) activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(messageSvc)) activityv1.RegisterTaskServiceServer(server, grpcserver.NewTaskServer(taskSvc)) activityv1.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(taskSvc)) broadcastServer := grpcserver.NewBroadcastServer(broadcastSvc) activityv1.RegisterBroadcastServiceServer(server, broadcastServer) activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer) health := grpchealth.NewServingChecker("activity-service") healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health)) workerCtx, workerStop := context.WithCancel(context.Background()) return &App{ server: server, listener: listener, health: health, mysqlRepo: repository, broadcast: broadcastSvc, broadcastWorkerEnabled: cfg.Broadcast.Enabled, userConn: userConn, walletConn: walletConn, workerCtx: workerCtx, workerStop: workerStop, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { a.health.MarkServing() if a.broadcastWorkerEnabled && a.broadcast != nil && a.workerCtx != nil { a.workerWG.Go(func() { a.broadcast.RunWorker(a.workerCtx, broadcastservice.WorkerOptions{}) }) } err := a.server.Serve(a.listener) if a.workerStop != nil { a.workerStop() } a.workerWG.Wait() 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.workerStop != nil { a.workerStop() } a.workerWG.Wait() a.server.GracefulStop() if a.userConn != nil { _ = a.userConn.Close() } if a.walletConn != nil { _ = a.walletConn.Close() } if a.mysqlRepo != nil { // MySQL 连接池最后关闭,保证 drain 中的查询或消费提交能完成。 _ = a.mysqlRepo.Close() } }) }