package config import ( "errors" "fmt" "net/http" "strings" "time" "hyapp/pkg/configx" "hyapp/pkg/logx" "hyapp/pkg/tencentim" ) // Config 描述 game-service 启动所需的最小依赖。 type Config struct { ServiceName string `yaml:"service_name"` NodeID string `yaml:"node_id"` Environment string `yaml:"environment"` GRPCAddr string `yaml:"grpc_addr"` // HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址,不承载业务流量。 HealthHTTPAddr string `yaml:"health_http_addr"` MySQLDSN string `yaml:"mysql_dsn"` WalletServiceAddr string `yaml:"wallet_service_addr"` UserServiceAddr string `yaml:"user_service_addr"` ActivityServiceAddr string `yaml:"activity_service_addr"` RobotServiceAddr string `yaml:"robot_service_addr"` LaunchSessionTTL time.Duration `yaml:"launch_session_ttl"` // TencentIM 是房内猜拳向房间群投递 PK 事件的腾讯 IM REST 配置;缺配置时必须显式关闭,不能靠环境变量隐式漂移。 TencentIM TencentIMConfig `yaml:"tencent_im"` RocketMQ RocketMQConfig `yaml:"rocketmq"` OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"` Log logx.Config `yaml:"log"` } // TencentIMConfig 描述 game-service 发房内 RPS 群自定义消息需要的服务端 IM 配置。 type TencentIMConfig struct { // Enabled 控制房内 RPS 是否向腾讯 IM 房间群投递事件;开启后缺少密钥或管理员账号应启动失败。 Enabled bool `yaml:"enabled"` // SDKAppID 是腾讯云 IM 应用 ID,必须和 gateway 签发 UserSig 使用同一个应用。 SDKAppID int64 `yaml:"sdk_app_id"` // SecretKey 只用于服务端签 admin UserSig,不能出现在响应或业务日志里。 SecretKey string `yaml:"secret_key"` // AdminIdentifier 是服务端 REST 发房间 PK 消息的管理员账号。 AdminIdentifier string `yaml:"admin_identifier"` // AdminUserSigTTL 控制 REST 调用签名有效期,默认 24h。 AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"` // Endpoint 是腾讯 IM REST 地域入口,例如 adminapisgp.im.qcloud.com。 Endpoint string `yaml:"endpoint"` // GroupType 是房间群类型;房内 RPS 复用房间群,默认 ChatRoom。 GroupType string `yaml:"group_type"` // RequestTimeout 是单次腾讯 IM REST 调用预算,避免创建 PK 时无限等待外部服务。 RequestTimeout time.Duration `yaml:"request_timeout"` } // RESTConfig 收敛为共享腾讯 IM client 配置,调用方不用重复处理默认超时和群类型。 func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig { timeout := c.RequestTimeout if timeout <= 0 { timeout = 5 * time.Second } ttl := c.AdminUserSigTTL if ttl <= 0 { ttl = 24 * time.Hour } return tencentim.RESTConfig{ SDKAppID: c.SDKAppID, SecretKey: c.SecretKey, AdminIdentifier: c.AdminIdentifier, AdminUserSigTTL: ttl, Endpoint: c.Endpoint, GroupType: c.GroupType, HTTPClient: &http.Client{Timeout: timeout}, } } type RocketMQConfig struct { Enabled bool `yaml:"enabled"` NameServers []string `yaml:"name_servers"` NameServerDomain string `yaml:"name_server_domain"` AccessKey string `yaml:"access_key"` SecretKey string `yaml:"secret_key"` SecurityToken string `yaml:"security_token"` Namespace string `yaml:"namespace"` VIPChannel bool `yaml:"vip_channel"` SendTimeout time.Duration `yaml:"send_timeout"` Retry int `yaml:"retry"` GameOutbox GameOutboxConfig `yaml:"game_outbox"` } type GameOutboxConfig struct { Enabled bool `yaml:"enabled"` Topic string `yaml:"topic"` ProducerGroup string `yaml:"producer_group"` } type OutboxWorkerConfig struct { Enabled bool `yaml:"enabled"` PollInterval time.Duration `yaml:"poll_interval"` BatchSize int `yaml:"batch_size"` PublishTimeout time.Duration `yaml:"publish_timeout"` } // Default 返回本地开发默认配置。 func Default() Config { return Config{ ServiceName: "game-service", NodeID: "game-local", Environment: "local", GRPCAddr: ":13008", HealthHTTPAddr: ":13108", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_game?parseTime=true&charset=utf8mb4&loc=UTC&multiStatements=true", WalletServiceAddr: "127.0.0.1:13004", UserServiceAddr: "127.0.0.1:13005", ActivityServiceAddr: "127.0.0.1:13006", RobotServiceAddr: "127.0.0.1:13011", LaunchSessionTTL: 15 * time.Minute, TencentIM: TencentIMConfig{ AdminIdentifier: "administrator", AdminUserSigTTL: 24 * time.Hour, Endpoint: tencentim.DefaultEndpoint, GroupType: tencentim.DefaultGroupType, RequestTimeout: 5 * time.Second, }, RocketMQ: defaultRocketMQConfig(), OutboxWorker: OutboxWorkerConfig{ Enabled: false, PollInterval: time.Second, BatchSize: 100, PublishTimeout: 3 * time.Second, }, Log: logx.Config{ Level: "info", Format: "json", MaxPayloadBytes: 2048, }, } } // Load 从 YAML 文件读取配置,并填充默认值。 func Load(path string) (Config, error) { cfg := Default() if path != "" { if err := configx.LoadYAML(path, &cfg); err != nil { return Config{}, err } } if err := cfg.Normalize(); err != nil { return Config{}, err } return cfg, nil } // Normalize 在启动时收敛配置,避免运行期才暴露关键依赖缺失。 func (cfg *Config) Normalize() error { cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) if cfg.ServiceName == "" { cfg.ServiceName = "game-service" } cfg.NodeID = strings.TrimSpace(cfg.NodeID) if cfg.NodeID == "" { cfg.NodeID = cfg.ServiceName } cfg.Environment = strings.TrimSpace(cfg.Environment) if cfg.Environment == "" { cfg.Environment = "local" } cfg.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr) if cfg.GRPCAddr == "" { cfg.GRPCAddr = ":13008" } cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr) if cfg.HealthHTTPAddr == "" { cfg.HealthHTTPAddr = ":13108" } cfg.MySQLDSN = strings.TrimSpace(cfg.MySQLDSN) if cfg.MySQLDSN == "" { return fmt.Errorf("mysql_dsn is required") } cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr) if cfg.WalletServiceAddr == "" { return fmt.Errorf("wallet_service_addr is required") } cfg.UserServiceAddr = strings.TrimSpace(cfg.UserServiceAddr) if cfg.UserServiceAddr == "" { return fmt.Errorf("user_service_addr is required") } cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr) if cfg.ActivityServiceAddr == "" { return fmt.Errorf("activity_service_addr is required") } cfg.RobotServiceAddr = strings.TrimSpace(cfg.RobotServiceAddr) if cfg.RobotServiceAddr == "" { return fmt.Errorf("robot_service_addr is required") } if cfg.LaunchSessionTTL <= 0 { cfg.LaunchSessionTTL = 15 * time.Minute } cfg.normalizeTencentIM() if cfg.TencentIM.Enabled && (cfg.TencentIM.SDKAppID <= 0 || cfg.TencentIM.SecretKey == "" || cfg.TencentIM.AdminIdentifier == "") { return errors.New("tencent_im enabled requires sdk_app_id, secret_key and admin_identifier") } cfg.RocketMQ = normalizeRocketMQConfig(cfg.RocketMQ) if cfg.OutboxWorker.PollInterval <= 0 { cfg.OutboxWorker.PollInterval = time.Second } if cfg.OutboxWorker.BatchSize <= 0 { cfg.OutboxWorker.BatchSize = 100 } if cfg.OutboxWorker.PublishTimeout <= 0 { cfg.OutboxWorker.PublishTimeout = 3 * time.Second } if cfg.OutboxWorker.Enabled && !cfg.RocketMQ.GameOutbox.Enabled { return errors.New("outbox_worker requires rocketmq.game_outbox.enabled") } if cfg.RocketMQ.Enabled { if len(cfg.RocketMQ.NameServers) == 0 && cfg.RocketMQ.NameServerDomain == "" { return errors.New("rocketmq name_servers or name_server_domain is required") } if (cfg.RocketMQ.AccessKey == "") != (cfg.RocketMQ.SecretKey == "") { return errors.New("rocketmq access_key and secret_key must be configured together") } } return nil } func (cfg *Config) normalizeTencentIM() { cfg.TencentIM.SecretKey = strings.TrimSpace(cfg.TencentIM.SecretKey) cfg.TencentIM.AdminIdentifier = strings.TrimSpace(cfg.TencentIM.AdminIdentifier) if cfg.TencentIM.AdminIdentifier == "" { cfg.TencentIM.AdminIdentifier = "administrator" } cfg.TencentIM.Endpoint = strings.TrimSpace(cfg.TencentIM.Endpoint) if cfg.TencentIM.Endpoint == "" { cfg.TencentIM.Endpoint = tencentim.DefaultEndpoint } cfg.TencentIM.GroupType = strings.TrimSpace(cfg.TencentIM.GroupType) if cfg.TencentIM.GroupType == "" { cfg.TencentIM.GroupType = tencentim.DefaultGroupType } if cfg.TencentIM.AdminUserSigTTL <= 0 { cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour } if cfg.TencentIM.RequestTimeout <= 0 { cfg.TencentIM.RequestTimeout = 5 * time.Second } } func defaultRocketMQConfig() RocketMQConfig { return RocketMQConfig{ Enabled: false, SendTimeout: 3 * time.Second, Retry: 2, GameOutbox: GameOutboxConfig{ Enabled: false, Topic: "hyapp_game_outbox", ProducerGroup: "hyapp-game-outbox-producer", }, } } func normalizeRocketMQConfig(cfg RocketMQConfig) RocketMQConfig { defaults := defaultRocketMQConfig() cfg.NameServers = normalizeStringSlice(cfg.NameServers) cfg.NameServerDomain = strings.TrimSpace(cfg.NameServerDomain) cfg.AccessKey = strings.TrimSpace(cfg.AccessKey) cfg.SecretKey = strings.TrimSpace(cfg.SecretKey) cfg.SecurityToken = strings.TrimSpace(cfg.SecurityToken) cfg.Namespace = strings.TrimSpace(cfg.Namespace) if cfg.SendTimeout <= 0 { cfg.SendTimeout = defaults.SendTimeout } if cfg.Retry <= 0 { cfg.Retry = defaults.Retry } if cfg.GameOutbox.Topic = strings.TrimSpace(cfg.GameOutbox.Topic); cfg.GameOutbox.Topic == "" { cfg.GameOutbox.Topic = defaults.GameOutbox.Topic } if cfg.GameOutbox.ProducerGroup = strings.TrimSpace(cfg.GameOutbox.ProducerGroup); cfg.GameOutbox.ProducerGroup == "" { cfg.GameOutbox.ProducerGroup = defaults.GameOutbox.ProducerGroup } if cfg.GameOutbox.Enabled { cfg.Enabled = true } return cfg } func normalizeStringSlice(values []string) []string { normalized := make([]string, 0, len(values)) for _, value := range values { if value = strings.TrimSpace(value); value != "" { normalized = append(normalized, value) } } return normalized }