// Package app 装配 user-service 的 gRPC 入口、领域服务、repository 和健康检查。 package app import ( "context" "errors" "net" "sync" "time" userv1 "hyapp/api/proto/user/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/idgen" "hyapp/services/user-service/internal/config" authservice "hyapp/services/user-service/internal/service/auth" userservice "hyapp/services/user-service/internal/service/user" "hyapp/services/user-service/internal/storage/memory" 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 在配置 MySQL 时持有连接池,memory 模式下为 nil。 mysqlRepo *mysqlstorage.Repository // closeOnce 防止信号退出和 Serve 异常同时触发重复关闭。 closeOnce sync.Once } // New 初始化 user-service 应用。 func New(cfg config.Config) (*App, error) { // userRepository 只表达用户主数据用例,authRepository 只表达认证用例;MySQL 实现同时满足两者。 var userRepository userservice.Repository var authRepository authservice.Repository var mysqlRepo *mysqlstorage.Repository if cfg.MySQLDSN != "" { // 配置 mysql_dsn 时走生产持久化路径,启动阶段必须确认 MySQL 可连接。 startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() repo, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) if err != nil { return nil, err } mysqlRepo = repo userRepository = repo authRepository = repo } else { // 未配置 MySQL 时使用 memory repository 让本地单测和进程骨架可独立启动。 // 生产配置必须提供 mysql_dsn,不能依赖该分支保存用户主数据。 repository := memory.NewRepository() userRepository = repository authRepository = repository } // listener 在 New 阶段创建,端口占用会作为启动失败返回。 listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { if mysqlRepo != nil { _ = mysqlRepo.Close() } return nil, err } server := grpc.NewServer() // auth service 负责登录、session 和 token;用户主数据和短号事务仍通过 repository 完成。 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.WithRepository(authRepository), authservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), authservice.WithDisplayUserIDAllocateMaxAttempts(cfg.DisplayUserID.AllocateMaxAttempts), authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier(cfg.ThirdParty.AllowedProviders)), ) // user service 负责用户主状态和 display_user_id/靓号用例,不进入房间高频流程。 userSvc := userservice.New(userRepository, userservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), userservice.WithDisplayUserIDPolicy(cfg.DisplayUserID.AllocateMaxAttempts, time.Duration(cfg.DisplayUserID.ChangeCooldownSec)*time.Second), ) userServer := grpcserver.NewServer(authSvc, userSvc) // 三个 protobuf service 共用一个 Server 适配器,但领域逻辑仍拆在 auth/user service。 userv1.RegisterAuthServiceServer(server, userServer) userv1.RegisterUserServiceServer(server, userServer) userv1.RegisterUserIdentityServiceServer(server, userServer) health := grpchealth.NewServingChecker("user-service") // user-service 目前使用简单 serving checker,存储探测由后续专用 health 可扩展。 healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health)) return &App{ server: server, listener: listener, health: health, mysqlRepo: mysqlRepo, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { // 只有 listener 进入 Serve 后才标记 serving,避免健康检查提前放行。 a.health.MarkServing() 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() // GracefulStop 等待已进入的 RPC 结束,避免 token/session 写入被硬切。 a.server.GracefulStop() if a.mysqlRepo != nil { // MySQL 连接池最后关闭,保证 GracefulStop 期间 repository 仍可用。 _ = a.mysqlRepo.Close() } }) }