388 lines
14 KiB
Go
388 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp/pkg/configx"
|
|
"hyapp/pkg/logx"
|
|
)
|
|
|
|
// Config 描述 wallet-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 是账户、交易、分录和 outbox 的必需存储连接串。
|
|
MySQLDSN string `yaml:"mysql_dsn"`
|
|
// ActivityServiceAddr 用于把 wallet 资源赠送 outbox 投影到 activity badge 展示读模型。
|
|
ActivityServiceAddr string `yaml:"activity_service_addr"`
|
|
// RedPacketExpiryWorker 控制红包过期退款扫描。
|
|
RedPacketExpiryWorker RedPacketExpiryWorkerConfig `yaml:"red_packet_expiry_worker"`
|
|
// RocketMQ 控制 wallet_outbox 事实发布。
|
|
RocketMQ RocketMQConfig `yaml:"rocketmq"`
|
|
// GooglePlay 控制 Google Play Developer API 购买校验。
|
|
GooglePlay GooglePlayConfig `yaml:"google_play"`
|
|
// OutboxWorker 控制 wallet_outbox 到 MQ 的补偿投递。
|
|
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
|
|
// RealtimeOutboxWorker 控制红包等实时 UI 事件到独立 MQ topic 的补偿投递。
|
|
RealtimeOutboxWorker OutboxWorkerConfig `yaml:"realtime_outbox_worker"`
|
|
// ProjectionWorker 控制 wallet_outbox MQ 到本服务读模型的投影消费。
|
|
ProjectionWorker ProjectionWorkerConfig `yaml:"projection_worker"`
|
|
// MySQLAutoMigrate 保留给显式本地迁移;线上 schema 由发布流程或 initdb 管理。
|
|
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
|
Log logx.Config `yaml:"log"`
|
|
}
|
|
|
|
// RedPacketExpiryWorkerConfig 保存红包过期退款 worker 策略。
|
|
type RedPacketExpiryWorkerConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
AppCode string `yaml:"app_code"`
|
|
PollInterval time.Duration `yaml:"poll_interval"`
|
|
BatchSize int32 `yaml:"batch_size"`
|
|
}
|
|
|
|
// RocketMQConfig 描述 wallet-service 发布账务事实的 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"`
|
|
SendTimeout time.Duration `yaml:"send_timeout"`
|
|
Retry int `yaml:"retry"`
|
|
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
|
|
RealtimeOutbox WalletOutboxMQConfig `yaml:"realtime_outbox"`
|
|
}
|
|
|
|
// WalletOutboxMQConfig 保存 wallet_outbox topic 的生产者配置。
|
|
type WalletOutboxMQConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Topic string `yaml:"topic"`
|
|
ProducerGroup string `yaml:"producer_group"`
|
|
}
|
|
|
|
// OutboxWorkerConfig 控制 wallet_outbox pending 事件的 MQ fanout。
|
|
type OutboxWorkerConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
PollInterval time.Duration `yaml:"poll_interval"`
|
|
BatchSize int `yaml:"batch_size"`
|
|
Concurrency int `yaml:"concurrency"`
|
|
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
|
MaxRetryCount int `yaml:"max_retry_count"`
|
|
InitialBackoff time.Duration `yaml:"initial_backoff"`
|
|
MaxBackoff time.Duration `yaml:"max_backoff"`
|
|
EventTypes []string `yaml:"event_types"`
|
|
}
|
|
|
|
// ProjectionWorkerConfig 控制礼物墙和 badge 展示读模型从 wallet_outbox MQ 投影的节奏。
|
|
type ProjectionWorkerConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
PollInterval time.Duration `yaml:"poll_interval"` // Deprecated: runtime projection is MQ-driven.
|
|
BatchSize int `yaml:"batch_size"`
|
|
LockTTL time.Duration `yaml:"lock_ttl"`
|
|
ConsumerGroup string `yaml:"consumer_group"`
|
|
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
|
}
|
|
|
|
// GooglePlayConfig 保存 Google Play Developer API 服务账号和网络配置。
|
|
type GooglePlayConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
PackageName string `yaml:"package_name"`
|
|
ServiceAccountFile string `yaml:"service_account_file"`
|
|
ServiceAccountJSON string `yaml:"service_account_json"`
|
|
APIBaseURL string `yaml:"api_base_url"`
|
|
TokenURL string `yaml:"token_url"`
|
|
HTTPTimeout time.Duration `yaml:"http_timeout"`
|
|
ConsumeEnabled bool `yaml:"consume_enabled"`
|
|
}
|
|
|
|
// Default 返回本地开发默认配置。
|
|
func Default() Config {
|
|
return Config{
|
|
ServiceName: "wallet-service",
|
|
NodeID: "wallet-local",
|
|
Environment: "local",
|
|
GRPCAddr: ":13004",
|
|
HealthHTTPAddr: ":13104",
|
|
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC",
|
|
ActivityServiceAddr: "127.0.0.1:13006",
|
|
RedPacketExpiryWorker: RedPacketExpiryWorkerConfig{
|
|
Enabled: true,
|
|
AppCode: "lalu",
|
|
PollInterval: 5 * time.Second,
|
|
BatchSize: 50,
|
|
},
|
|
RocketMQ: defaultRocketMQConfig(),
|
|
GooglePlay: GooglePlayConfig{
|
|
Enabled: false,
|
|
APIBaseURL: "https://androidpublisher.googleapis.com",
|
|
TokenURL: "https://oauth2.googleapis.com/token",
|
|
HTTPTimeout: 10 * time.Second,
|
|
ConsumeEnabled: true,
|
|
},
|
|
OutboxWorker: OutboxWorkerConfig{
|
|
Enabled: false,
|
|
PollInterval: time.Second,
|
|
BatchSize: 100,
|
|
Concurrency: 1,
|
|
PublishTimeout: 3 * time.Second,
|
|
MaxRetryCount: 10,
|
|
InitialBackoff: 5 * time.Second,
|
|
MaxBackoff: 5 * time.Minute,
|
|
},
|
|
RealtimeOutboxWorker: OutboxWorkerConfig{
|
|
Enabled: false,
|
|
PollInterval: time.Second,
|
|
BatchSize: 50,
|
|
Concurrency: 2,
|
|
PublishTimeout: 3 * time.Second,
|
|
MaxRetryCount: 10,
|
|
InitialBackoff: 2 * time.Second,
|
|
MaxBackoff: time.Minute,
|
|
EventTypes: defaultRealtimeOutboxEventTypes(),
|
|
},
|
|
ProjectionWorker: ProjectionWorkerConfig{
|
|
Enabled: false,
|
|
PollInterval: 60 * time.Second,
|
|
BatchSize: 10,
|
|
LockTTL: 30 * time.Second,
|
|
ConsumerGroup: "hyapp-wallet-projection",
|
|
ConsumerMaxReconsumeTimes: 16,
|
|
},
|
|
MySQLAutoMigrate: false,
|
|
Log: logx.Config{
|
|
Level: "info",
|
|
Format: "json",
|
|
MaxPayloadBytes: 2048,
|
|
},
|
|
}
|
|
}
|
|
|
|
func defaultRocketMQConfig() RocketMQConfig {
|
|
return RocketMQConfig{
|
|
Enabled: false,
|
|
SendTimeout: 3 * time.Second,
|
|
Retry: 2,
|
|
WalletOutbox: WalletOutboxMQConfig{
|
|
Enabled: false,
|
|
Topic: "hyapp_wallet_outbox",
|
|
ProducerGroup: "hyapp-wallet-outbox-producer",
|
|
},
|
|
RealtimeOutbox: WalletOutboxMQConfig{
|
|
Enabled: false,
|
|
Topic: "hyapp_wallet_realtime_outbox",
|
|
ProducerGroup: "hyapp-wallet-realtime-outbox-producer",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Load 从独立 config.yaml 读取 wallet-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 = "wallet-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 = ":13104"
|
|
}
|
|
cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr)
|
|
if cfg.ActivityServiceAddr == "" {
|
|
cfg.ActivityServiceAddr = "127.0.0.1:13006"
|
|
}
|
|
cfg.RedPacketExpiryWorker.AppCode = strings.TrimSpace(cfg.RedPacketExpiryWorker.AppCode)
|
|
if cfg.RedPacketExpiryWorker.AppCode == "" {
|
|
cfg.RedPacketExpiryWorker.AppCode = Default().RedPacketExpiryWorker.AppCode
|
|
}
|
|
if cfg.RedPacketExpiryWorker.PollInterval <= 0 {
|
|
cfg.RedPacketExpiryWorker.PollInterval = Default().RedPacketExpiryWorker.PollInterval
|
|
}
|
|
if cfg.RedPacketExpiryWorker.BatchSize <= 0 {
|
|
cfg.RedPacketExpiryWorker.BatchSize = Default().RedPacketExpiryWorker.BatchSize
|
|
}
|
|
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RocketMQ = rocketMQ
|
|
cfg.GooglePlay.PackageName = strings.TrimSpace(cfg.GooglePlay.PackageName)
|
|
cfg.GooglePlay.ServiceAccountFile = strings.TrimSpace(cfg.GooglePlay.ServiceAccountFile)
|
|
cfg.GooglePlay.ServiceAccountJSON = strings.TrimSpace(cfg.GooglePlay.ServiceAccountJSON)
|
|
cfg.GooglePlay.APIBaseURL = strings.TrimRight(strings.TrimSpace(cfg.GooglePlay.APIBaseURL), "/")
|
|
if cfg.GooglePlay.APIBaseURL == "" {
|
|
cfg.GooglePlay.APIBaseURL = Default().GooglePlay.APIBaseURL
|
|
}
|
|
cfg.GooglePlay.TokenURL = strings.TrimSpace(cfg.GooglePlay.TokenURL)
|
|
if cfg.GooglePlay.TokenURL == "" {
|
|
cfg.GooglePlay.TokenURL = Default().GooglePlay.TokenURL
|
|
}
|
|
if cfg.GooglePlay.HTTPTimeout <= 0 {
|
|
cfg.GooglePlay.HTTPTimeout = Default().GooglePlay.HTTPTimeout
|
|
}
|
|
if cfg.GooglePlay.Enabled {
|
|
if cfg.GooglePlay.PackageName == "" {
|
|
return Config{}, errors.New("google_play package_name is required")
|
|
}
|
|
if cfg.GooglePlay.ServiceAccountFile == "" && cfg.GooglePlay.ServiceAccountJSON == "" {
|
|
return Config{}, errors.New("google_play service account is required")
|
|
}
|
|
}
|
|
cfg.OutboxWorker = normalizeOutboxWorkerConfig(cfg.OutboxWorker, Default().OutboxWorker)
|
|
if cfg.OutboxWorker.Enabled && !cfg.RocketMQ.WalletOutbox.Enabled {
|
|
return Config{}, errors.New("outbox_worker requires rocketmq.wallet_outbox.enabled")
|
|
}
|
|
cfg.RealtimeOutboxWorker = normalizeOutboxWorkerConfig(cfg.RealtimeOutboxWorker, Default().RealtimeOutboxWorker)
|
|
if len(cfg.RealtimeOutboxWorker.EventTypes) == 0 {
|
|
cfg.RealtimeOutboxWorker.EventTypes = defaultRealtimeOutboxEventTypes()
|
|
}
|
|
if cfg.RealtimeOutboxWorker.Enabled && !cfg.RocketMQ.RealtimeOutbox.Enabled {
|
|
return Config{}, errors.New("realtime_outbox_worker requires rocketmq.realtime_outbox.enabled")
|
|
}
|
|
if cfg.ProjectionWorker.PollInterval <= 0 {
|
|
cfg.ProjectionWorker.PollInterval = Default().ProjectionWorker.PollInterval
|
|
}
|
|
if cfg.ProjectionWorker.BatchSize <= 0 {
|
|
cfg.ProjectionWorker.BatchSize = Default().ProjectionWorker.BatchSize
|
|
}
|
|
if cfg.ProjectionWorker.BatchSize > 100 {
|
|
cfg.ProjectionWorker.BatchSize = 100
|
|
}
|
|
if cfg.ProjectionWorker.LockTTL <= 0 {
|
|
cfg.ProjectionWorker.LockTTL = Default().ProjectionWorker.LockTTL
|
|
}
|
|
if cfg.ProjectionWorker.ConsumerGroup = strings.TrimSpace(cfg.ProjectionWorker.ConsumerGroup); cfg.ProjectionWorker.ConsumerGroup == "" {
|
|
cfg.ProjectionWorker.ConsumerGroup = Default().ProjectionWorker.ConsumerGroup
|
|
}
|
|
if cfg.ProjectionWorker.ConsumerMaxReconsumeTimes <= 0 {
|
|
cfg.ProjectionWorker.ConsumerMaxReconsumeTimes = Default().ProjectionWorker.ConsumerMaxReconsumeTimes
|
|
}
|
|
if cfg.ProjectionWorker.Enabled && !cfg.RocketMQ.WalletOutbox.Enabled {
|
|
return Config{}, errors.New("projection_worker requires rocketmq.wallet_outbox.enabled")
|
|
}
|
|
|
|
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.SendTimeout <= 0 {
|
|
cfg.SendTimeout = defaults.SendTimeout
|
|
}
|
|
if cfg.Retry <= 0 {
|
|
cfg.Retry = defaults.Retry
|
|
}
|
|
if cfg.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" {
|
|
cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic
|
|
}
|
|
if cfg.WalletOutbox.ProducerGroup = strings.TrimSpace(cfg.WalletOutbox.ProducerGroup); cfg.WalletOutbox.ProducerGroup == "" {
|
|
cfg.WalletOutbox.ProducerGroup = defaults.WalletOutbox.ProducerGroup
|
|
}
|
|
if cfg.RealtimeOutbox.Topic = strings.TrimSpace(cfg.RealtimeOutbox.Topic); cfg.RealtimeOutbox.Topic == "" {
|
|
cfg.RealtimeOutbox.Topic = defaults.RealtimeOutbox.Topic
|
|
}
|
|
if cfg.RealtimeOutbox.ProducerGroup = strings.TrimSpace(cfg.RealtimeOutbox.ProducerGroup); cfg.RealtimeOutbox.ProducerGroup == "" {
|
|
cfg.RealtimeOutbox.ProducerGroup = defaults.RealtimeOutbox.ProducerGroup
|
|
}
|
|
if cfg.WalletOutbox.Enabled || cfg.RealtimeOutbox.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 normalizeOutboxWorkerConfig(cfg OutboxWorkerConfig, defaults OutboxWorkerConfig) OutboxWorkerConfig {
|
|
if cfg.PollInterval <= 0 {
|
|
cfg.PollInterval = defaults.PollInterval
|
|
}
|
|
if cfg.BatchSize <= 0 {
|
|
cfg.BatchSize = defaults.BatchSize
|
|
}
|
|
if cfg.BatchSize > 500 {
|
|
cfg.BatchSize = 500
|
|
}
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = defaults.Concurrency
|
|
}
|
|
if cfg.Concurrency > 64 {
|
|
cfg.Concurrency = 64
|
|
}
|
|
if cfg.PublishTimeout <= 0 {
|
|
cfg.PublishTimeout = defaults.PublishTimeout
|
|
}
|
|
if cfg.MaxRetryCount <= 0 {
|
|
cfg.MaxRetryCount = defaults.MaxRetryCount
|
|
}
|
|
if cfg.InitialBackoff <= 0 {
|
|
cfg.InitialBackoff = defaults.InitialBackoff
|
|
}
|
|
if cfg.MaxBackoff <= 0 {
|
|
cfg.MaxBackoff = defaults.MaxBackoff
|
|
}
|
|
if cfg.MaxBackoff < cfg.InitialBackoff {
|
|
cfg.MaxBackoff = cfg.InitialBackoff
|
|
}
|
|
cfg.EventTypes = normalizeStringSlice(cfg.EventTypes)
|
|
return cfg
|
|
}
|
|
|
|
func normalizeStringSlice(values []string) []string {
|
|
normalized := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
normalized = append(normalized, value)
|
|
}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func defaultRealtimeOutboxEventTypes() []string {
|
|
return []string{
|
|
"WalletRedPacketCreated",
|
|
"WalletRedPacketClaimed",
|
|
"WalletRedPacketRefunded",
|
|
}
|
|
}
|