// Package config 定义 room-service 的启动配置和 YAML 加载入口。 package config import ( "fmt" "net/http" "strings" "time" "hyapp/pkg/configx" "hyapp/pkg/tencentim" ) // Config 描述 room-service 进程启动所需的最小配置。 type Config struct { // NodeID 是当前 room-service 实例标识,Redis lease owner 使用它。 NodeID string `yaml:"node_id"` // GRPCAddr 是内部 gRPC 监听地址,gateway-service 通过它访问 room-service。 GRPCAddr string `yaml:"grpc_addr"` // 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"` // TencentIM 是腾讯云 IM 群组和房间系统消息的外部集成配置。 TencentIM TencentIMConfig `yaml:"tencent_im"` // 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"` } // 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 当前只接受 fixed_interval,避免配置错误静默改变投递语义。 RetryStrategy string `yaml:"retry_strategy"` } // 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"` } // 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{ NodeID: "room-node-local", GRPCAddr: ":13001", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, WalletServiceAddr: "127.0.0.1:13004", TencentIM: TencentIMConfig{ Enabled: false, AdminIdentifier: "administrator", AdminUserSigTTL: 24 * time.Hour, Endpoint: tencentim.DefaultEndpoint, GroupType: tencentim.DefaultGroupType, RequestTimeout: 5 * time.Second, }, MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local", 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(), } } func defaultOutboxWorkerConfig() OutboxWorkerConfig { // 默认启动补偿 worker,保证同步腾讯云 IM 失败后 pending outbox 能自动重试。 return OutboxWorkerConfig{ Enabled: true, PollInterval: time.Second, BatchSize: 100, PublishTimeout: 3 * time.Second, RetryStrategy: "fixed_interval", } } // Normalize 补齐动态配置默认值,并拒绝会改变补偿语义的未知策略。 func Normalize(cfg Config) (Config, error) { // 配置归一化只处理需要跨环境保持语义的字段,其他默认值由 Default 提供。 outboxWorker, err := normalizeOutboxWorkerConfig(cfg.OutboxWorker) if err != nil { return Config{}, err } cfg.OutboxWorker = outboxWorker return cfg, nil } 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 } cfg.RetryStrategy = strings.TrimSpace(cfg.RetryStrategy) if cfg.RetryStrategy == "" { cfg.RetryStrategy = defaults.RetryStrategy } if cfg.RetryStrategy != defaults.RetryStrategy { // V1 只实现 fixed_interval;未知策略直接启动失败,避免误以为退避或死信已生效。 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) }