// Package config 定义 room-service 的启动配置和 YAML 加载入口。 package config import ( "fmt" "net" "net/http" "strings" "time" "hyapp/pkg/configx" "hyapp/pkg/logx" "hyapp/pkg/tencentim" "hyapp/pkg/tencentrtc" ) // Config 描述 room-service 进程启动所需的最小配置。 type Config struct { // ServiceName 是日志和部署识别使用的稳定服务名。 ServiceName string `yaml:"service_name"` // Environment 是 local/dev/staging/prod 等运行环境标签。 Environment string `yaml:"environment"` // NodeID 是当前 room-service 实例标识,Redis lease owner 使用它。 NodeID string `yaml:"node_id"` // GRPCAddr 是内部 gRPC 监听地址,gateway-service 通过它访问 room-service。 GRPCAddr string `yaml:"grpc_addr"` // AdvertiseAddr 是其他 room-service 实例转发到本节点时使用的私网地址,不能写负载均衡地址。 AdvertiseAddr string `yaml:"advertise_addr"` // HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址,不承载业务流量。 HealthHTTPAddr string `yaml:"health_http_addr"` // NodeRegistryTTL 是 node_id -> advertise_addr 注册的有效期。 NodeRegistryTTL time.Duration `yaml:"node_registry_ttl"` // NodeRegistryHeartbeatInterval 是刷新节点注册的周期。 NodeRegistryHeartbeatInterval time.Duration `yaml:"node_registry_heartbeat_interval"` // OwnerForwardTimeout 是非 owner 节点转发到当前 owner 的单次 RPC 超时。 OwnerForwardTimeout time.Duration `yaml:"owner_forward_timeout"` // LeaseTTL 是 room owner 租约有效期,节点故障后必须等它过期才能接管。 LeaseTTL time.Duration `yaml:"lease_ttl"` // RankLimit 是房间本地礼物榜 top N。 RankLimit int `yaml:"rank_limit"` // SnapshotEveryN 控制按房间版本保存快照的频率。 SnapshotEveryN int64 `yaml:"snapshot_every_n"` // WalletServiceAddr 是 SendGift 同步扣费 gRPC 地址。 WalletServiceAddr string `yaml:"wallet_service_addr"` // ActivityServiceAddr 是房间事实事件投给 activity-service 的内部 gRPC 地址。 ActivityServiceAddr string `yaml:"activity_service_addr"` // TencentIM 是腾讯云 IM 群组和房间系统消息的外部集成配置。 TencentIM TencentIMConfig `yaml:"tencent_im"` // TencentRTC 是平台级封禁后移出实时音频房的外部集成配置。 TencentRTC TencentRTCConfig `yaml:"tencent_rtc"` // MySQLDSN 是 rooms、snapshot、command log 和 outbox 的持久化连接串。 MySQLDSN string `yaml:"mysql_dsn"` // MySQLMaxOpenConns 控制 MySQL 最大打开连接数。 MySQLMaxOpenConns int `yaml:"mysql_max_open_conns"` // MySQLMaxIdleConns 控制 MySQL 空闲连接数。 MySQLMaxIdleConns int `yaml:"mysql_max_idle_conns"` // MySQLAutoMigrate 控制启动时是否自动建表。 MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` // RedisAddr 是 room owner lease 目录地址。 RedisAddr string `yaml:"redis_addr"` // RedisPassword 是 Redis 鉴权密码,本地默认留空。 RedisPassword string `yaml:"redis_password"` // RedisDB 是 Redis logical DB。 RedisDB int `yaml:"redis_db"` // PresenceStaleAfter 是业务 presence 超过多久未刷新后被视为断线离房。 PresenceStaleAfter time.Duration `yaml:"presence_stale_after"` // PresenceStaleScanInterval 是本节点扫描已装载 Room Cell 的周期。 PresenceStaleScanInterval time.Duration `yaml:"presence_stale_scan_interval"` // MicPublishTimeout 是 MicUp 后等待客户端/RTC 发流确认的最长时间。 MicPublishTimeout time.Duration `yaml:"mic_publish_timeout"` // MicPublishScanInterval 是本节点扫描 pending_publish 超时麦位的周期。 MicPublishScanInterval time.Duration `yaml:"mic_publish_scan_interval"` // OutboxWorker 控制 room_outbox 到腾讯云 IM 补偿投递 worker。 OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"` // Log 控制 stdout 结构化日志格式、等级和 access body 策略。 Log logx.Config `yaml:"log"` } // OutboxWorkerConfig 控制 room_outbox pending 事件的补偿投递循环。 type OutboxWorkerConfig struct { // Enabled 为 false 时不启动补偿 worker,测试或临时止血可用。 Enabled bool `yaml:"enabled"` // PollInterval 是两轮 pending outbox 扫描之间的固定间隔。 PollInterval time.Duration `yaml:"poll_interval"` // BatchSize 是单轮最多处理的 pending outbox 数,防止全表扫描。 BatchSize int `yaml:"batch_size"` // PublishTimeout 是单条 outbox 投递腾讯云 IM 的最大耗时。 PublishTimeout time.Duration `yaml:"publish_timeout"` // RetryStrategy 当前只接受 exponential_backoff,避免配置错误静默改变投递语义。 RetryStrategy string `yaml:"retry_strategy"` // MaxRetryCount 是转入 failed 前允许的最大失败次数。 MaxRetryCount int `yaml:"max_retry_count"` // InitialBackoff 是首次失败后的等待时间。 InitialBackoff time.Duration `yaml:"initial_backoff"` // MaxBackoff 是指数退避的最大等待时间。 MaxBackoff time.Duration `yaml:"max_backoff"` } // TencentIMConfig 描述 room-service 调用腾讯云 IM REST API 的配置。 type TencentIMConfig struct { // Enabled 控制是否启用腾讯云 IM REST 桥接;关闭时只保留 Room Cell 内部提交。 Enabled bool `yaml:"enabled"` // SDKAppID 是腾讯云 IM 控制台分配的应用 ID。 SDKAppID int64 `yaml:"sdk_app_id"` // SecretKey 用于生成 App 管理员 UserSig,不能写入公开配置。 SecretKey string `yaml:"secret_key"` // AdminIdentifier 是腾讯云 IM App 管理员账号。 AdminIdentifier string `yaml:"admin_identifier"` // AdminUserSigTTL 是 REST API 管理员票据有效期。 AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"` // Endpoint 是腾讯云 IM REST API 区域域名。 Endpoint string `yaml:"endpoint"` // GroupType 是语音房对应的腾讯云 IM 群组类型。 GroupType string `yaml:"group_type"` // RequestTimeout 是单次 REST API 调用超时。 RequestTimeout time.Duration `yaml:"request_timeout"` } // TencentRTCConfig 描述 room-service 调用腾讯云 TRTC 房间管理 API 的配置。 type TencentRTCConfig struct { // Enabled 控制是否启用服务端 RTC 踢人;关闭时仍会提交 Room Cell 驱逐。 Enabled bool `yaml:"enabled"` // SDKAppID 是 TRTC 控制台分配的应用 ID,必须和 gateway 签发 RTC UserSig 的应用一致。 SDKAppID int64 `yaml:"sdk_app_id"` // SecretID/SecretKey 是腾讯云 API 3.0 访问密钥,只能写在线上密钥系统或私有配置中。 SecretID string `yaml:"secret_id"` SecretKey string `yaml:"secret_key"` // Region 是腾讯云 API 公共参数;TRTC 房间管理接口当前使用 ap-guangzhou/ap-beijing。 Region string `yaml:"region"` // Endpoint 是腾讯云 API 域名,默认 trtc.tencentcloudapi.com。 Endpoint string `yaml:"endpoint"` // RequestTimeout 是单次公网调用超时。 RequestTimeout time.Duration `yaml:"request_timeout"` } // RESTConfig 把 YAML 配置转换成 pkg/tencentrtc 的 REST 客户端配置。 func (c TencentRTCConfig) RESTConfig() tencentrtc.RESTConfig { timeout := c.RequestTimeout if timeout <= 0 { timeout = 5 * time.Second } return tencentrtc.RESTConfig{ Enabled: c.Enabled, SDKAppID: c.SDKAppID, SecretID: c.SecretID, SecretKey: c.SecretKey, Region: c.Region, Endpoint: c.Endpoint, HTTPClient: &http.Client{Timeout: timeout}, } } // RESTConfig 把 YAML 配置转换成 pkg/tencentim 的 REST 客户端配置。 func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig { timeout := c.RequestTimeout if timeout <= 0 { // 腾讯云 REST 后台超时约 3 秒,本地客户端默认给 5 秒预算。 timeout = 5 * time.Second } return tencentim.RESTConfig{ SDKAppID: c.SDKAppID, SecretKey: c.SecretKey, AdminIdentifier: c.AdminIdentifier, AdminUserSigTTL: c.AdminUserSigTTL, Endpoint: c.Endpoint, GroupType: c.GroupType, HTTPClient: &http.Client{Timeout: timeout}, } } // Default 返回没有外部私有密钥时可启动的基础配置。 func Default() Config { // 默认端口遵守项目 13xxx 约束,MySQL/Redis 指向本地 Docker 暴露端口。 return Config{ ServiceName: "room-service", Environment: "local", NodeID: "room-node-local", GRPCAddr: ":13001", AdvertiseAddr: "127.0.0.1:13001", HealthHTTPAddr: ":13101", NodeRegistryTTL: 30 * time.Second, OwnerForwardTimeout: 2 * time.Second, LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 10, WalletServiceAddr: "127.0.0.1:13004", ActivityServiceAddr: "127.0.0.1:13006", TencentIM: TencentIMConfig{ Enabled: false, AdminIdentifier: "administrator", AdminUserSigTTL: 24 * time.Hour, Endpoint: tencentim.DefaultEndpoint, GroupType: tencentim.DefaultGroupType, RequestTimeout: 5 * time.Second, }, TencentRTC: TencentRTCConfig{ Enabled: false, Region: tencentrtc.DefaultRESTRegion, Endpoint: tencentrtc.DefaultRESTEndpoint, RequestTimeout: 5 * time.Second, }, MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=UTC", MySQLMaxOpenConns: 20, MySQLMaxIdleConns: 10, MySQLAutoMigrate: true, RedisAddr: "127.0.0.1:13379", RedisDB: 0, PresenceStaleAfter: 2 * time.Minute, PresenceStaleScanInterval: 30 * time.Second, MicPublishTimeout: 15 * time.Second, MicPublishScanInterval: time.Second, OutboxWorker: defaultOutboxWorkerConfig(), Log: logx.Config{ Level: "info", Format: "json", MaxPayloadBytes: 2048, }, } } func defaultOutboxWorkerConfig() OutboxWorkerConfig { // 默认启动补偿 worker,使用退避和死信避免外部故障刷屏。 return OutboxWorkerConfig{ Enabled: true, PollInterval: time.Second, BatchSize: 100, PublishTimeout: 3 * time.Second, RetryStrategy: "exponential_backoff", MaxRetryCount: 10, InitialBackoff: 5 * time.Second, MaxBackoff: 5 * time.Minute, } } // Normalize 补齐动态配置默认值,并拒绝会改变补偿语义的未知策略。 func Normalize(cfg Config) (Config, error) { // 配置归一化只处理需要跨环境保持语义的字段,其他默认值由 Default 提供。 cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) if cfg.ServiceName == "" { cfg.ServiceName = "room-service" } cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) if cfg.Environment == "" { cfg.Environment = "local" } cfg.NodeID = strings.TrimSpace(cfg.NodeID) if cfg.NodeID == "" { cfg.NodeID = cfg.ServiceName } cfg.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr) if cfg.GRPCAddr == "" { cfg.GRPCAddr = ":13001" } cfg.AdvertiseAddr = strings.TrimSpace(cfg.AdvertiseAddr) if cfg.AdvertiseAddr == "" { cfg.AdvertiseAddr = defaultAdvertiseAddr(cfg.GRPCAddr) } cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr) if cfg.HealthHTTPAddr == "" { cfg.HealthHTTPAddr = ":13101" } if cfg.NodeRegistryTTL <= 0 { cfg.NodeRegistryTTL = 30 * time.Second } if cfg.NodeRegistryHeartbeatInterval <= 0 { cfg.NodeRegistryHeartbeatInterval = cfg.NodeRegistryTTL / 3 } if cfg.NodeRegistryHeartbeatInterval <= 0 { cfg.NodeRegistryHeartbeatInterval = 10 * time.Second } if cfg.NodeRegistryHeartbeatInterval >= cfg.NodeRegistryTTL { cfg.NodeRegistryHeartbeatInterval = cfg.NodeRegistryTTL / 3 } if cfg.OwnerForwardTimeout <= 0 { cfg.OwnerForwardTimeout = 2 * time.Second } cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr) if cfg.WalletServiceAddr == "" { cfg.WalletServiceAddr = "127.0.0.1:13004" } cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr) if cfg.ActivityServiceAddr == "" { cfg.ActivityServiceAddr = "127.0.0.1:13006" } cfg.TencentRTC.SecretID = strings.TrimSpace(cfg.TencentRTC.SecretID) cfg.TencentRTC.SecretKey = strings.TrimSpace(cfg.TencentRTC.SecretKey) cfg.TencentRTC.Region = strings.TrimSpace(cfg.TencentRTC.Region) if cfg.TencentRTC.Region == "" { cfg.TencentRTC.Region = tencentrtc.DefaultRESTRegion } cfg.TencentRTC.Endpoint = strings.TrimSpace(cfg.TencentRTC.Endpoint) if cfg.TencentRTC.Endpoint == "" { cfg.TencentRTC.Endpoint = tencentrtc.DefaultRESTEndpoint } if cfg.TencentRTC.RequestTimeout <= 0 { cfg.TencentRTC.RequestTimeout = 5 * time.Second } outboxWorker, err := normalizeOutboxWorkerConfig(cfg.OutboxWorker) if err != nil { return Config{}, err } cfg.OutboxWorker = outboxWorker return cfg, nil } func defaultAdvertiseAddr(grpcAddr string) string { host, port, err := net.SplitHostPort(grpcAddr) if err != nil { // 非 host:port 形态保持原值,由启动拨测暴露错误。 return grpcAddr } if host == "" || host == "0.0.0.0" || host == "::" { // 本地默认值只能服务单机开发;多机部署必须显式配置真实私网 IP 或主机名。 host = "127.0.0.1" } return net.JoinHostPort(host, port) } func normalizeOutboxWorkerConfig(cfg OutboxWorkerConfig) (OutboxWorkerConfig, error) { // Outbox worker 的所有时间和批量参数都必须有安全下限,避免配置缺省导致 busy loop。 defaults := defaultOutboxWorkerConfig() if cfg.PollInterval <= 0 { cfg.PollInterval = defaults.PollInterval } if cfg.BatchSize <= 0 { cfg.BatchSize = defaults.BatchSize } if cfg.PublishTimeout <= 0 { cfg.PublishTimeout = defaults.PublishTimeout } if cfg.MaxRetryCount <= 0 { cfg.MaxRetryCount = defaults.MaxRetryCount } if cfg.InitialBackoff <= 0 { cfg.InitialBackoff = defaults.InitialBackoff } if cfg.MaxBackoff <= 0 { cfg.MaxBackoff = defaults.MaxBackoff } if cfg.MaxBackoff < cfg.InitialBackoff { cfg.MaxBackoff = cfg.InitialBackoff } cfg.RetryStrategy = strings.TrimSpace(cfg.RetryStrategy) if cfg.RetryStrategy == "" { cfg.RetryStrategy = defaults.RetryStrategy } if cfg.RetryStrategy != defaults.RetryStrategy { // 当前只实现指数退避;未知策略直接启动失败,避免误以为退避或死信已生效。 return OutboxWorkerConfig{}, fmt.Errorf("unsupported outbox_worker retry_strategy %q", cfg.RetryStrategy) } return cfg, nil } // Load 从独立 config.yaml 读取 room-service 配置。 func Load(path string) (Config, error) { cfg := Default() if path == "" { // 空路径代表显式使用默认配置,便于单元测试和最小启动。 return Normalize(cfg) } if err := configx.LoadYAML(path, &cfg); err != nil { // YAML 解析失败返回空 Config,避免调用方误用部分默认值启动。 return Config{}, err } return Normalize(cfg) }