package config import ( "errors" "net/http" "strings" "time" "hyapp/pkg/configx" "hyapp/pkg/logx" "hyapp/pkg/tencentim" ) // Config 描述 notice-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 是 notice-service 自己的投递位点和死信状态库。 MySQLDSN string `yaml:"mysql_dsn"` // WalletDatabase 仅保留给历史离线修复工具使用;运行态 notice 不再直扫 wallet_outbox。 WalletDatabase string `yaml:"wallet_database"` // RoomDatabase 仅保留给历史离线修复工具使用;运行态 notice 不再直扫 room_outbox。 RoomDatabase string `yaml:"room_database"` // TencentIM 是私有 IM 投递通道配置;本地默认关闭,避免误发真实消息。 TencentIM TencentIMConfig `yaml:"tencent_im"` // WalletNoticeWorker 消费 wallet_outbox 中的钱包用户私有通知事件。 WalletNoticeWorker WalletNoticeWorkerConfig `yaml:"wallet_notice_worker"` // RoomNoticeWorker 消费 room_outbox 中需要给单个用户发送的房间通知。 RoomNoticeWorker WalletNoticeWorkerConfig `yaml:"room_notice_worker"` // CPNoticeWorker 控制 CP user_outbox 事实转腾讯 IM 的投递锁和重试策略。 CPNoticeWorker WalletNoticeWorkerConfig `yaml:"cp_notice_worker"` // RocketMQ 消费 owner service 发布的 outbox fanout topic。 RocketMQ RocketMQConfig `yaml:"rocketmq"` MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` Log logx.Config `yaml:"log"` } // TencentIMConfig 描述服务端调用腾讯云 IM REST API 的配置。 type TencentIMConfig struct { Enabled bool `yaml:"enabled"` SDKAppID int64 `yaml:"sdk_app_id"` SecretKey string `yaml:"secret_key"` AdminIdentifier string `yaml:"admin_identifier"` AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"` Endpoint string `yaml:"endpoint"` RequestTimeout time.Duration `yaml:"request_timeout"` GroupIDPrefix string `yaml:"group_id_prefix"` } // RESTConfig converts YAML config into shared Tencent IM REST config. func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig { timeout := c.RequestTimeout if timeout <= 0 { timeout = 5 * time.Second } return tencentim.RESTConfig{ SDKAppID: c.SDKAppID, SecretKey: c.SecretKey, AdminIdentifier: c.AdminIdentifier, AdminUserSigTTL: c.AdminUserSigTTL, Endpoint: c.Endpoint, HTTPClient: &http.Client{Timeout: timeout}, } } // WalletNoticeWorkerConfig 控制钱包私有通知投递 worker。 type WalletNoticeWorkerConfig struct { Enabled bool `yaml:"enabled"` PollInterval time.Duration `yaml:"poll_interval"` BatchSize int `yaml:"batch_size"` LockTTL time.Duration `yaml:"lock_ttl"` PublishTimeout time.Duration `yaml:"publish_timeout"` MaxRetryCount int `yaml:"max_retry_count"` InitialBackoff time.Duration `yaml:"initial_backoff"` MaxBackoff time.Duration `yaml:"max_backoff"` } // RocketMQConfig 描述 notice-service 消费 owner outbox 事实的 MQ 连接。 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"` WalletOutbox RoomOutboxMQConfig `yaml:"wallet_outbox"` RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"` UserOutbox RoomOutboxMQConfig `yaml:"user_outbox"` } // RoomOutboxMQConfig 控制 notice 对 room_outbox topic 的消费位点。 type RoomOutboxMQConfig struct { Enabled bool `yaml:"enabled"` Topic string `yaml:"topic"` ConsumerGroup string `yaml:"consumer_group"` ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"` } // Default 返回本地开发默认配置。 func Default() Config { return Config{ ServiceName: "notice-service", NodeID: "notice-local", Environment: "local", GRPCAddr: ":13009", HealthHTTPAddr: ":13109", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC", WalletDatabase: "hyapp_wallet", RoomDatabase: "hyapp_room", MySQLAutoMigrate: false, TencentIM: TencentIMConfig{ Enabled: false, AdminIdentifier: "administrator", AdminUserSigTTL: 24 * time.Hour, Endpoint: tencentim.DefaultEndpoint, RequestTimeout: 5 * time.Second, }, WalletNoticeWorker: WalletNoticeWorkerConfig{ Enabled: false, PollInterval: time.Second, BatchSize: 100, LockTTL: 30 * time.Second, PublishTimeout: 3 * time.Second, MaxRetryCount: 10, InitialBackoff: 5 * time.Second, MaxBackoff: 5 * time.Minute, }, RoomNoticeWorker: WalletNoticeWorkerConfig{ Enabled: false, PollInterval: time.Second, BatchSize: 100, LockTTL: 30 * time.Second, PublishTimeout: 3 * time.Second, MaxRetryCount: 10, InitialBackoff: 5 * time.Second, MaxBackoff: 5 * time.Minute, }, CPNoticeWorker: WalletNoticeWorkerConfig{ Enabled: false, PollInterval: time.Second, BatchSize: 100, LockTTL: 30 * time.Second, PublishTimeout: 3 * time.Second, MaxRetryCount: 10, InitialBackoff: 5 * time.Second, MaxBackoff: 5 * time.Minute, }, RocketMQ: defaultRocketMQConfig(), Log: logx.Config{ Level: "info", Format: "json", MaxPayloadBytes: 2048, }, } } func defaultRocketMQConfig() RocketMQConfig { return RocketMQConfig{ Enabled: false, WalletOutbox: RoomOutboxMQConfig{ Enabled: false, Topic: "hyapp_wallet_outbox", ConsumerGroup: "hyapp-notice-wallet-outbox", ConsumerMaxReconsumeTimes: 16, }, RoomOutbox: RoomOutboxMQConfig{ Enabled: false, Topic: "hyapp_room_outbox", ConsumerGroup: "hyapp-notice-room-outbox", ConsumerMaxReconsumeTimes: 16, }, UserOutbox: RoomOutboxMQConfig{ Enabled: false, Topic: "hyapp_user_outbox", ConsumerGroup: "hyapp-notice-cp-user-outbox", ConsumerMaxReconsumeTimes: 16, }, } } // Load 从独立 config.yaml 读取 notice-service 配置。 func Load(path string) (Config, error) { cfg := Default() if path == "" { return cfg, nil } if err := configx.LoadYAML(path, &cfg); err != nil { return Config{}, err } cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) if cfg.ServiceName == "" { cfg.ServiceName = "notice-service" } cfg.NodeID = strings.TrimSpace(cfg.NodeID) if cfg.NodeID == "" { cfg.NodeID = cfg.ServiceName } cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) if cfg.Environment == "" { cfg.Environment = "local" } cfg.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr) if cfg.GRPCAddr == "" { cfg.GRPCAddr = ":13009" } cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr) if cfg.HealthHTTPAddr == "" { cfg.HealthHTTPAddr = ":13109" } cfg.WalletDatabase = strings.TrimSpace(cfg.WalletDatabase) if cfg.WalletDatabase == "" { cfg.WalletDatabase = "hyapp_wallet" } cfg.RoomDatabase = strings.TrimSpace(cfg.RoomDatabase) if cfg.RoomDatabase == "" { cfg.RoomDatabase = "hyapp_room" } cfg.WalletNoticeWorker = normalizeWalletNoticeWorker(cfg.WalletNoticeWorker) cfg.RoomNoticeWorker = normalizeWalletNoticeWorker(cfg.RoomNoticeWorker) cfg.CPNoticeWorker = normalizeWalletNoticeWorker(cfg.CPNoticeWorker) rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ) if err != nil { return Config{}, err } cfg.RocketMQ = rocketMQ if cfg.WalletNoticeWorker.Enabled { return Config{}, errors.New("wallet_notice_worker direct wallet_outbox polling is disabled; enable rocketmq.wallet_outbox") } if cfg.RoomNoticeWorker.Enabled { return Config{}, errors.New("room_notice_worker direct room_outbox polling is disabled; enable rocketmq.room_outbox") } if cfg.CPNoticeWorker.Enabled { return Config{}, errors.New("cp_notice_worker direct user_outbox polling is disabled; enable rocketmq.user_outbox") } if strings.TrimSpace(cfg.TencentIM.AdminIdentifier) == "" { cfg.TencentIM.AdminIdentifier = "administrator" } if cfg.TencentIM.AdminUserSigTTL <= 0 { cfg.TencentIM.AdminUserSigTTL = 24 * time.Hour } if strings.TrimSpace(cfg.TencentIM.Endpoint) == "" { cfg.TencentIM.Endpoint = tencentim.DefaultEndpoint } cfg.TencentIM.GroupIDPrefix = strings.TrimSpace(cfg.TencentIM.GroupIDPrefix) if cfg.TencentIM.RequestTimeout <= 0 { cfg.TencentIM.RequestTimeout = 5 * time.Second } return cfg, nil } func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) { 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.RoomOutbox.Topic = strings.TrimSpace(cfg.RoomOutbox.Topic); cfg.RoomOutbox.Topic == "" { cfg.RoomOutbox.Topic = defaults.RoomOutbox.Topic } if cfg.RoomOutbox.ConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.ConsumerGroup); cfg.RoomOutbox.ConsumerGroup == "" { cfg.RoomOutbox.ConsumerGroup = defaults.RoomOutbox.ConsumerGroup } if cfg.RoomOutbox.ConsumerMaxReconsumeTimes <= 0 { cfg.RoomOutbox.ConsumerMaxReconsumeTimes = defaults.RoomOutbox.ConsumerMaxReconsumeTimes } if cfg.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" { cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic } if cfg.WalletOutbox.ConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.ConsumerGroup); cfg.WalletOutbox.ConsumerGroup == "" { cfg.WalletOutbox.ConsumerGroup = defaults.WalletOutbox.ConsumerGroup } if cfg.WalletOutbox.ConsumerMaxReconsumeTimes <= 0 { cfg.WalletOutbox.ConsumerMaxReconsumeTimes = defaults.WalletOutbox.ConsumerMaxReconsumeTimes } if cfg.UserOutbox.Topic = strings.TrimSpace(cfg.UserOutbox.Topic); cfg.UserOutbox.Topic == "" { cfg.UserOutbox.Topic = defaults.UserOutbox.Topic } if cfg.UserOutbox.ConsumerGroup = strings.TrimSpace(cfg.UserOutbox.ConsumerGroup); cfg.UserOutbox.ConsumerGroup == "" { cfg.UserOutbox.ConsumerGroup = defaults.UserOutbox.ConsumerGroup } if cfg.UserOutbox.ConsumerMaxReconsumeTimes <= 0 { cfg.UserOutbox.ConsumerMaxReconsumeTimes = defaults.UserOutbox.ConsumerMaxReconsumeTimes } if cfg.RoomOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled { cfg.Enabled = true } if cfg.Enabled { if len(cfg.NameServers) == 0 && cfg.NameServerDomain == "" { return RocketMQConfig{}, errors.New("rocketmq name_servers or name_server_domain is required") } if (cfg.AccessKey == "") != (cfg.SecretKey == "") { return RocketMQConfig{}, errors.New("rocketmq access_key and secret_key must be configured together") } } return cfg, nil } func normalizeStringSlice(input []string) []string { out := make([]string, 0, len(input)) for _, value := range input { if value = strings.TrimSpace(value); value != "" { out = append(out, value) } } return out } func normalizeWalletNoticeWorker(cfg WalletNoticeWorkerConfig) WalletNoticeWorkerConfig { def := Default().WalletNoticeWorker if cfg.PollInterval <= 0 { cfg.PollInterval = def.PollInterval } if cfg.BatchSize <= 0 { cfg.BatchSize = def.BatchSize } if cfg.LockTTL <= 0 { cfg.LockTTL = def.LockTTL } if cfg.PublishTimeout <= 0 { cfg.PublishTimeout = def.PublishTimeout } if cfg.MaxRetryCount <= 0 { cfg.MaxRetryCount = def.MaxRetryCount } if cfg.InitialBackoff <= 0 { cfg.InitialBackoff = def.InitialBackoff } if cfg.MaxBackoff <= 0 { cfg.MaxBackoff = def.MaxBackoff } return cfg }