package app import ( "context" "errors" "fmt" "net" "net/http" "os" "strings" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "hyapp/pkg/tencentcos" "hyapp/services/gateway-service/internal/appconfig" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/client" "hyapp/services/gateway-service/internal/config" "hyapp/services/gateway-service/internal/healthcheck" httptransport "hyapp/services/gateway-service/internal/transport/http" ) // App 装配 gateway 的 HTTP 入口。 type App struct { server *http.Server listener net.Listener roomConn *grpc.ClientConn userConn *grpc.ClientConn walletConn *grpc.ClientConn activityConn *grpc.ClientConn appConfig *appconfig.MySQLReader redisClose func() error health *healthcheck.State closeOnce sync.Once } // New 初始化 gateway 应用。 func New(cfg config.Config) (*App, error) { if err := cfg.Normalize(); err != nil { return nil, err } roomConn, err := grpc.Dial(cfg.RoomServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err } userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = roomConn.Close() return nil, err } walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = roomConn.Close() _ = userConn.Close() return nil, err } activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() return nil, err } var roomClient client.RoomClient = client.NewGRPCRoomClient(roomConn) var roomGuardClient client.RoomGuardClient = client.NewGRPCRoomGuardClient(roomConn) var roomQueryClient client.RoomQueryClient = client.NewGRPCRoomQueryClient(roomConn) var userClient client.UserAuthClient = client.NewGRPCUserAuthClient(userConn) var userIdentityClient client.UserIdentityClient = client.NewGRPCUserIdentityClient(userConn) var userProfileClient client.UserProfileClient = client.NewGRPCUserProfileClient(userConn) var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn) var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn) var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn) var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn) var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn) var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn) var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn) appConfigReader, err := openAppConfigReader(cfg.AppConfig) if err != nil { _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() _ = activityConn.Close() return nil, err } handler := httptransport.NewHandlerWithConfig(roomClient, roomGuardClient, userClient, userIdentityClient, httptransport.TencentIMConfig{ SDKAppID: cfg.TencentIM.SDKAppID, SecretKey: cfg.TencentIM.SecretKey, UserSigTTL: cfg.TencentIM.UserSigTTL, CallbackAuthToken: cfg.TencentIM.CallbackAuthToken, }, userProfileClient) handler.SetRoomQueryClient(roomQueryClient) handler.SetUserDeviceClient(userDeviceClient) handler.SetUserCountryQueryClient(userCountryQueryClient) handler.SetUserHostClient(userHostClient) handler.SetAppRegistryClient(appRegistryClient) handler.SetWalletClient(walletClient) handler.SetMessageInboxClient(messageClient) handler.SetTaskClient(taskClient) if appConfigReader != nil { handler.SetAppConfigReader(appConfigReader) } handler.SetTencentRTC(httptransport.TencentRTCConfig{ Enabled: cfg.TencentRTC.Enabled, SDKAppID: cfg.TencentRTC.SDKAppID, SecretKey: cfg.TencentRTC.SecretKey, UserSigTTL: cfg.TencentRTC.UserSigTTL, RoomIDType: cfg.TencentRTC.RoomIDType, AppScene: cfg.TencentRTC.AppScene, CallbackSignKey: cfg.TencentRTC.CallbackSignKey, }) if cfg.TencentCOS.Enabled { uploader, err := tencentcos.NewUploader(tencentcos.Config{ SecretID: cfg.TencentCOS.SecretID, SecretKey: cfg.TencentCOS.SecretKey, Bucket: cfg.TencentCOS.BucketName, Region: cfg.TencentCOS.Region, AccessURL: cfg.TencentCOS.AccessURL, }, nil) if err != nil { if appConfigReader != nil { _ = appConfigReader.Close() } _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() _ = activityConn.Close() return nil, err } handler.SetObjectUploader(uploader) } rateLimitConfig := httptransport.AuthRateLimitConfig{ Enabled: cfg.AuthRateLimit.Enabled, KeyPrefix: cfg.AuthRateLimit.KeyPrefix, Window: time.Duration(cfg.AuthRateLimit.WindowSec) * time.Second, IPLimit: cfg.AuthRateLimit.IPLimit, DeviceIDLimit: cfg.AuthRateLimit.DeviceIDLimit, ProviderIPLimit: cfg.AuthRateLimit.ProviderIPLimit, DisplayUserIDIPLimit: cfg.AuthRateLimit.DisplayUserIDIPLimit, DisplayUserIDLimit: cfg.AuthRateLimit.DisplayUserIDLimit, SessionIDIPLimit: cfg.AuthRateLimit.SessionIDIPLimit, } loginRiskConfig := httptransport.LoginRiskConfig{ Enabled: cfg.LoginRisk.Enabled, KeyPrefix: cfg.LoginRisk.KeyPrefix, FastGuard: httptransport.LoginRiskFastGuardConfig{ BlockedLanguages: cfg.LoginRisk.FastGuard.BlockedLanguages, BlockedLanguagePrimaryTags: cfg.LoginRisk.FastGuard.BlockedLanguagePrimaryTags, BlockedTimezones: cfg.LoginRisk.FastGuard.BlockedTimezones, BlockedTimezonePrefixes: cfg.LoginRisk.FastGuard.BlockedTimezonePrefixes, }, } handler.SetLoginRisk(loginRiskConfig) redisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig) if err != nil { if appConfigReader != nil { _ = appConfigReader.Close() } _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() _ = activityConn.Close() return nil, err } loginRiskRedisClose, err := configureLoginRisk(handler, cfg.LoginRisk, loginRiskConfig) if err != nil { if redisClose != nil { _ = redisClose() } if appConfigReader != nil { _ = appConfigReader.Close() } _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() _ = activityConn.Close() return nil, err } redisClose = joinClose(redisClose, loginRiskRedisClose) verifier := auth.NewVerifier(cfg.JWTSecret) healthState := healthcheck.NewState(nodeID(), cfg.JWTSecret, roomConn) mux := http.NewServeMux() healthHandler := healthcheck.NewHTTPHandler(healthState) mux.HandleFunc("/healthz/live", healthHandler.Live) mux.HandleFunc("/healthz/ready", healthHandler.Ready) mux.Handle("/", handler.Routes(verifier)) listener, err := net.Listen("tcp", cfg.HTTPAddr) if err != nil { if redisClose != nil { _ = redisClose() } if appConfigReader != nil { _ = appConfigReader.Close() } _ = roomConn.Close() _ = userConn.Close() _ = walletConn.Close() _ = activityConn.Close() return nil, err } server := &http.Server{ Handler: mux, } return &App{ server: server, listener: listener, roomConn: roomConn, userConn: userConn, walletConn: walletConn, activityConn: activityConn, appConfig: appConfigReader, redisClose: redisClose, health: healthState, }, nil } func openAppConfigReader(cfg config.AppConfigConfig) (*appconfig.MySQLReader, error) { if strings.TrimSpace(cfg.MySQLDSN) == "" { return nil, nil } return appconfig.OpenMySQLReader(cfg.MySQLDSN) } func configureAuthRateLimit(handler *httptransport.Handler, cfg config.AuthRateLimitConfig, rateLimitConfig httptransport.AuthRateLimitConfig) (func() error, error) { if !rateLimitConfig.Enabled { // 显式关闭频控时仍记录配置,避免 handler 使用默认开启策略。 handler.SetAuthRateLimit(rateLimitConfig) return nil, nil } redisAddr := strings.TrimSpace(cfg.RedisAddr) if redisAddr == "" { return nil, errors.New("auth rate limit redis_addr is required") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() redisClient, err := httptransport.NewAuthRateLimitRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB) if err != nil { return nil, fmt.Errorf("connect auth rate limit redis: %w", err) } // 生产入口必须走 Redis 共享计数;内存 backend 只保留给单元测试和显式禁用频控。 handler.SetAuthRateLimitBackend(rateLimitConfig, httptransport.NewRedisAuthRateLimiter(redisClient, rateLimitConfig)) return redisClient.Close, nil } func configureLoginRisk(handler *httptransport.Handler, cfg config.LoginRiskConfig, riskConfig httptransport.LoginRiskConfig) (func() error, error) { if !riskConfig.Enabled { handler.SetLoginRisk(riskConfig) return nil, nil } redisAddr := strings.TrimSpace(cfg.RedisAddr) if redisAddr == "" { handler.SetLoginRisk(riskConfig) return nil, nil } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() redisClient, err := httptransport.NewLoginRiskRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB) if err != nil { // IP 风控缓存按文档 fail-open:Redis 不可用不能阻断 gateway 启动或登录。 handler.SetLoginRisk(riskConfig) return nil, nil } handler.SetLoginRiskCache(riskConfig, httptransport.NewRedisLoginRiskCache(redisClient, riskConfig)) return redisClient.Close, nil } func joinClose(left func() error, right func() error) func() error { if left == nil { return right } if right == nil { return left } return func() error { return errors.Join(left(), right()) } } // Run 启动 HTTP 服务。 func (a *App) Run() error { a.health.MarkHTTPServing() err := a.server.Serve(a.listener) a.health.MarkHTTPStopped() if errors.Is(err, http.ErrServerClosed) { return nil } return err } // Close 关闭 HTTP 服务。 func (a *App) Close() error { var err error a.closeOnce.Do(func() { a.health.MarkDraining() if a.roomConn != nil { err = errors.Join(err, a.roomConn.Close()) } if a.userConn != nil { err = errors.Join(err, a.userConn.Close()) } if a.walletConn != nil { err = errors.Join(err, a.walletConn.Close()) } if a.activityConn != nil { err = errors.Join(err, a.activityConn.Close()) } if a.appConfig != nil { err = errors.Join(err, a.appConfig.Close()) } if a.redisClose != nil { err = errors.Join(err, a.redisClose()) } err = errors.Join(err, a.server.Close()) }) return err } func nodeID() string { hostname, err := os.Hostname() if err != nil || hostname == "" { return "gateway-service" } return hostname }