package app import ( "context" "errors" "net" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/healthhttp" "hyapp/pkg/logx" "hyapp/pkg/rocketmqx" servicegrpc "hyapp/pkg/servicekit/grpcserver" servicehealth "hyapp/pkg/servicekit/health" "hyapp/services/wallet-service/internal/client" "hyapp/services/wallet-service/internal/client/googleplay" "hyapp/services/wallet-service/internal/client/mifapay" "hyapp/services/wallet-service/internal/client/trongrid" "hyapp/services/wallet-service/internal/client/v5pay" "hyapp/services/wallet-service/internal/config" walletservice "hyapp/services/wallet-service/internal/service/wallet" mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql" grpcserver "hyapp/services/wallet-service/internal/transport/grpc" ) // App 装配 wallet-service gRPC 入口。 type App struct { server *grpc.Server listener net.Listener health *grpchealth.ServingChecker healthHTTP *healthhttp.Server mysqlRepo *mysqlstorage.Repository walletSvc *walletservice.Service activityConn *grpc.ClientConn outboxProducer *rocketmqx.Producer realtimeOutboxProducer *rocketmqx.Producer projectionConsumer *rocketmqx.Consumer outboxWorkerCfg config.OutboxWorkerConfig realtimeOutboxWorkerCfg config.OutboxWorkerConfig projectionWorkerCfg config.ProjectionWorkerConfig externalRechargeReconcileWorkerCfg config.ExternalRechargeReconcileWorkerConfig walletOutboxTopic string realtimeWalletOutboxTopic string nodeID string redPacketExpiryWorkerCfg config.RedPacketExpiryWorkerConfig stopWorker context.CancelFunc workers sync.WaitGroup closeOnce sync.Once } // New 初始化 wallet-service。 func New(cfg config.Config) (*App, error) { startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() 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 } activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = listener.Close() _ = repository.Close() return nil, err } var outboxProducer *rocketmqx.Producer if cfg.RocketMQ.WalletOutbox.Enabled { outboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.ProducerGroup)) if err != nil { _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } } var realtimeOutboxProducer *rocketmqx.Producer if cfg.RocketMQ.RealtimeOutbox.Enabled { realtimeOutboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.RealtimeOutbox.ProducerGroup)) if err != nil { shutdownProducers(outboxProducer) _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } } server := servicegrpc.New("wallet-service") svc := walletservice.New(repository, client.NewActivityAchievementClient(activityConn)) svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{ USDTTRC20Enabled: cfg.ExternalRecharge.USDTTRC20Enabled, USDTTRC20Address: cfg.ExternalRecharge.USDTTRC20Address, MifaPayNotifyURL: cfg.ExternalRecharge.MifaPayNotifyURL, MifaPayReturnURL: cfg.ExternalRecharge.MifaPayReturnURL, V5PayNotifyURL: cfg.ExternalRecharge.V5PayNotifyURL, V5PayReturnURL: cfg.ExternalRecharge.V5PayReturnURL, }) if cfg.GooglePlay.Enabled { googleClient, err := googleplay.New(cfg.GooglePlay) if err != nil { shutdownProducers(outboxProducer, realtimeOutboxProducer) _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } svc.SetGooglePlayClient(googleClient) } if cfg.MifaPay.Enabled { if mifapayConfigReady(cfg.MifaPay) { mifaPayClient, err := mifapay.New(cfg.MifaPay) if err != nil { shutdownProducers(outboxProducer, realtimeOutboxProducer) _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } svc.SetMifaPayClient(mifaPayClient) } else { logx.Warn(context.Background(), "mifapay_enabled_without_credentials") } } if cfg.V5Pay.Enabled { if v5payConfigReady(cfg.V5Pay) { svc.SetV5PayClient(v5pay.New(cfg.V5Pay)) } else { logx.Warn(context.Background(), "v5pay_enabled_without_credentials") } } if cfg.TronGrid.Enabled { svc.SetTronUSDTClient(trongrid.New(cfg.TronGrid)) } walletv1.RegisterWalletServiceServer(server, grpcserver.NewServer(svc)) walletv1.RegisterWalletCronServiceServer(server, grpcserver.NewCronServer(svc)) health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{ ServiceName: "wallet-service", HTTPAddr: cfg.HealthHTTPAddr, NodeID: cfg.NodeID, Dependencies: []grpchealth.Dependency{{ Name: "mysql", Check: repository.Ping, }}, }) if err != nil { shutdownProducers(outboxProducer, realtimeOutboxProducer) _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } projectionConsumer, err := newWalletProjectionConsumer(cfg, svc) if err != nil { shutdownProducers(outboxProducer, realtimeOutboxProducer) _ = activityConn.Close() _ = listener.Close() _ = repository.Close() return nil, err } return &App{ server: server, listener: listener, health: health, healthHTTP: healthHTTP, mysqlRepo: repository, walletSvc: svc, activityConn: activityConn, outboxProducer: outboxProducer, realtimeOutboxProducer: realtimeOutboxProducer, projectionConsumer: projectionConsumer, outboxWorkerCfg: cfg.OutboxWorker, realtimeOutboxWorkerCfg: cfg.RealtimeOutboxWorker, projectionWorkerCfg: cfg.ProjectionWorker, externalRechargeReconcileWorkerCfg: cfg.ExternalRechargeReconcileWorker, walletOutboxTopic: cfg.RocketMQ.WalletOutbox.Topic, realtimeWalletOutboxTopic: cfg.RocketMQ.RealtimeOutbox.Topic, nodeID: cfg.NodeID, redPacketExpiryWorkerCfg: cfg.RedPacketExpiryWorker, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { a.runHealthHTTP() if err := a.startMQ(); err != nil { a.health.MarkStopped() return err } a.runBackgroundWorkers() a.health.MarkServing() err := a.server.Serve(a.listener) a.health.MarkStopped() a.closeBackgroundWorkers() a.shutdownMQ() if errors.Is(err, grpc.ErrServerStopped) { return nil } return err } // Close 关闭 gRPC 服务。 func (a *App) Close() { a.closeOnce.Do(func() { a.health.MarkDraining() a.closeBackgroundWorkers() servicegrpc.GracefulStop(a.server, 15*time.Second) a.closeHealthHTTP() if a.activityConn != nil { _ = a.activityConn.Close() } a.shutdownMQ() if a.mysqlRepo != nil { _ = a.mysqlRepo.Close() } }) }