2026-05-02 13:02:38 +08:00

194 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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()
}
})
}