537 lines
19 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
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"google.golang.org/grpc"
"hyapp/pkg/dingtalkrobot"
"hyapp/pkg/grpcclient"
serviceapp "hyapp/pkg/servicekit/app"
"hyapp/pkg/tencentcos"
"hyapp/pkg/tencentim"
"hyapp/pkg/userleaderboard"
"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/financewithdrawal"
"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
luckyGiftConn *grpc.ClientConn
gameConn *grpc.ClientConn
appConfig *appconfig.MySQLReader
withdrawal *financewithdrawal.MySQLWriter
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
}
grpcConfig := cfg.GRPCClient.Runtime()
roomConn, err := grpcclient.Dial(cfg.RoomServiceAddr, grpcConfig)
if err != nil {
return nil, err
}
userConn, err := grpcclient.Dial(cfg.UserServiceAddr, grpcConfig)
if err != nil {
_ = roomConn.Close()
return nil, err
}
walletConn, err := grpcclient.Dial(cfg.WalletServiceAddr, grpcConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
return nil, err
}
activityConn, err := grpcclient.Dial(cfg.ActivityServiceAddr, grpcConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
return nil, err
}
luckyGiftConn, err := grpcclient.Dial(cfg.LuckyGiftServiceAddr, grpcConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
return nil, err
}
gameConn, err := grpcclient.Dial(cfg.GameServiceAddr, grpcConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
_ = luckyGiftConn.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 userInviteClient client.UserInviteClient = client.NewGRPCUserInviteClient(userConn)
var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
var userRegionClient client.UserRegionClient = client.NewGRPCUserRegionClient(userConn)
var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
var userHostAdminClient client.UserHostAdminClient = client.NewGRPCUserHostAdminClient(userConn)
var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
var userCPClient client.UserCPClient = client.NewGRPCUserCPClient(userConn)
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
var messageActionClient client.MessageActionConfirmClient = client.NewGRPCMessageActionConfirmClient(activityConn)
var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn)
var growthLevelClient client.GrowthLevelClient = client.NewGRPCGrowthLevelClient(activityConn)
var achievementClient client.AchievementClient = client.NewGRPCAchievementClient(activityConn)
var registrationRewardClient client.RegistrationRewardClient = client.NewGRPCRegistrationRewardClient(activityConn)
var firstRechargeRewardClient client.FirstRechargeRewardClient = client.NewGRPCFirstRechargeRewardClient(activityConn)
var cumulativeRechargeRewardClient client.CumulativeRechargeRewardClient = client.NewGRPCCumulativeRechargeRewardClient(activityConn)
var inviteActivityRewardClient client.InviteActivityRewardClient = client.NewGRPCInviteActivityRewardClient(activityConn)
var sevenDayCheckInClient client.SevenDayCheckInClient = client.NewGRPCSevenDayCheckInClient(activityConn)
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(luckyGiftConn)
var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(activityConn)
var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
var cpWeeklyRankClient client.CPWeeklyRankClient = client.NewGRPCCPWeeklyRankClient(activityConn)
var agencyOpeningClient client.AgencyOpeningClient = client.NewGRPCAgencyOpeningClient(activityConn)
var wheelClient client.WheelClient = client.NewGRPCWheelClient(activityConn)
var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
var statisticsClient client.StatisticsClient = client.NewHTTPStatisticsClient(cfg.Statistics.BaseURL, cfg.Statistics.Timeout)
appConfigReader, err := openAppConfigReader(cfg.AppConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
_ = luckyGiftConn.Close()
_ = gameConn.Close()
return nil, err
}
withdrawalWriter, err := openWithdrawalApplicationWriter(cfg.AppConfig)
if err != nil {
if appConfigReader != nil {
_ = appConfigReader.Close()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
_ = luckyGiftConn.Close()
_ = gameConn.Close()
return nil, err
}
withdrawalNotifier, err := openWithdrawalApplicationNotifier(cfg.FinanceNotifications)
if err != nil {
if withdrawalWriter != nil {
_ = withdrawalWriter.Close()
}
if appConfigReader != nil {
_ = appConfigReader.Close()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
_ = luckyGiftConn.Close()
_ = gameConn.Close()
return nil, err
}
closeGatewayDeps := func(redisClose func() error) {
if withdrawalWriter != nil {
_ = withdrawalWriter.Close()
}
if appConfigReader != nil {
_ = appConfigReader.Close()
}
if redisClose != nil {
_ = redisClose()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
_ = luckyGiftConn.Close()
_ = gameConn.Close()
}
handler := httptransport.NewHandlerWithConfig(roomClient, roomGuardClient, userClient, userIdentityClient, httptransport.TencentIMConfig{
SDKAppID: cfg.TencentIM.SDKAppID,
SecretKey: cfg.TencentIM.SecretKey,
UserSigTTL: cfg.TencentIM.UserSigTTL,
AdminIdentifier: cfg.TencentIM.AdminIdentifier,
CallbackAuthToken: cfg.TencentIM.CallbackAuthToken,
GroupIDPrefix: cfg.TencentIM.GroupIDPrefix,
}, userProfileClient)
if cfg.TencentIM.SDKAppID > 0 && strings.TrimSpace(cfg.TencentIM.SecretKey) != "" {
imClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
if err != nil {
closeGatewayDeps(nil)
return nil, err
}
handler.SetTencentIMAccountImporter(gatewayTencentIMAccountImporter{client: imClient})
handler.SetTencentIMUserMessagePublisher(imClient)
}
handler.SetRoomQueryClient(roomQueryClient)
handler.SetUserDeviceClient(userDeviceClient)
handler.SetUserInviteClient(userInviteClient)
handler.SetUserCountryQueryClient(userCountryQueryClient)
handler.SetUserRegionClient(userRegionClient)
handler.SetUserHostClient(userHostClient)
handler.SetUserHostAdminClient(userHostAdminClient)
handler.SetUserSocialClient(userSocialClient)
handler.SetUserCPClient(userCPClient)
handler.SetAppRegistryClient(appRegistryClient)
handler.SetWalletClient(walletClient)
handler.SetMessageInboxClient(messageClient)
handler.SetMessageActionConfirmClient(messageActionClient)
handler.SetTaskClient(taskClient)
handler.SetGrowthLevelClient(growthLevelClient)
handler.SetAchievementClient(achievementClient)
handler.SetRegistrationRewardClient(registrationRewardClient)
handler.SetFirstRechargeRewardClient(firstRechargeRewardClient)
handler.SetCumulativeRechargeRewardClient(cumulativeRechargeRewardClient)
handler.SetInviteActivityRewardClient(inviteActivityRewardClient)
handler.SetSevenDayCheckInClient(sevenDayCheckInClient)
handler.SetLuckyGiftClient(luckyGiftClient)
handler.SetRoomTurnoverRewardClient(roomTurnoverRewardClient)
handler.SetWeeklyStarClient(weeklyStarClient)
handler.SetCPWeeklyRankClient(cpWeeklyRankClient)
handler.SetAgencyOpeningClient(agencyOpeningClient)
handler.SetWheelClient(wheelClient)
handler.SetBroadcastClient(broadcastClient)
handler.SetGameClient(gameClient)
handler.SetStatisticsClient(statisticsClient)
handler.SetWithdrawalApplicationWriter(withdrawalWriter)
handler.SetWithdrawalApplicationNotifier(withdrawalNotifier)
leaderboardRedisClose, err := configureUserLeaderboard(handler, cfg.Leaderboard)
if err != nil {
closeGatewayDeps(nil)
return nil, err
}
redisClose := leaderboardRedisClose
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 {
closeGatewayDeps(redisClose)
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)
authRedisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig)
if err != nil {
closeGatewayDeps(redisClose)
return nil, err
}
redisClose = joinClose(redisClose, authRedisClose)
loginRiskRedisClose, err := configureLoginRisk(handler, cfg.LoginRisk, loginRiskConfig)
if err != nil {
closeGatewayDeps(redisClose)
return nil, err
}
redisClose = joinClose(redisClose, loginRiskRedisClose)
verifier := auth.NewVerifier(cfg.JWTSecret)
healthState := healthcheck.NewState(nodeID(), cfg.JWTSecret, roomConn,
healthcheck.GRPCDependency{Name: "user_grpc", Conn: userConn},
healthcheck.GRPCDependency{Name: "wallet_grpc", Conn: walletConn},
healthcheck.GRPCDependency{Name: "activity_grpc", Conn: activityConn},
healthcheck.GRPCDependency{Name: "lucky_gift_grpc", Conn: luckyGiftConn},
healthcheck.GRPCDependency{Name: "game_grpc", Conn: gameConn},
)
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 {
closeGatewayDeps(redisClose)
return nil, err
}
server := &http.Server{
Handler: httptransport.WithCORS(httptransport.CORSConfig{
Enabled: cfg.CORS.Enabled,
AllowedOrigins: cfg.CORS.AllowedOrigins,
AllowedMethods: cfg.CORS.AllowedMethods,
AllowedHeaders: cfg.CORS.AllowedHeaders,
ExposeHeaders: cfg.CORS.ExposeHeaders,
AllowCredentials: cfg.CORS.AllowCredentials,
MaxAgeSec: cfg.CORS.MaxAgeSec,
}, mux),
}
return &App{
server: server,
listener: listener,
roomConn: roomConn,
userConn: userConn,
walletConn: walletConn,
activityConn: activityConn,
luckyGiftConn: luckyGiftConn,
gameConn: gameConn,
appConfig: appConfigReader,
withdrawal: withdrawalWriter,
redisClose: redisClose,
health: healthState,
}, nil
}
func openAppConfigReader(cfg config.AppConfigConfig) (*appconfig.MySQLReader, error) {
if strings.TrimSpace(cfg.MySQLDSN) == "" {
return nil, nil
}
reader, err := appconfig.OpenMySQLReader(cfg.MySQLDSN)
if err != nil {
return nil, err
}
if strings.TrimSpace(cfg.RedisAddr) == "" {
return reader, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cacheClient, err := appconfig.OpenRedisCache(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)
if err != nil {
_ = reader.Close()
return nil, fmt.Errorf("connect app config redis: %w", err)
}
reader.SetRedisCache(cacheClient, cfg.KeyPrefix, cfg.CacheTTL)
return reader, nil
}
func openWithdrawalApplicationWriter(cfg config.AppConfigConfig) (*financewithdrawal.MySQLWriter, error) {
// H5 提现申请进入后台 finance 审核表,表在 hyapp_admin这里复用后台库 DSN但只开放单表写入器。
return financewithdrawal.OpenMySQLWriter(cfg.MySQLDSN)
}
func openWithdrawalApplicationNotifier(cfg config.FinanceNotificationsConfig) (financewithdrawal.Notifier, error) {
if !cfg.DingTalk.Enabled || strings.TrimSpace(cfg.DingTalk.WebhookURL) == "" {
return nil, nil
}
client, err := dingtalkrobot.New(dingtalkrobot.Config{
WebhookURL: cfg.DingTalk.WebhookURL,
Secret: cfg.DingTalk.Secret,
AtMobiles: cfg.DingTalk.AtMobiles,
AtAll: cfg.DingTalk.AtAll,
RequestTimeout: cfg.DingTalk.RequestTimeout,
}, nil)
if err != nil {
return nil, err
}
return financewithdrawal.NewDingTalkNotifier(client), nil
}
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 configureUserLeaderboard(handler *httptransport.Handler, cfg config.LeaderboardConfig) (func() error, error) {
redisAddr := strings.TrimSpace(cfg.RedisAddr)
if redisAddr == "" {
return nil, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
redisClient, err := userleaderboard.NewRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
if err != nil {
return nil, fmt.Errorf("connect user leaderboard redis: %w", err)
}
// gateway 只读取 activity-service 维护的 Redis 聚合结果,不再在 HTTP 请求中扫描 wallet_transactions。
handler.SetUserLeaderboardStore(userleaderboard.NewStore(redisClient, cfg.KeyPrefix))
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-openRedis 不可用不能阻断 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())
}
}
type gatewayTencentIMAccountImporter struct {
client *tencentim.RESTClient
}
func (i gatewayTencentIMAccountImporter) ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error {
return i.client.ImportAccount(ctx, tencentim.AccountProfile{
UserID: userID,
Nick: nickname,
FaceURL: faceURL,
})
}
// Run 启动 HTTP 服务。
func (a *App) Run() error {
a.health.MarkHTTPServing()
err := serviceapp.ServeHTTP(a.server, a.listener)
a.health.MarkHTTPStopped()
return err
}
// Close 关闭 HTTP 服务。
func (a *App) Close() error {
var err error
a.closeOnce.Do(func() {
a.health.MarkDraining()
// HTTP drain 要先于内部 gRPC 连接关闭,保证已进入 handler 的编排请求能自然完成。
err = errors.Join(err, serviceapp.ShutdownHTTP(a.server, 15*time.Second))
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.luckyGiftConn != nil {
err = errors.Join(err, a.luckyGiftConn.Close())
}
if a.gameConn != nil {
err = errors.Join(err, a.gameConn.Close())
}
if a.appConfig != nil {
err = errors.Join(err, a.appConfig.Close())
}
if a.withdrawal != nil {
err = errors.Join(err, a.withdrawal.Close())
}
if a.redisClose != nil {
err = errors.Join(err, a.redisClose())
}
})
return err
}
func nodeID() string {
hostname, err := os.Hostname()
if err != nil || hostname == "" {
return "gateway-service"
}
return hostname
}