252 lines
9.7 KiB
Go
252 lines
9.7 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/configx"
|
||
"hyapp/pkg/logx"
|
||
)
|
||
|
||
// Config 描述 lucky-gift-service 的进程启动边界。
|
||
// 规则、RTP、奖池、应用级外部接入和 outbox 都属于本服务;room/activity/admin 只能通过 gRPC 访问。
|
||
type Config struct {
|
||
ServiceName string `yaml:"service_name"`
|
||
NodeID string `yaml:"node_id"`
|
||
Environment string `yaml:"environment"`
|
||
GRPCAddr string `yaml:"grpc_addr"`
|
||
HealthHTTPAddr string `yaml:"health_http_addr"`
|
||
MySQLDSN string `yaml:"mysql_dsn"`
|
||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||
WalletServiceAddr string `yaml:"wallet_service_addr"`
|
||
LuckyGiftWorker LuckyGiftWorkerConfig `yaml:"lucky_gift_worker"`
|
||
// RocketMQ 只发布 lucky-gift-service 自有 outbox 事实;room/activity 不能直读幸运礼物数据库。
|
||
RocketMQ RocketMQConfig `yaml:"rocketmq"`
|
||
Log logx.Config `yaml:"log"`
|
||
}
|
||
|
||
// RocketMQConfig 保存幸运礼物 owner topic 的连接身份。
|
||
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"`
|
||
LuckyGiftOutbox LuckyGiftOutboxMQConfig `yaml:"lucky_gift_outbox"`
|
||
}
|
||
|
||
// LuckyGiftOutboxMQConfig 控制 granted 中奖事实发布;地址和凭据来自运行环境,业务开关在可运行配置中默认开启。
|
||
type LuckyGiftOutboxMQConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Topic string `yaml:"topic"`
|
||
ProducerGroup string `yaml:"producer_group"`
|
||
SendTimeout time.Duration `yaml:"send_timeout"`
|
||
Retry int `yaml:"retry"`
|
||
}
|
||
|
||
// LuckyGiftWorkerConfig 控制 draw outbox 补偿和统计快照刷新。
|
||
// 外部 App 抽奖不走 HyApp wallet,但仍写 outbox 给审计/统计;内部房间抽奖中奖后由 worker 兜底返奖,
|
||
// 并在 granted 收敛后把 owner 事实可靠发布给 room/activity 的独立消费者。
|
||
type LuckyGiftWorkerConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
WorkerPollInterval time.Duration `yaml:"worker_poll_interval"`
|
||
WorkerBatchSize int `yaml:"worker_batch_size"`
|
||
WorkerConcurrency int `yaml:"worker_concurrency"`
|
||
WorkerLockTTL time.Duration `yaml:"worker_lock_ttl"`
|
||
WorkerMaxRetry int `yaml:"worker_max_retry"`
|
||
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
||
StatsRefreshInterval time.Duration `yaml:"stats_refresh_interval"`
|
||
StatsBatchSize int `yaml:"stats_batch_size"`
|
||
// Retention 控制 delivered 历史事件的定期清理;它独立于 Enabled,临时关闭补偿投递不会停掉清理。
|
||
Retention OutboxRetentionConfig `yaml:"retention"`
|
||
}
|
||
|
||
// OutboxRetentionConfig 控制 lucky_gift_outbox 中 delivered 历史事件的小批量定期删除。
|
||
// 只清理 delivered:pending/retryable/delivering 仍在投递生命周期内,failed 留给人工补偿。
|
||
// 抽奖事实和审计都在 lucky_draw_records;下游只读 RocketMQ owner 事实,不直读本表,因此 delivered 行可定期清理。
|
||
type OutboxRetentionConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
// MaxAge 是 delivered 事件按 updated_at_ms 计算的保留时长,早于 now-MaxAge 的才会删除。
|
||
MaxAge time.Duration `yaml:"max_age"`
|
||
// ScanInterval 是清空一轮可删事件后到下一轮扫描的间隔。
|
||
ScanInterval time.Duration `yaml:"scan_interval"`
|
||
// BatchSize 是单条 DELETE 的行数上限,小批量避免大事务长时间持有行锁。
|
||
BatchSize int `yaml:"batch_size"`
|
||
}
|
||
|
||
func Default() Config {
|
||
return Config{
|
||
ServiceName: "lucky-gift-service",
|
||
NodeID: "lucky-gift-local",
|
||
Environment: "local",
|
||
GRPCAddr: ":13013",
|
||
HealthHTTPAddr: ":13113",
|
||
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_lucky_gift?parseTime=true&charset=utf8mb4&loc=UTC",
|
||
MySQLAutoMigrate: true,
|
||
WalletServiceAddr: "127.0.0.1:13004",
|
||
LuckyGiftWorker: LuckyGiftWorkerConfig{
|
||
Enabled: true,
|
||
WorkerPollInterval: time.Second,
|
||
WorkerBatchSize: 100,
|
||
WorkerConcurrency: 8,
|
||
WorkerLockTTL: 30 * time.Second,
|
||
WorkerMaxRetry: 8,
|
||
PublishTimeout: 5 * time.Second,
|
||
StatsRefreshInterval: 10 * time.Minute,
|
||
StatsBatchSize: 5000,
|
||
Retention: OutboxRetentionConfig{
|
||
Enabled: true,
|
||
// 线上 delivered 约 9 万行/天且无下游读方;7 天足够排障,30 天会让表膨胀到近 300 万行。
|
||
MaxAge: 7 * 24 * time.Hour,
|
||
ScanInterval: 5 * time.Minute,
|
||
BatchSize: 500,
|
||
},
|
||
},
|
||
RocketMQ: defaultRocketMQConfig(),
|
||
Log: logx.Config{
|
||
Level: "info",
|
||
Format: "json",
|
||
MaxPayloadBytes: 2048,
|
||
},
|
||
}
|
||
}
|
||
|
||
func defaultRocketMQConfig() RocketMQConfig {
|
||
return RocketMQConfig{
|
||
Enabled: false,
|
||
LuckyGiftOutbox: LuckyGiftOutboxMQConfig{
|
||
Enabled: false,
|
||
Topic: "hyapp_lucky_gift_outbox",
|
||
ProducerGroup: "hyapp-lucky-gift-outbox-producer",
|
||
SendTimeout: 5 * time.Second,
|
||
Retry: 2,
|
||
},
|
||
}
|
||
}
|
||
|
||
func Load(path string) (Config, error) {
|
||
cfg := Default()
|
||
if strings.TrimSpace(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 = "lucky-gift-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 = ":13013"
|
||
}
|
||
cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr)
|
||
if cfg.HealthHTTPAddr == "" {
|
||
cfg.HealthHTTPAddr = ":13113"
|
||
}
|
||
cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr)
|
||
if cfg.WalletServiceAddr == "" {
|
||
cfg.WalletServiceAddr = "127.0.0.1:13004"
|
||
}
|
||
if cfg.LuckyGiftWorker.WorkerPollInterval <= 0 {
|
||
cfg.LuckyGiftWorker.WorkerPollInterval = time.Second
|
||
}
|
||
if cfg.LuckyGiftWorker.WorkerBatchSize <= 0 {
|
||
cfg.LuckyGiftWorker.WorkerBatchSize = 100
|
||
}
|
||
if cfg.LuckyGiftWorker.WorkerConcurrency <= 0 {
|
||
cfg.LuckyGiftWorker.WorkerConcurrency = 8
|
||
}
|
||
if cfg.LuckyGiftWorker.WorkerLockTTL <= 0 {
|
||
cfg.LuckyGiftWorker.WorkerLockTTL = 30 * time.Second
|
||
}
|
||
if cfg.LuckyGiftWorker.WorkerMaxRetry <= 0 {
|
||
cfg.LuckyGiftWorker.WorkerMaxRetry = 8
|
||
}
|
||
if cfg.LuckyGiftWorker.PublishTimeout <= 0 {
|
||
cfg.LuckyGiftWorker.PublishTimeout = 5 * time.Second
|
||
}
|
||
if cfg.LuckyGiftWorker.StatsRefreshInterval <= 0 {
|
||
cfg.LuckyGiftWorker.StatsRefreshInterval = 10 * time.Minute
|
||
}
|
||
if cfg.LuckyGiftWorker.StatsBatchSize <= 0 {
|
||
cfg.LuckyGiftWorker.StatsBatchSize = 5000
|
||
}
|
||
defaults := Default().LuckyGiftWorker.Retention
|
||
if cfg.LuckyGiftWorker.Retention.MaxAge <= 0 {
|
||
cfg.LuckyGiftWorker.Retention.MaxAge = defaults.MaxAge
|
||
}
|
||
if cfg.LuckyGiftWorker.Retention.ScanInterval <= 0 {
|
||
cfg.LuckyGiftWorker.Retention.ScanInterval = defaults.ScanInterval
|
||
}
|
||
if cfg.LuckyGiftWorker.Retention.BatchSize <= 0 {
|
||
cfg.LuckyGiftWorker.Retention.BatchSize = defaults.BatchSize
|
||
}
|
||
if cfg.LuckyGiftWorker.Retention.Enabled && cfg.LuckyGiftWorker.Retention.MaxAge < 24*time.Hour {
|
||
// 保留窗口低于一天基本可以断定是把单位写错(比如 30 写成 30ns/30s),宁可拒绝启动。
|
||
return Config{}, fmt.Errorf("lucky_gift_worker retention max_age %s is below the 24h safety floor", cfg.LuckyGiftWorker.Retention.MaxAge)
|
||
}
|
||
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
|
||
if err != nil {
|
||
return Config{}, err
|
||
}
|
||
cfg.RocketMQ = rocketMQ
|
||
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.LuckyGiftOutbox.Topic = strings.TrimSpace(cfg.LuckyGiftOutbox.Topic); cfg.LuckyGiftOutbox.Topic == "" {
|
||
cfg.LuckyGiftOutbox.Topic = defaults.LuckyGiftOutbox.Topic
|
||
}
|
||
if cfg.LuckyGiftOutbox.ProducerGroup = strings.TrimSpace(cfg.LuckyGiftOutbox.ProducerGroup); cfg.LuckyGiftOutbox.ProducerGroup == "" {
|
||
cfg.LuckyGiftOutbox.ProducerGroup = defaults.LuckyGiftOutbox.ProducerGroup
|
||
}
|
||
if cfg.LuckyGiftOutbox.SendTimeout <= 0 {
|
||
cfg.LuckyGiftOutbox.SendTimeout = defaults.LuckyGiftOutbox.SendTimeout
|
||
}
|
||
if cfg.LuckyGiftOutbox.Retry < 0 {
|
||
cfg.LuckyGiftOutbox.Retry = defaults.LuckyGiftOutbox.Retry
|
||
}
|
||
if cfg.LuckyGiftOutbox.Enabled {
|
||
cfg.Enabled = true
|
||
}
|
||
if cfg.Enabled {
|
||
if len(cfg.NameServers) == 0 && cfg.NameServerDomain == "" {
|
||
return RocketMQConfig{}, fmt.Errorf("rocketmq name_servers or name_server_domain is required")
|
||
}
|
||
if (cfg.AccessKey == "") != (cfg.SecretKey == "") {
|
||
return RocketMQConfig{}, fmt.Errorf("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
|
||
}
|