2026-06-12 19:40:21 +08:00

297 lines
10 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 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
}