// Package app 装配 user-service 的 gRPC 入口、领域服务、repository 和健康检查。 package app import ( "context" "errors" "net" "sync" "time" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/idgen" "hyapp/pkg/logx" "hyapp/services/user-service/internal/config" authservice "hyapp/services/user-service/internal/service/auth" hostservice "hyapp/services/user-service/internal/service/host" userservice "hyapp/services/user-service/internal/service/user" mysqlstorage "hyapp/services/user-service/internal/storage/mysql" grpcserver "hyapp/services/user-service/internal/transport/grpc" "google.golang.org/grpc" healthgrpc "google.golang.org/grpc/health/grpc_health_v1" ) // App 装配 user-service gRPC 入口和底座依赖。 type App struct { // server 承载 AuthService、UserService、UserIdentityService 和 gRPC health。 server *grpc.Server // listener 是 user-service 的唯一网络入口。 listener net.Listener // health 是标准 gRPC health 的 serving/draining 状态来源。 health *grpchealth.ServingChecker // mysqlRepo 持有用户主数据、认证身份、session 和短号事务的唯一运行时存储。 mysqlRepo *mysqlstorage.Repository // userSvc 持有用户主数据用例,后台 region rebuild worker 复用它消费任务。 userSvc *userservice.Service // cfg 保存 worker 运行参数,避免 Run 阶段重新读配置。 cfg config.Config // workerCtx 统一控制后台 worker 生命周期。 workerCtx context.Context // workerCancel 停止 region rebuild worker。 workerCancel context.CancelFunc // workerWG 等待后台 worker 当前批次安全退出。 workerWG sync.WaitGroup // closeOnce 防止信号退出和 Serve 异常同时触发重复关闭。 closeOnce sync.Once } // New 初始化 user-service 应用。 func New(cfg config.Config) (*App, error) { // userRepository 只表达用户主数据用例,authRepository 只表达认证用例;MySQL 实现同时满足两者。 startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // user-service 的登录、注册、短号和 session 都必须落 MySQL;空 DSN 直接启动失败。 mysqlRepo, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) if err != nil { return nil, err } // listener 在 New 阶段创建,端口占用会作为启动失败返回。 listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { if mysqlRepo != nil { _ = mysqlRepo.Close() } return nil, err } server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("user-service"))) thirdPartyVerifier := authservice.NewRoutedThirdPartyVerifier( authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{ ProjectID: cfg.ThirdParty.Firebase.ProjectID, AllowedSignInProviders: cfg.ThirdParty.Firebase.AllowedSignInProviders, CertsURL: cfg.ThirdParty.Firebase.CertsURL, }), authservice.NewStaticThirdPartyVerifier(cfg.ThirdParty.AllowedProviders), ) authRepo := mysqlRepo.AuthRepository() appRepo := mysqlRepo.AppRepository() userRepo := mysqlRepo.UserRepository() identityRepo := mysqlRepo.IdentityRepository() regionRepo := mysqlRepo.RegionRepository() hostRepo := mysqlRepo.HostRepository() // auth service 负责登录、session 和 token;用户主数据和短号事务仍通过各自领域存储完成。 authSvc := authservice.New(authservice.Config{ Issuer: cfg.JWT.Issuer, AccessTokenTTLSec: cfg.JWT.AccessTokenTTLSec, RefreshTokenTTLSec: cfg.JWT.RefreshTokenTTLSec, SigningAlg: cfg.JWT.SigningAlg, SigningSecret: cfg.JWT.SigningSecret, }, authservice.WithAuthRepository(authRepo), authservice.WithUserRepository(userRepo), authservice.WithIdentityRepository(identityRepo), authservice.WithCountryRegionRepository(regionRepo), authservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), authservice.WithDisplayUserIDAllocateMaxAttempts(cfg.DisplayUserID.AllocateMaxAttempts), authservice.WithThirdPartyVerifier(thirdPartyVerifier), ) // user service 负责用户主状态和 display_user_id/靓号用例,不进入房间高频流程。 userSvc := userservice.New(userRepo, userservice.WithAppRegistryRepository(appRepo), userservice.WithIdentityRepository(identityRepo), userservice.WithCountryRegionRepository(regionRepo), userservice.WithCountryAdminRepository(regionRepo), userservice.WithRegionAdminRepository(regionRepo), userservice.WithRegionRebuildRepository(regionRepo), userservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), userservice.WithDisplayUserIDPolicy(cfg.DisplayUserID.AllocateMaxAttempts, time.Duration(cfg.DisplayUserID.ChangeCooldownSec)*time.Second), ) hostSvc := hostservice.New(hostRepo, hostservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), ) userServer := grpcserver.NewServer(authSvc, userSvc, hostSvc) // 多个 protobuf service 共用一个 Server 适配器,领域逻辑仍拆在 auth/user/host service。 userv1.RegisterAuthServiceServer(server, userServer) userv1.RegisterUserServiceServer(server, userServer) userv1.RegisterAppRegistryServiceServer(server, userServer) userv1.RegisterUserIdentityServiceServer(server, userServer) userv1.RegisterCountryAdminServiceServer(server, userServer) userv1.RegisterCountryQueryServiceServer(server, userServer) userv1.RegisterRegionAdminServiceServer(server, userServer) userv1.RegisterUserHostServiceServer(server, userServer) userv1.RegisterUserHostAdminServiceServer(server, userServer) health := grpchealth.NewServingChecker("user-service") // user-service 目前使用简单 serving checker,存储探测由后续专用 health 可扩展。 healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health)) workerCtx, workerCancel := context.WithCancel(context.Background()) return &App{ server: server, listener: listener, health: health, mysqlRepo: mysqlRepo, userSvc: userSvc, cfg: cfg, workerCtx: workerCtx, workerCancel: workerCancel, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { // 只有 listener 进入 Serve 后才标记 serving,避免健康检查提前放行。 a.health.MarkServing() defer func() { if a.workerCancel != nil { a.workerCancel() } a.workerWG.Wait() }() if a.cfg.RegionRebuildWorker.Enabled { a.workerWG.Add(1) go func() { defer a.workerWG.Done() a.userSvc.RunRegionRebuildWorker(a.workerCtx, userservice.RegionRebuildWorkerOptions{ WorkerID: a.cfg.NodeID, PollInterval: time.Duration(a.cfg.RegionRebuildWorker.PollIntervalSec) * time.Second, LockTTL: time.Duration(a.cfg.RegionRebuildWorker.LockTTLSec) * time.Second, BatchSize: a.cfg.RegionRebuildWorker.BatchSize, }) }() } err := a.server.Serve(a.listener) a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { // GracefulStop 触发的退出不是故障。 return nil } return err } // Close 关闭 gRPC 服务和底层持久化连接。 func (a *App) Close() { a.closeOnce.Do(func() { // draining 会让 health 立即失败,避免下线进程继续接新 RPC。 a.health.MarkDraining() if a.workerCancel != nil { a.workerCancel() } a.workerWG.Wait() // GracefulStop 等待已进入的 RPC 结束,避免 token/session 写入被硬切。 a.server.GracefulStop() if a.mysqlRepo != nil { // MySQL 连接池最后关闭,保证 GracefulStop 期间 repository 仍可用。 _ = a.mysqlRepo.Close() } }) }