2026-04-27 02:29:42 +08:00

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