package config import ( "errors" "fmt" "os" "strings" "time" "hyapp-admin-server/internal/platform/logging" "gopkg.in/yaml.v3" ) type Config struct { ServiceName string `yaml:"service_name"` NodeID string `yaml:"node_id"` Environment string `yaml:"environment"` Log logging.Config `yaml:"log"` HTTPAddr string `yaml:"http_addr"` MySQLDSN string `yaml:"mysql_dsn"` UserMySQLDSN string `yaml:"user_mysql_dsn"` WalletMySQLDSN string `yaml:"wallet_mysql_dsn"` MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` Migrations MigrationConfig `yaml:"migrations"` JWTSecret string `yaml:"jwt_secret"` AccessTokenTTL time.Duration `yaml:"access_token_ttl"` RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl"` CORSAllowedOrigins []string `yaml:"cors_allowed_origins"` RefreshCookieSecure bool `yaml:"refresh_cookie_secure"` RefreshCookieSameSite string `yaml:"refresh_cookie_same_site"` Bootstrap BootstrapConfig `yaml:"bootstrap"` BootstrapPassword string `yaml:"bootstrap_password"` UserService UserServiceConfig `yaml:"user_service"` TencentCOS TencentCOSConfig `yaml:"tencent-cos"` Redis RedisConfig `yaml:"redis"` Jobs JobsConfig `yaml:"jobs"` WalletService WalletServiceConfig `yaml:"wallet_service"` RoomService RoomServiceConfig `yaml:"room_service"` ActivityService ActivityServiceConfig `yaml:"activity_service"` GameService GameServiceConfig `yaml:"game_service"` StatisticsService StatisticsServiceConfig `yaml:"statistics_service"` } type MigrationConfig struct { Enabled bool `yaml:"enabled"` Dir string `yaml:"dir"` AllowChecksumRepair bool `yaml:"allow_checksum_repair"` } type UserServiceConfig struct { Addr string `yaml:"addr"` RequestTimeout time.Duration `yaml:"request_timeout"` } type WalletServiceConfig struct { Addr string `yaml:"addr"` RequestTimeout time.Duration `yaml:"request_timeout"` } type RoomServiceConfig struct { Addr string `yaml:"addr"` RequestTimeout time.Duration `yaml:"request_timeout"` } type ActivityServiceConfig struct { Addr string `yaml:"addr"` RequestTimeout time.Duration `yaml:"request_timeout"` } type GameServiceConfig struct { Addr string `yaml:"addr"` RequestTimeout time.Duration `yaml:"request_timeout"` } type StatisticsServiceConfig struct { BaseURL string `yaml:"base_url"` RequestTimeout time.Duration `yaml:"request_timeout"` } type TencentCOSConfig struct { Enabled bool `yaml:"enabled"` SecretID string `yaml:"secret-id"` SecretKey string `yaml:"secret-key"` BucketName string `yaml:"bucket-name"` Region string `yaml:"region"` AccessURL string `yaml:"access-url"` ObjectPrefix string `yaml:"object-prefix"` } type BootstrapConfig struct { Enabled bool `yaml:"enabled"` SeedDemoData bool `yaml:"seed_demo_data"` Username string `yaml:"username"` Name string `yaml:"name"` Team string `yaml:"team"` Password string `yaml:"password"` } type RedisConfig struct { Enabled bool `yaml:"enabled"` Addr string `yaml:"addr"` Password string `yaml:"password"` DB int `yaml:"db"` KeyPrefix string `yaml:"key_prefix"` } type JobsConfig struct { Enabled bool `yaml:"enabled"` WorkerInterval time.Duration `yaml:"worker_interval"` LeaseTTL time.Duration `yaml:"lease_ttl"` MaxAttempts int `yaml:"max_attempts"` ArtifactDir string `yaml:"artifact_dir"` } func Default() Config { return Config{ ServiceName: "admin-server", NodeID: "admin-local", Environment: "local", Log: logging.DefaultConfig(), HTTPAddr: ":13100", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC", UserMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC", WalletMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC", MySQLAutoMigrate: true, Migrations: MigrationConfig{ Enabled: true, Dir: "migrations", AllowChecksumRepair: true, }, JWTSecret: "dev-shared-secret", AccessTokenTTL: 12 * time.Hour, RefreshTokenTTL: 7 * 24 * time.Hour, CORSAllowedOrigins: []string{ "http://localhost:7001", "http://127.0.0.1:7001", }, RefreshCookieSecure: false, RefreshCookieSameSite: "lax", Bootstrap: BootstrapConfig{ Enabled: true, SeedDemoData: true, Username: "admin", Name: "admin", Team: "平台运维", Password: "admin123", }, BootstrapPassword: "admin123", UserService: UserServiceConfig{ Addr: "127.0.0.1:13005", RequestTimeout: 3 * time.Second, }, WalletService: WalletServiceConfig{ Addr: "127.0.0.1:13004", RequestTimeout: 3 * time.Second, }, RoomService: RoomServiceConfig{ Addr: "127.0.0.1:13001", RequestTimeout: 3 * time.Second, }, ActivityService: ActivityServiceConfig{ Addr: "127.0.0.1:13006", RequestTimeout: 3 * time.Second, }, GameService: GameServiceConfig{ Addr: "127.0.0.1:13008", RequestTimeout: 3 * time.Second, }, StatisticsService: StatisticsServiceConfig{ BaseURL: "http://127.0.0.1:13010", RequestTimeout: 3 * time.Second, }, TencentCOS: TencentCOSConfig{ Enabled: false, ObjectPrefix: "admin", }, Redis: RedisConfig{ Enabled: true, Addr: "127.0.0.1:13379", KeyPrefix: "admin:", }, Jobs: JobsConfig{ Enabled: true, WorkerInterval: 5 * time.Second, LeaseTTL: 2 * time.Minute, MaxAttempts: 3, ArtifactDir: "storage/exports", }, } } func Load(path string) (Config, error) { cfg := Default() if path == "" { return cfg, nil } body, err := os.ReadFile(path) if err != nil { return Config{}, err } if err := yaml.Unmarshal(body, &cfg); err != nil { return Config{}, err } cfg.Normalize() if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func (cfg *Config) Normalize() { cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) if cfg.Environment == "" { cfg.Environment = "local" } if cfg.Log.Level == "" { cfg.Log.Level = "info" } if cfg.Log.Format == "" { cfg.Log.Format = "json" } if cfg.Log.MaxPayloadBytes <= 0 { cfg.Log.MaxPayloadBytes = 2048 } cfg.RefreshCookieSameSite = strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite)) if cfg.RefreshCookieSameSite == "" { cfg.RefreshCookieSameSite = "lax" } cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) cfg.NodeID = strings.TrimSpace(cfg.NodeID) if cfg.NodeID == "" { cfg.NodeID = cfg.ServiceName } if cfg.Bootstrap.Password == "" && cfg.BootstrapPassword != "" { cfg.Bootstrap.Password = cfg.BootstrapPassword } if cfg.Bootstrap.Username == "" { cfg.Bootstrap.Username = "admin" } if cfg.Bootstrap.Name == "" { cfg.Bootstrap.Name = cfg.Bootstrap.Username } if cfg.Bootstrap.Team == "" { cfg.Bootstrap.Team = "平台运维" } if cfg.Jobs.LeaseTTL <= 0 { cfg.Jobs.LeaseTTL = 2 * time.Minute } if cfg.Jobs.MaxAttempts <= 0 { cfg.Jobs.MaxAttempts = 3 } if cfg.Jobs.ArtifactDir == "" { cfg.Jobs.ArtifactDir = "storage/exports" } cfg.UserService.Addr = strings.TrimSpace(cfg.UserService.Addr) if cfg.UserService.RequestTimeout <= 0 { cfg.UserService.RequestTimeout = 3 * time.Second } cfg.WalletService.Addr = strings.TrimSpace(cfg.WalletService.Addr) if cfg.WalletService.RequestTimeout <= 0 { cfg.WalletService.RequestTimeout = 3 * time.Second } cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr) if cfg.RoomService.Addr == "" { cfg.RoomService.Addr = "127.0.0.1:13001" } if cfg.RoomService.RequestTimeout <= 0 { cfg.RoomService.RequestTimeout = 3 * time.Second } cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr) if cfg.ActivityService.RequestTimeout <= 0 { cfg.ActivityService.RequestTimeout = 3 * time.Second } cfg.GameService.Addr = strings.TrimSpace(cfg.GameService.Addr) if cfg.GameService.Addr == "" { cfg.GameService.Addr = "127.0.0.1:13008" } if cfg.GameService.RequestTimeout <= 0 { cfg.GameService.RequestTimeout = 3 * time.Second } cfg.StatisticsService.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.StatisticsService.BaseURL), "/") if cfg.StatisticsService.BaseURL == "" { cfg.StatisticsService.BaseURL = "http://127.0.0.1:13010" } if cfg.StatisticsService.RequestTimeout <= 0 { cfg.StatisticsService.RequestTimeout = 3 * time.Second } cfg.TencentCOS.SecretID = strings.TrimSpace(cfg.TencentCOS.SecretID) cfg.TencentCOS.SecretKey = strings.TrimSpace(cfg.TencentCOS.SecretKey) cfg.TencentCOS.BucketName = strings.TrimSpace(cfg.TencentCOS.BucketName) cfg.TencentCOS.Region = strings.TrimSpace(cfg.TencentCOS.Region) cfg.TencentCOS.AccessURL = strings.TrimRight(strings.TrimSpace(cfg.TencentCOS.AccessURL), "/") cfg.TencentCOS.ObjectPrefix = strings.Trim(strings.TrimSpace(cfg.TencentCOS.ObjectPrefix), "/") if cfg.TencentCOS.ObjectPrefix == "" { cfg.TencentCOS.ObjectPrefix = "admin" } } func (cfg Config) Validate() error { if !validEnvironment(cfg.Environment) { return errors.New("environment must be one of local, dev, staging, prod") } if strings.TrimSpace(cfg.ServiceName) == "" { return errors.New("service_name is required") } if strings.TrimSpace(cfg.NodeID) == "" { return errors.New("node_id is required") } if strings.TrimSpace(cfg.HTTPAddr) == "" { return errors.New("http_addr is required") } if strings.TrimSpace(cfg.MySQLDSN) == "" { return errors.New("mysql_dsn is required") } if strings.TrimSpace(cfg.UserMySQLDSN) == "" { return errors.New("user_mysql_dsn is required") } if strings.TrimSpace(cfg.WalletMySQLDSN) == "" { return errors.New("wallet_mysql_dsn is required") } if cfg.Migrations.Enabled && strings.TrimSpace(cfg.Migrations.Dir) == "" { return errors.New("migrations.dir is required when migrations are enabled") } if isLockedEnvironment(cfg.Environment) && cfg.Migrations.AllowChecksumRepair { return errors.New("migrations.allow_checksum_repair must be false outside local/dev") } if strings.TrimSpace(cfg.JWTSecret) == "" { return errors.New("jwt_secret is required") } if cfg.AccessTokenTTL <= 0 { return errors.New("access_token_ttl must be greater than 0") } if cfg.RefreshTokenTTL <= 0 { return errors.New("refresh_token_ttl must be greater than 0") } switch strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite)) { case "lax", "strict", "none": default: return errors.New("refresh_cookie_same_site must be one of lax, strict, none") } if strings.EqualFold(strings.TrimSpace(cfg.RefreshCookieSameSite), "none") && !cfg.RefreshCookieSecure { return errors.New("refresh_cookie_secure must be true when refresh_cookie_same_site is none") } if isLockedEnvironment(cfg.Environment) { if cfg.JWTSecret == "dev-shared-secret" || len(cfg.JWTSecret) < 32 { return errors.New("jwt_secret must be a non-default value with at least 32 characters outside local/dev") } if cfg.MySQLAutoMigrate { return errors.New("mysql_auto_migrate must be false outside local/dev") } if cfg.Bootstrap.Enabled && cfg.Bootstrap.Password == "admin123" { return errors.New("bootstrap.password must not use the default value outside local/dev") } } if len(cfg.CORSAllowedOrigins) == 0 { return errors.New("cors_allowed_origins must not be empty") } if cfg.Bootstrap.Enabled && strings.TrimSpace(cfg.Bootstrap.Password) == "" { return errors.New("bootstrap.password is required when bootstrap is enabled") } if strings.TrimSpace(cfg.UserService.Addr) == "" { return errors.New("user_service.addr is required") } if cfg.UserService.RequestTimeout <= 0 { return errors.New("user_service.request_timeout must be greater than 0") } if strings.TrimSpace(cfg.WalletService.Addr) == "" { return errors.New("wallet_service.addr is required") } if cfg.WalletService.RequestTimeout <= 0 { return errors.New("wallet_service.request_timeout must be greater than 0") } if strings.TrimSpace(cfg.RoomService.Addr) == "" { return errors.New("room_service.addr is required") } if cfg.RoomService.RequestTimeout <= 0 { return errors.New("room_service.request_timeout must be greater than 0") } if strings.TrimSpace(cfg.ActivityService.Addr) == "" { return errors.New("activity_service.addr is required") } if cfg.ActivityService.RequestTimeout <= 0 { return errors.New("activity_service.request_timeout must be greater than 0") } if strings.TrimSpace(cfg.GameService.Addr) == "" { return errors.New("game_service.addr is required") } if cfg.GameService.RequestTimeout <= 0 { return errors.New("game_service.request_timeout must be greater than 0") } if strings.TrimSpace(cfg.StatisticsService.BaseURL) == "" { return errors.New("statistics_service.base_url is required") } if cfg.StatisticsService.RequestTimeout <= 0 { return errors.New("statistics_service.request_timeout must be greater than 0") } if cfg.TencentCOS.Enabled { if cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" { return errors.New("tencent-cos config is incomplete") } if !strings.HasPrefix(cfg.TencentCOS.AccessURL, "http://") && !strings.HasPrefix(cfg.TencentCOS.AccessURL, "https://") { return errors.New("tencent-cos.access-url must be an absolute url") } } if cfg.Redis.Enabled && strings.TrimSpace(cfg.Redis.Addr) == "" { return errors.New("redis.addr is required when redis is enabled") } if cfg.Redis.DB < 0 { return fmt.Errorf("redis.db must be greater than or equal to 0") } if cfg.Jobs.Enabled && cfg.Jobs.WorkerInterval <= 0 { return errors.New("jobs.worker_interval must be greater than 0") } if cfg.Jobs.Enabled && cfg.Jobs.LeaseTTL <= 0 { return errors.New("jobs.lease_ttl must be greater than 0") } if cfg.Jobs.Enabled && cfg.Jobs.MaxAttempts <= 0 { return errors.New("jobs.max_attempts must be greater than 0") } if cfg.Jobs.Enabled && strings.TrimSpace(cfg.Jobs.ArtifactDir) == "" { return errors.New("jobs.artifact_dir is required when jobs are enabled") } return nil } func validEnvironment(value string) bool { switch value { case "local", "dev", "staging", "prod": return true default: return false } } func isLockedEnvironment(value string) bool { return value == "staging" || value == "prod" }