307 lines
11 KiB
Go

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"`
// 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"`
}
// 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"`
}
// 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,
},
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,
},
}
}
// 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)
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 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
}
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.RoomOutbox.Enabled || cfg.WalletOutbox.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
}