package main import ( "context" "database/sql" "errors" "flag" "log" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "hyapp-admin-server/internal/cache" "hyapp-admin-server/internal/config" "hyapp-admin-server/internal/integration/activityclient" dingtalkclient "hyapp-admin-server/internal/integration/dingtalk" "hyapp-admin-server/internal/integration/gameclient" "hyapp-admin-server/internal/integration/robotclient" "hyapp-admin-server/internal/integration/roomclient" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/integration/walletclient" jobrunner "hyapp-admin-server/internal/job" "hyapp-admin-server/internal/migration" achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig" adminusermodule "hyapp-admin-server/internal/modules/adminuser" agencyopeningmodule "hyapp-admin-server/internal/modules/agencyopening" appconfigmodule "hyapp-admin-server/internal/modules/appconfig" appregistrymodule "hyapp-admin-server/internal/modules/appregistry" appusermodule "hyapp-admin-server/internal/modules/appuser" auditmodule "hyapp-admin-server/internal/modules/audit" authmodule "hyapp-admin-server/internal/modules/auth" coinledgermodule "hyapp-admin-server/internal/modules/coinledger" countryregionmodule "hyapp-admin-server/internal/modules/countryregion" cprelationmodule "hyapp-admin-server/internal/modules/cprelation" cpweeklyrankmodule "hyapp-admin-server/internal/modules/cpweeklyrank" cumulativerechargerewardmodule "hyapp-admin-server/internal/modules/cumulativerechargereward" dailytaskmodule "hyapp-admin-server/internal/modules/dailytask" dashboardmodule "hyapp-admin-server/internal/modules/dashboard" financeapplicationmodule "hyapp-admin-server/internal/modules/financeapplication" financewithdrawalmodule "hyapp-admin-server/internal/modules/financewithdrawal" firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward" fullservernoticemodule "hyapp-admin-server/internal/modules/fullservernotice" gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement" giftdiamondmodule "hyapp-admin-server/internal/modules/giftdiamond" healthmodule "hyapp-admin-server/internal/modules/health" hostagencypolicymodule "hyapp-admin-server/internal/modules/hostagencypolicy" hostorgmodule "hyapp-admin-server/internal/modules/hostorg" hostsalarysettlementmodule "hyapp-admin-server/internal/modules/hostsalarysettlement" hostwithdrawalmodule "hyapp-admin-server/internal/modules/hostwithdrawal" inviteactivityrewardmodule "hyapp-admin-server/internal/modules/inviteactivityreward" jobmodule "hyapp-admin-server/internal/modules/job" levelconfigmodule "hyapp-admin-server/internal/modules/levelconfig" luckygiftmodule "hyapp-admin-server/internal/modules/luckygift" menumodule "hyapp-admin-server/internal/modules/menu" paymentmodule "hyapp-admin-server/internal/modules/payment" prettyidmodule "hyapp-admin-server/internal/modules/prettyid" rbacmodule "hyapp-admin-server/internal/modules/rbac" redpacketmodule "hyapp-admin-server/internal/modules/redpacket" regionblockmodule "hyapp-admin-server/internal/modules/regionblock" registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward" reportmodule "hyapp-admin-server/internal/modules/report" resourcemodule "hyapp-admin-server/internal/modules/resource" riskconfigmodule "hyapp-admin-server/internal/modules/riskconfig" roomadminmodule "hyapp-admin-server/internal/modules/roomadmin" roomrocketmodule "hyapp-admin-server/internal/modules/roomrocket" roomturnoverrewardmodule "hyapp-admin-server/internal/modules/roomturnoverreward" searchmodule "hyapp-admin-server/internal/modules/search" sevendaycheckinmodule "hyapp-admin-server/internal/modules/sevendaycheckin" teammodule "hyapp-admin-server/internal/modules/team" teamsalarypolicymodule "hyapp-admin-server/internal/modules/teamsalarypolicy" teamsalarysettlementmodule "hyapp-admin-server/internal/modules/teamsalarysettlement" uploadmodule "hyapp-admin-server/internal/modules/upload" userleaderboardmodule "hyapp-admin-server/internal/modules/userleaderboard" vipconfigmodule "hyapp-admin-server/internal/modules/vipconfig" weeklystarmodule "hyapp-admin-server/internal/modules/weeklystar" wheelmodule "hyapp-admin-server/internal/modules/wheel" "hyapp-admin-server/internal/platform/logging" "hyapp-admin-server/internal/platform/tencentcos" "hyapp-admin-server/internal/repository" "hyapp-admin-server/internal/router" "hyapp-admin-server/internal/service" mysqlDriver "github.com/go-sql-driver/mysql" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/mongo/readpref" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" walletv1 "hyapp.local/api/proto/wallet/v1" ) func main() { configPath := flag.String("config", "configs/config.yaml", "path to admin-server config") runBootstrap := flag.Bool("bootstrap", false, "run configured bootstrap seed before starting") flag.Parse() cfg, err := config.Load(*configPath) if err != nil { log.Fatal(err) } if err := logging.Init(cfg.ServiceName, cfg.Environment, cfg.NodeID, cfg.Log); err != nil { log.Fatal(err) } if err := ensureDatabase(cfg.MySQLDSN); err != nil { fatalRuntime("ensure_mysql_database_failed", err) } dbLogger := logger.New(log.New(logging.StdWriter(), "", 0), logger.Config{ LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, }) db, err := gorm.Open(mysql.Open(cfg.MySQLDSN), &gorm.Config{ Logger: dbLogger, NowFunc: func() time.Time { return time.Now().UTC() }, }) if err != nil { fatalRuntime("connect_mysql_failed", err) } sqlDB, err := db.DB() if err != nil { fatalRuntime("mysql_db_handle_failed", err) } if cfg.Migrations.Enabled { if err := migration.ApplyWithOptions(context.Background(), sqlDB, cfg.Migrations.Dir, migration.Options{ AllowChecksumRepair: cfg.Migrations.AllowChecksumRepair, }); err != nil { fatalRuntime("apply_migrations_failed", err) } } redisClient, err := cache.NewRedis(context.Background(), cfg.Redis) if err != nil { fatalRuntime("connect_redis_failed", err) } if redisClient != nil { defer redisClient.Close() } userDB, err := sql.Open("mysql", cfg.UserMySQLDSN) if err != nil { fatalRuntime("connect_user_mysql_failed", err) } if err := userDB.PingContext(context.Background()); err != nil { fatalRuntime("ping_user_mysql_failed", err) } defer userDB.Close() walletDB, err := sql.Open("mysql", cfg.WalletMySQLDSN) if err != nil { fatalRuntime("connect_wallet_mysql_failed", err) } if err := walletDB.PingContext(context.Background()); err != nil { fatalRuntime("ping_wallet_mysql_failed", err) } defer walletDB.Close() var robotProfileSource gamemanagementmodule.RobotProfileSource if strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN) != "" { robotProfileDB, err := sql.Open("mysql", cfg.RobotProfileSource.MySQLDSN) if err != nil { fatalRuntime("connect_robot_profile_mysql_failed", err) } robotProfileCtx, robotProfileCancel := context.WithTimeout(context.Background(), cfg.RobotProfileSource.RequestTimeout) if err := robotProfileDB.PingContext(robotProfileCtx); err != nil { robotProfileCancel() fatalRuntime("ping_robot_profile_mysql_failed", err) } robotProfileCancel() defer robotProfileDB.Close() robotProfileSource = gamemanagementmodule.NewLikeiRobotProfileSource(robotProfileDB) } moneyRegionSources, closeMoneyRegionSources, err := connectMoneyRegionSources(context.Background(), cfg.MoneyRegionSources) if err != nil { fatalRuntime("connect_money_region_sources_failed", err) } defer closeMoneyRegionSources(context.Background()) financeBillSources, closeFinanceBillSources, err := connectFinanceBillSources(context.Background(), cfg.FinanceBillSources, moneyRegionSources) if err != nil { fatalRuntime("connect_finance_bill_sources_failed", err) } defer closeFinanceBillSources(context.Background()) dashboardExternalSources, closeDashboardExternalSources, err := connectDashboardExternalSources(context.Background(), cfg.DashboardExternalSources, dashboardRegionResolvers(moneyRegionSources)...) if err != nil { fatalRuntime("connect_dashboard_external_sources_failed", err) } defer closeDashboardExternalSources() store := repository.New(db) if cfg.MySQLAutoMigrate { if err := store.AutoMigrate(); err != nil { fatalRuntime("auto_migrate_failed", err) } } if *runBootstrap || cfg.Bootstrap.Enabled { if err := store.Seed(cfg); err != nil { fatalRuntime("seed_database_failed", err) } } userConn, err := dialBackendGRPC(cfg.UserService.Addr) if err != nil { fatalRuntime("connect_user_service_failed", err) } defer userConn.Close() walletConn, err := dialBackendGRPC(cfg.WalletService.Addr) if err != nil { fatalRuntime("connect_wallet_service_failed", err) } defer walletConn.Close() roomConn, err := dialBackendGRPC(cfg.RoomService.Addr) if err != nil { fatalRuntime("connect_room_service_failed", err) } defer roomConn.Close() roomClient := roomclient.NewGRPC(roomConn) robotConn, err := dialBackendGRPC(cfg.RobotService.Addr) if err != nil { fatalRuntime("connect_robot_service_failed", err) } defer robotConn.Close() robotClient := robotclient.NewGRPC(robotConn) activityConn, err := dialBackendGRPC(cfg.ActivityService.Addr) if err != nil { fatalRuntime("connect_activity_service_failed", err) } defer activityConn.Close() gameConn, err := dialBackendGRPC(cfg.GameService.Addr) if err != nil { fatalRuntime("connect_game_service_failed", err) } defer gameConn.Close() auth := service.NewAuthService(cfg.JWTSecret, cfg.AccessTokenTTL) var runner *jobrunner.Runner var jobStatus healthmodule.JobStatusProvider if cfg.Jobs.Enabled { runner = jobrunner.NewRunner(store, redisClient, cfg) runner.Start() defer runner.Close() jobStatus = runner } auditHandler := auditmodule.New(store) teamSalarySettlementHandler := teamsalarysettlementmodule.New(sqlDB, walletDB, userDB, auditHandler) if cfg.Jobs.Enabled { teamSalaryRunner := teamsalarysettlementmodule.NewAutoRunner(teamSalarySettlementHandler.Service(), "lalu", cfg.NodeID) teamSalaryRunner.Start() defer teamSalaryRunner.Close() } var objectUploader uploadmodule.ObjectUploader 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 { fatalRuntime("create_tencent_cos_uploader_failed", err) } objectUploader = uploader } var financeApplicationOptions []financeapplicationmodule.Option if cfg.FinanceNotifications.DingTalk.Enabled && strings.TrimSpace(cfg.FinanceNotifications.DingTalk.WebhookURL) != "" { client, err := dingtalkclient.New(dingtalkclient.Config{ WebhookURL: cfg.FinanceNotifications.DingTalk.WebhookURL, Secret: cfg.FinanceNotifications.DingTalk.Secret, AtMobiles: cfg.FinanceNotifications.DingTalk.AtMobiles, AtAll: cfg.FinanceNotifications.DingTalk.AtAll, RequestTimeout: cfg.FinanceNotifications.DingTalk.RequestTimeout, }, nil) if err != nil { fatalRuntime("create_finance_dingtalk_client_failed", err) } financeApplicationOptions = append(financeApplicationOptions, financeapplicationmodule.WithApplicationNotifier(financeapplicationmodule.NewDingTalkNotifier(client))) } financeApplicationOptions = append(financeApplicationOptions, financeapplicationmodule.WithUserClient(userclient.NewGRPC(userConn)), financeapplicationmodule.WithWalletClient(walletclient.NewGRPC(walletConn)), ) handlers := router.Handlers{ Audit: auditHandler, Auth: authmodule.New(store, auth, auditHandler, cfg), AdminUser: adminusermodule.New(store, cfg, auditHandler), AgencyOpening: agencyopeningmodule.New(activityclient.NewGRPC(activityConn), auditHandler), AchievementConfig: achievementconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler), AppConfig: appconfigmodule.New(store, auditHandler), AppRegistry: appregistrymodule.New(userDB), AppUser: appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler), CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), auditHandler), CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler), CPRelation: cprelationmodule.NewWithActivity(userDB, activityclient.NewGRPC(activityConn), auditHandler), CPWeeklyRank: cpweeklyrankmodule.New(activityclient.NewGRPC(activityConn), auditHandler), CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler), Dashboard: dashboardmodule.New(store, cfg, userclient.NewGRPC(userConn), dashboardmodule.WithExternalDashboardSources(dashboardExternalSources...)), FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), FinanceApplication: financeapplicationmodule.New(store, auditHandler, financeApplicationOptions...), FinanceWithdrawal: financewithdrawalmodule.New(store, walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn), auditHandler), FullServerNotice: fullservernoticemodule.New(activityclient.NewGRPC(activityConn), auditHandler), Game: gamemanagementmodule.New( gameclient.NewGRPC(gameConn), userclient.NewGRPC(userConn), auditHandler, gamemanagementmodule.WithRobotProfileSource(robotProfileSource), gamemanagementmodule.WithRobotAppearanceServices(walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn)), ), GiftDiamond: giftdiamondmodule.New(walletDB, auditHandler), Health: healthmodule.New(store, redisClient, jobStatus), HostAgencyPolicy: hostagencypolicymodule.New(store, walletDB, auditHandler), HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler), HostSalarySettlement: hostsalarysettlementmodule.New(walletDB, userDB, walletv1.NewWalletCronServiceClient(walletConn), cfg.WalletService.RequestTimeout, auditHandler), HostWithdrawal: hostwithdrawalmodule.New(sqlDB, walletDB, userDB), InviteActivityReward: inviteactivityrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), Job: jobmodule.New(store, cfg, auditHandler), LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler), LuckyGift: luckygiftmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler), Menu: menumodule.New(store, auditHandler), Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), userDB, walletDB, store, auditHandler, paymentmodule.WithMoneyRegionSources(moneyRegionSources...), paymentmodule.WithRechargeBillSources(financeBillSources...)), PrettyID: prettyidmodule.New(userclient.NewGRPC(userConn), auditHandler), RBAC: rbacmodule.New(store, auditHandler), RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler), Report: reportmodule.New(userDB, roomClient), RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), RegionBlock: regionblockmodule.New(userDB, userclient.NewGRPC(userConn), auditHandler), Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler), RiskConfig: riskconfigmodule.New(userclient.NewGRPC(userConn), auditHandler), RoomAdmin: roomadminmodule.New(userDB, store, roomClient, robotClient, auditHandler), RoomRocket: roomrocketmodule.New(roomClient, auditHandler), RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), Search: searchmodule.New(store), SevenDayCheckIn: sevendaycheckinmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler), Team: teammodule.New(store, auditHandler), TeamSalaryPolicy: teamsalarypolicymodule.New(store, auditHandler), TeamSalarySettlement: teamSalarySettlementHandler, Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler), UserLeaderboard: userleaderboardmodule.New(walletDB, userDB, roomClient), VIPConfig: vipconfigmodule.New(walletclient.NewGRPC(walletConn), auditHandler), WeeklyStar: weeklystarmodule.New(activityclient.NewGRPC(activityConn), auditHandler), Wheel: wheelmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler), } engine := router.New(cfg, auth, handlers) server := &http.Server{ Addr: cfg.HTTPAddr, Handler: engine, ReadHeaderTimeout: 5 * time.Second, } go func() { slog.Info("service_listening", "addr", cfg.HTTPAddr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { fatalRuntime("listen_failed", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { slog.Error("shutdown_failed", "error", err.Error()) } } func fatalRuntime(msg string, err error) { slog.Error(msg, "error", err.Error()) os.Exit(1) } func dialBackendGRPC(addr string) (*grpc.ClientConn, error) { // admin-server 在广州跨区访问 Saudi 内网 CLB;gRPC 长连接如果长时间空闲, // TCP 层可能留下半开连接。客户端 keepalive 主动探活并触发重连,避免请求卡到 HTTP 超时。 return grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 30 * time.Second, Timeout: 10 * time.Second, PermitWithoutStream: true, }), ) } func connectMoneyRegionSources(ctx context.Context, configs []config.MoneyRegionSourceConfig) ([]paymentmodule.MoneyRegionSource, func(context.Context), error) { sources := []paymentmodule.MoneyRegionSource{} clients := []*mongo.Client{} cleanup := func(closeCtx context.Context) { for _, client := range clients { if client != nil { _ = client.Disconnect(closeCtx) } } } for _, sourceConfig := range configs { if !sourceConfig.Enabled { continue } connectCtx, cancel := context.WithTimeout(ctx, sourceConfig.RequestTimeout) // 财务范围页面依赖 legacy likei 区域目录;启用后启动阶段先 ping,避免运行时静默漏掉 Yumi/Aslan 区域。 client, err := mongo.Connect(options.Client().ApplyURI(sourceConfig.MongoURI).SetServerSelectionTimeout(sourceConfig.RequestTimeout)) if err != nil { cancel() cleanup(ctx) return nil, nil, err } if err := client.Ping(connectCtx, readpref.Primary()); err != nil { cancel() _ = client.Disconnect(ctx) cleanup(ctx) return nil, nil, err } cancel() clients = append(clients, client) source := paymentmodule.NewMongoMoneyRegionSource(client.Database(sourceConfig.MongoDatabase), sourceConfig) if source != nil { sources = append(sources, source) } } return sources, cleanup, nil } // connectFinanceBillSources 连接 legacy App 的账单 Mongo;启动阶段 ping 失败直接退出,避免财务明细静默漏掉 Yumi 账单。 func connectFinanceBillSources(ctx context.Context, configs []config.FinanceBillSourceConfig, regionSources []paymentmodule.MoneyRegionSource) ([]paymentmodule.RechargeBillSource, func(context.Context), error) { sources := []paymentmodule.RechargeBillSource{} clients := []*mongo.Client{} cleanup := func(closeCtx context.Context) { for _, client := range clients { if client != nil { _ = client.Disconnect(closeCtx) } } } for _, sourceConfig := range configs { if !sourceConfig.Enabled { continue } connectCtx, cancel := context.WithTimeout(ctx, sourceConfig.RequestTimeout) client, err := mongo.Connect(options.Client().ApplyURI(sourceConfig.MongoURI).SetServerSelectionTimeout(sourceConfig.RequestTimeout)) if err != nil { cancel() cleanup(ctx) return nil, nil, err } if err := client.Ping(connectCtx, readpref.Primary()); err != nil { cancel() _ = client.Disconnect(ctx) cleanup(ctx) return nil, nil, err } cancel() clients = append(clients, client) source := paymentmodule.NewMongoRechargeBillSource(client.Database(sourceConfig.MongoDatabase), sourceConfig, regionSources...) if source != nil { sources = append(sources, source) } } return sources, cleanup, nil } func dashboardRegionResolvers(sources []paymentmodule.MoneyRegionSource) []dashboardmodule.ExternalDashboardRegionResolver { resolvers := []dashboardmodule.ExternalDashboardRegionResolver{} for _, source := range sources { resolver, ok := source.(dashboardmodule.ExternalDashboardRegionResolver) if ok && resolver != nil { resolvers = append(resolvers, resolver) } } return resolvers } func connectDashboardExternalSources(ctx context.Context, configs []config.DashboardExternalSourceConfig, regionResolvers ...dashboardmodule.ExternalDashboardRegionResolver) ([]dashboardmodule.ExternalDashboardSource, func(), error) { sources := []dashboardmodule.ExternalDashboardSource{} dbs := []*sql.DB{} mongoClients := []*mongo.Client{} cleanup := func() { for _, db := range dbs { if db != nil { _ = db.Close() } } closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for _, client := range mongoClients { if client != nil { _ = client.Disconnect(closeCtx) } } } for _, sourceConfig := range configs { if !sourceConfig.Enabled { continue } switch strings.ToLower(strings.TrimSpace(sourceConfig.SourceType)) { case "aslan_http": // Aslan 大屏数据由对方 console 免鉴权接口返回;这里不建立数据库连接,避免 admin 绕过 Aslan 服务端口径。 source, err := dashboardmodule.NewAslanHTTPDashboardSource(sourceConfig, regionResolvers...) if err != nil { cleanup() return nil, nil, err } if source != nil { sources = append(sources, source) } continue case "aslan_mongo": // Aslan 当前要求 admin-server 直接读外接项目 Mongo 聚合事实;启动期先 ping, // 避免前端选择 Aslan 后才暴露连接或凭证错误。 client, err := mongo.Connect(options.Client().ApplyURI(sourceConfig.MongoURI).SetServerSelectionTimeout(sourceConfig.RequestTimeout)) if err != nil { cleanup() return nil, nil, err } pingCtx, cancel := context.WithTimeout(ctx, sourceConfig.RequestTimeout) if err := client.Ping(pingCtx, readpref.PrimaryPreferred()); err != nil { cancel() _ = client.Disconnect(ctx) cleanup() return nil, nil, err } cancel() source, err := dashboardmodule.NewAslanMongoDashboardSource(client.Database(sourceConfig.MongoDatabase), sourceConfig, regionResolvers...) if err != nil { _ = client.Disconnect(ctx) cleanup() return nil, nil, err } if source == nil { _ = client.Disconnect(ctx) continue } mongoClients = append(mongoClients, client) sources = append(sources, source) continue case "", "mysql": default: cleanup() return nil, nil, errors.New("unsupported dashboard external source type: " + sourceConfig.SourceType) } db, err := sql.Open("mysql", sourceConfig.MySQLDSN) if err != nil { cleanup() return nil, nil, err } pingCtx, cancel := context.WithTimeout(ctx, sourceConfig.RequestTimeout) // Yumi 大屏使用外接项目 dashboard-cdc-worker 的只读聚合库;启动期先 ping,避免 admin 正常启动但 Yumi 查询静默回退到错误事实域。 if err := db.PingContext(pingCtx); err != nil { cancel() _ = db.Close() cleanup() return nil, nil, err } cancel() source := dashboardmodule.NewMySQLExternalDashboardSource(db, sourceConfig, regionResolvers...) if source == nil { _ = db.Close() continue } dbs = append(dbs, db) sources = append(sources, source) } return sources, cleanup, nil } func ensureDatabase(dsn string) error { parsed, err := mysqlDriver.ParseDSN(dsn) if err != nil { return err } if parsed.DBName == "" { return nil } dbName := parsed.DBName parsed.DBName = "" db, err := sql.Open("mysql", parsed.FormatDSN()) if err != nil { return err } defer db.Close() escaped := strings.ReplaceAll(dbName, "`", "``") _, err = db.Exec("CREATE DATABASE IF NOT EXISTS `" + escaped + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") return err }