// Package config 定义 user-service 的启动配置和 YAML 加载入口。 package config import ( "errors" "net/http" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/configx" "hyapp/pkg/logx" "hyapp/pkg/tencentim" ) // Config 描述 user-service 启动配置。 type Config struct { // ServiceName 是进程服务名,当前主要用于配置可读性。 ServiceName string `yaml:"service_name"` // NodeID 是当前服务节点标识,预留给日志和后续健康响应使用。 NodeID string `yaml:"node_id"` // Environment 是日志和部署策略使用的环境标签。 Environment string `yaml:"environment"` // GRPCAddr 是 user-service 内部 gRPC 监听地址。 GRPCAddr string `yaml:"grpc_addr"` // HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址,不承载业务流量。 HealthHTTPAddr string `yaml:"health_http_addr"` // MySQLDSN 是用户主数据、认证身份、session 和短号事务的必需存储连接串。 MySQLDSN string `yaml:"mysql_dsn"` // MySQLAutoMigrate 保留给显式本地迁移;线上 schema 由发布流程或 initdb 管理。 MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` // IDGenerator 控制 user_id 发号节点。 IDGenerator IDGeneratorConfig `yaml:"id_generator"` // DisplayUserID 控制短号分配重试和默认短号修改冷却期。 DisplayUserID DisplayUserIDConfig `yaml:"display_user_id"` // ThirdParty 控制本地三方登录 provider allowlist。 ThirdParty ThirdPartyConfig `yaml:"third_party"` // InviteRechargeWorker 控制邀请有效人数的 wallet recharge outbox 消费。 InviteRechargeWorker InviteRechargeWorkerConfig `yaml:"invite_recharge_worker"` // MicTimeWorker 控制 room-service 麦位事件消费和用户麦上时长聚合。 MicTimeWorker MicTimeWorkerConfig `yaml:"mic_time_worker"` // CPRelationshipWorker 控制 room-service 送礼事件消费和 CP 关系申请/亲密值维护。 CPRelationshipWorker CPRelationshipWorkerConfig `yaml:"cp_relationship_worker"` // CPIntimacyLeaderboard 控制 CP 亲密值排行榜 Redis 读模型。 CPIntimacyLeaderboard CPIntimacyLeaderboardConfig `yaml:"cp_intimacy_leaderboard"` // OutboxWorker 控制 user_outbox 到 MQ 的补偿投递。 OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"` // RocketMQ 控制 wallet/room outbox MQ 消费。 RocketMQ RocketMQConfig `yaml:"rocketmq"` // LoginRisk 控制登录后 IP 风控策略、Redis session denylist 和 IP 查询 provider。 LoginRisk LoginRiskConfig `yaml:"login_risk"` // RoomService 是封禁后驱逐当前房间用户的内部 gRPC 依赖。 RoomService RoomServiceConfig `yaml:"room_service"` // ActivityService 是注册成功后发放注册奖励的内部 gRPC 依赖。 ActivityService ActivityServiceConfig `yaml:"activity_service"` // WalletService 是 CP 榜头像框等资源读模型的内部 gRPC 依赖。 WalletService WalletServiceConfig `yaml:"wallet_service"` // TencentIM 是封禁后失效用户 IM 登录态的外部集成配置。 TencentIM TencentIMConfig `yaml:"tencent_im"` // JWT 控制 access token 和 refresh session 的签发参数。 JWT JWTConfig `yaml:"jwt"` // Log 控制 stdout 结构化日志格式、等级和 access body 策略。 Log logx.Config `yaml:"log"` } // LoginRiskConfig 保存登录地区风控的 Redis、TTL 和 provider 顺序;后台扫描由 cron-service 调度。 type LoginRiskConfig struct { Enabled bool `yaml:"enabled"` RedisAddr string `yaml:"redis_addr"` RedisPassword string `yaml:"redis_password"` RedisDB int `yaml:"redis_db"` UnsupportedCountries []string `yaml:"unsupported_countries"` BlockedTTLSec int64 `yaml:"blocked_ttl_sec"` AllowedTTLSec int64 `yaml:"allowed_ttl_sec"` UnknownTTLSec int64 `yaml:"unknown_ttl_sec"` ProviderTimeoutMs int64 `yaml:"provider_timeout_ms"` Providers []string `yaml:"providers"` } // RoomServiceConfig 保存 user-service 到 room-service 的低频治理 RPC 连接参数。 type RoomServiceConfig struct { Enabled bool `yaml:"enabled"` Addr string `yaml:"addr"` RequestTimeoutMs int64 `yaml:"request_timeout_ms"` } // ActivityServiceConfig 保存 user-service 到 activity-service 的低频注册奖励 RPC 连接参数。 type ActivityServiceConfig struct { Enabled bool `yaml:"enabled"` Addr string `yaml:"addr"` RequestTimeoutMs int64 `yaml:"request_timeout_ms"` } // WalletServiceConfig 保存 user-service 到 wallet-service 的资源读模型 RPC 连接参数。 type WalletServiceConfig struct { Enabled bool `yaml:"enabled"` Addr string `yaml:"addr"` RequestTimeoutMs int64 `yaml:"request_timeout_ms"` } // TencentIMConfig 描述 user-service 调用腾讯云 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"` AdminUserSigTTLSec int64 `yaml:"admin_user_sig_ttl_sec"` Endpoint string `yaml:"endpoint"` RequestTimeoutMs int64 `yaml:"request_timeout_ms"` } // RESTConfig 转换成 pkg/tencentim REST client 配置。 func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig { timeout := time.Duration(c.RequestTimeoutMs) * time.Millisecond if timeout <= 0 { timeout = 5 * time.Second } ttl := time.Duration(c.AdminUserSigTTLSec) * time.Second 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: tencentim.DefaultGroupType, HTTPClient: &http.Client{Timeout: timeout}, } } // IDGeneratorConfig 保存 user_id 发号节点配置。 type IDGeneratorConfig struct { // NodeID 参与雪花 ID 发号,多个 user-service 节点必须不同。 NodeID int64 `yaml:"node_id"` } // DisplayUserIDConfig 保存短号分配和修改策略。 type DisplayUserIDConfig struct { // AllocateMaxAttempts 是默认短号冲突时的最大重试次数。 AllocateMaxAttempts int `yaml:"allocate_max_attempts"` // ChangeCooldownSec 是默认短号修改冷却期秒数。 ChangeCooldownSec int64 `yaml:"change_cooldown_sec"` } // ThirdPartyConfig 保存三方登录 provider allowlist 和真实 verifier 配置。 type ThirdPartyConfig struct { // AllowedProviders 是 provider allowlist;firebase 会被强制路由到 Firebase verifier。 AllowedProviders []string `yaml:"allowed_providers"` // Firebase 保存 Firebase Auth ID token verifier 配置。 Firebase FirebaseConfig `yaml:"firebase"` } // FirebaseConfig 保存 Firebase Auth ID token verifier 配置。 type FirebaseConfig struct { // ProjectID 必须匹配 Firebase token 的 aud 和 issuer。 ProjectID string `yaml:"project_id"` // AllowedSignInProviders 限制 firebase.sign_in_provider,首版默认 google.com。 AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"` // CertsURL 只用于测试或私有代理;生产为空时使用 Firebase 官方公钥端点。 CertsURL string `yaml:"certs_url"` // Projects 按 app_code 配置 Firebase 项目;Huwaa/Lalu 必须各自校验自己的 aud/iss。 Projects map[string]FirebaseProjectConfig `yaml:"projects"` } // FirebaseProjectConfig 保存单个 App 的 Firebase 项目配置。 type FirebaseProjectConfig struct { // ProjectID 必须匹配该 App Firebase token 的 aud 和 issuer。 ProjectID string `yaml:"project_id"` // AllowedSignInProviders 为空时继承 firebase.allowed_sign_in_providers。 AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"` // CertsURL 为空时继承 firebase.certs_url,生产通常不需要配置。 CertsURL string `yaml:"certs_url"` } // InviteRechargeWorkerConfig 保存钱包充值事实消费策略。 type InviteRechargeWorkerConfig struct { // Enabled 控制是否启动钱包充值 MQ 消费。 Enabled bool `yaml:"enabled"` } // MicTimeWorkerConfig 保存房间麦位事件消费策略。 type MicTimeWorkerConfig struct { // Enabled 控制是否启动 room outbox MQ 消费;关闭后查询仍可读取已聚合数据。 Enabled bool `yaml:"enabled"` } // CPRelationshipWorkerConfig 保存 CP 礼物事件消费策略。 type CPRelationshipWorkerConfig struct { // Enabled 控制是否启动 room gift MQ 消费;关闭后只能处理历史已消费关系。 Enabled bool `yaml:"enabled"` // ConsumerGroup 必须独立于麦位消费,避免两个读模型共享 MQ 位点。 ConsumerGroup string `yaml:"consumer_group"` } // CPIntimacyLeaderboardConfig 保存 CP 亲密榜 Redis zset 的连接和 key 前缀。 type CPIntimacyLeaderboardConfig struct { Enabled bool `yaml:"enabled"` RedisAddr string `yaml:"redis_addr"` RedisPassword string `yaml:"redis_password"` RedisDB int `yaml:"redis_db"` KeyPrefix string `yaml:"key_prefix"` } // OutboxWorkerConfig 保存 user_outbox MQ 发布策略。 type OutboxWorkerConfig struct { Enabled bool `yaml:"enabled"` PollInterval time.Duration `yaml:"poll_interval"` BatchSize int `yaml:"batch_size"` PublishTimeout time.Duration `yaml:"publish_timeout"` } // RocketMQConfig 保存 user-service 消费 owner service 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 WalletOutboxMQConfig `yaml:"wallet_outbox"` RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"` UserOutbox UserOutboxMQConfig `yaml:"user_outbox"` } type WalletOutboxMQConfig struct { Enabled bool `yaml:"enabled"` Topic string `yaml:"topic"` ConsumerGroup string `yaml:"consumer_group"` ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"` } type RoomOutboxMQConfig struct { Enabled bool `yaml:"enabled"` Topic string `yaml:"topic"` ConsumerGroup string `yaml:"consumer_group"` ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"` } type UserOutboxMQConfig struct { Enabled bool `yaml:"enabled"` Topic string `yaml:"topic"` ProducerGroup string `yaml:"producer_group"` } // JWTConfig 保存 access token 和 refresh token 的签发策略。 type JWTConfig struct { // Issuer 写入 access token iss claim。 Issuer string `yaml:"issuer"` // AccessTokenTTLSec 是 App 登录 access token 有效期;默认 168 小时,减少正常用户被动重登。 AccessTokenTTLSec int64 `yaml:"access_token_ttl_sec"` // RefreshTokenTTLSec 是服务端 session/refresh token 有效期。 RefreshTokenTTLSec int64 `yaml:"refresh_token_ttl_sec"` // SigningAlg 当前只支持 HS256。 SigningAlg string `yaml:"signing_alg"` // SigningSecret 是 HS256 签名密钥,生产不能使用默认开发值。 SigningSecret string `yaml:"signing_secret"` } // Default 返回本地默认配置。 func Default() Config { // 默认端口遵守项目 13xxx 约束;本地进程默认连接 docker-compose 暴露的 MySQL。 return Config{ ServiceName: "user-service", NodeID: "user-local", Environment: "local", GRPCAddr: ":13005", HealthHTTPAddr: ":13105", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC", MySQLAutoMigrate: false, IDGenerator: IDGeneratorConfig{ NodeID: 1, }, DisplayUserID: DisplayUserIDConfig{ AllocateMaxAttempts: 8, ChangeCooldownSec: 2592000, }, ThirdParty: ThirdPartyConfig{ AllowedProviders: []string{"firebase"}, Firebase: FirebaseConfig{ AllowedSignInProviders: []string{"google.com"}, }, }, InviteRechargeWorker: InviteRechargeWorkerConfig{ Enabled: false, }, MicTimeWorker: MicTimeWorkerConfig{ Enabled: false, }, CPRelationshipWorker: CPRelationshipWorkerConfig{ Enabled: false, ConsumerGroup: "hyapp-user-cp-room-outbox", }, CPIntimacyLeaderboard: CPIntimacyLeaderboardConfig{ Enabled: true, RedisAddr: "127.0.0.1:13379", KeyPrefix: "user:cp_intimacy_leaderboard", }, OutboxWorker: OutboxWorkerConfig{ Enabled: false, PollInterval: time.Second, BatchSize: 100, PublishTimeout: 3 * time.Second, }, RocketMQ: defaultRocketMQConfig(), LoginRisk: LoginRiskConfig{ Enabled: true, RedisAddr: "127.0.0.1:13379", UnsupportedCountries: []string{"CN"}, BlockedTTLSec: 2592000, AllowedTTLSec: 604800, UnknownTTLSec: 1800, ProviderTimeoutMs: 800, Providers: []string{"edge_country"}, }, RoomService: RoomServiceConfig{ Enabled: true, Addr: "127.0.0.1:13001", RequestTimeoutMs: 3000, }, ActivityService: ActivityServiceConfig{ Enabled: true, Addr: "127.0.0.1:13006", RequestTimeoutMs: 3000, }, WalletService: WalletServiceConfig{ Enabled: true, Addr: "127.0.0.1:13004", RequestTimeoutMs: 3000, }, TencentIM: TencentIMConfig{ Enabled: false, AdminIdentifier: "administrator", AdminUserSigTTLSec: 86400, Endpoint: tencentim.DefaultEndpoint, RequestTimeoutMs: 5000, }, JWT: JWTConfig{ Issuer: "hyapp", AccessTokenTTLSec: 604800, RefreshTokenTTLSec: 2592000, SigningAlg: "HS256", SigningSecret: "dev-shared-secret", }, Log: logx.Config{ Level: "info", Format: "json", MaxPayloadBytes: 2048, }, } } // Load 从独立 config.yaml 读取 user-service 配置。 func Load(path string) (Config, error) { cfg := Default() if path == "" { // 空路径显式使用默认配置,便于单元测试和最小启动。 return cfg, nil } if err := configx.LoadYAML(path, &cfg); err != nil { // 配置解析失败返回空 Config,避免调用方误用部分默认值继续启动。 return Config{}, err } cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) if cfg.ServiceName == "" { cfg.ServiceName = "user-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.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr) if cfg.HealthHTTPAddr == "" { cfg.HealthHTTPAddr = ":13105" } cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.LoginRisk.RedisAddr) cfg.CPIntimacyLeaderboard.RedisAddr = strings.TrimSpace(cfg.CPIntimacyLeaderboard.RedisAddr) cfg.CPIntimacyLeaderboard.KeyPrefix = strings.Trim(strings.TrimSpace(cfg.CPIntimacyLeaderboard.KeyPrefix), ":") if cfg.CPIntimacyLeaderboard.RedisAddr == "" { cfg.CPIntimacyLeaderboard.RedisAddr = cfg.LoginRisk.RedisAddr } if cfg.CPIntimacyLeaderboard.KeyPrefix == "" { cfg.CPIntimacyLeaderboard.KeyPrefix = "user:cp_intimacy_leaderboard" } cfg.ThirdParty.AllowedProviders = normalizeStringSlice(cfg.ThirdParty.AllowedProviders) cfg.ThirdParty.Firebase = normalizeFirebaseConfig(cfg.ThirdParty.Firebase) cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr) if cfg.RoomService.Addr == "" { cfg.RoomService.Addr = "127.0.0.1:13001" } if cfg.RoomService.RequestTimeoutMs <= 0 { cfg.RoomService.RequestTimeoutMs = 3000 } cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr) if cfg.ActivityService.Addr == "" { cfg.ActivityService.Addr = "127.0.0.1:13006" } if cfg.ActivityService.RequestTimeoutMs <= 0 { cfg.ActivityService.RequestTimeoutMs = 3000 } cfg.WalletService.Addr = strings.TrimSpace(cfg.WalletService.Addr) if cfg.WalletService.Addr == "" { cfg.WalletService.Addr = "127.0.0.1:13004" } if cfg.WalletService.RequestTimeoutMs <= 0 { cfg.WalletService.RequestTimeoutMs = 3000 } 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 } if cfg.TencentIM.AdminUserSigTTLSec <= 0 { cfg.TencentIM.AdminUserSigTTLSec = 86400 } if cfg.TencentIM.RequestTimeoutMs <= 0 { cfg.TencentIM.RequestTimeoutMs = 5000 } if cfg.LoginRisk.BlockedTTLSec <= 0 { cfg.LoginRisk.BlockedTTLSec = 2592000 } if cfg.LoginRisk.AllowedTTLSec <= 0 { cfg.LoginRisk.AllowedTTLSec = 604800 } if cfg.LoginRisk.UnknownTTLSec <= 0 { cfg.LoginRisk.UnknownTTLSec = 1800 } if cfg.LoginRisk.ProviderTimeoutMs <= 0 { cfg.LoginRisk.ProviderTimeoutMs = 800 } for index, provider := range cfg.LoginRisk.Providers { cfg.LoginRisk.Providers[index] = strings.TrimSpace(provider) } rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ) if err != nil { return Config{}, err } cfg.RocketMQ = rocketMQ if cfg.InviteRechargeWorker.Enabled && !cfg.RocketMQ.WalletOutbox.Enabled { return Config{}, errors.New("invite_recharge_worker requires rocketmq.wallet_outbox.enabled") } if cfg.MicTimeWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled { return Config{}, errors.New("mic_time_worker requires rocketmq.room_outbox.enabled") } cfg.CPRelationshipWorker.ConsumerGroup = strings.TrimSpace(cfg.CPRelationshipWorker.ConsumerGroup) if cfg.CPRelationshipWorker.ConsumerGroup == "" { cfg.CPRelationshipWorker.ConsumerGroup = "hyapp-user-cp-room-outbox" } if cfg.CPRelationshipWorker.Enabled && !cfg.RocketMQ.RoomOutbox.Enabled { return Config{}, errors.New("cp_relationship_worker requires rocketmq.room_outbox.enabled") } 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.UserOutbox.Enabled { return Config{}, errors.New("outbox_worker requires rocketmq.user_outbox.enabled") } return cfg, nil } func defaultRocketMQConfig() RocketMQConfig { return RocketMQConfig{ Enabled: false, WalletOutbox: WalletOutboxMQConfig{ Enabled: false, Topic: "hyapp_wallet_outbox", ConsumerGroup: "hyapp-user-invite-wallet-outbox", ConsumerMaxReconsumeTimes: 16, }, RoomOutbox: RoomOutboxMQConfig{ Enabled: false, Topic: "hyapp_room_outbox", ConsumerGroup: "hyapp-user-mictime-room-outbox", ConsumerMaxReconsumeTimes: 16, }, UserOutbox: UserOutboxMQConfig{ Enabled: false, Topic: "hyapp_user_outbox", ProducerGroup: "hyapp-user-outbox-producer", }, } } 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.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.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.UserOutbox.Topic = strings.TrimSpace(cfg.UserOutbox.Topic); cfg.UserOutbox.Topic == "" { cfg.UserOutbox.Topic = defaults.UserOutbox.Topic } if cfg.UserOutbox.ProducerGroup = strings.TrimSpace(cfg.UserOutbox.ProducerGroup); cfg.UserOutbox.ProducerGroup == "" { cfg.UserOutbox.ProducerGroup = defaults.UserOutbox.ProducerGroup } if cfg.WalletOutbox.Enabled || cfg.RoomOutbox.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 normalizeFirebaseConfig(cfg FirebaseConfig) FirebaseConfig { cfg.ProjectID = strings.TrimSpace(cfg.ProjectID) cfg.AllowedSignInProviders = normalizeStringSlice(cfg.AllowedSignInProviders) cfg.CertsURL = strings.TrimSpace(cfg.CertsURL) if len(cfg.Projects) == 0 { return cfg } projects := make(map[string]FirebaseProjectConfig, len(cfg.Projects)) for rawAppCode, project := range cfg.Projects { app := appcode.Normalize(rawAppCode) project.ProjectID = strings.TrimSpace(project.ProjectID) project.CertsURL = strings.TrimSpace(project.CertsURL) project.AllowedSignInProviders = normalizeStringSlice(project.AllowedSignInProviders) if len(project.AllowedSignInProviders) == 0 { // 单 App 没写登录方式时继承全局配置;全局为空时 auth verifier 仍会默认 google.com。 project.AllowedSignInProviders = cfg.AllowedSignInProviders } if project.CertsURL == "" { // Firebase 公钥端点对所有项目相同;测试代理或私有代理可以只写一次。 project.CertsURL = cfg.CertsURL } projects[app] = project } cfg.Projects = projects 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 }