2026-05-16 08:12:42 +08:00

411 lines
14 KiB
Go

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"`
RoomMySQLDSN string `yaml:"room_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"`
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"`
}
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 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",
RoomMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=UTC",
MySQLAutoMigrate: true,
Migrations: MigrationConfig{
Enabled: true,
Dir: "migrations",
AllowChecksumRepair: true,
},
JWTSecret: "dev-shared-secret",
AccessTokenTTL: 30 * time.Minute,
RefreshTokenTTL: 7 * 24 * time.Hour,
CORSAllowedOrigins: []string{
"http://localhost:7001",
"http://127.0.0.1:7001",
},
RefreshCookieSecure: false,
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,
},
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.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.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 strings.TrimSpace(cfg.RoomMySQLDSN) == "" {
return errors.New("room_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")
}
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 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"
}