2026-05-22 14:03:24 +08:00

240 lines
6.3 KiB
Go

package config
import (
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
MySQL MySQLConfig
Redis RedisConfig
CDC CDCConfig
Dashboard DashboardConfig
Health HealthConfig
}
type MySQLConfig struct {
AppDSN string
CDCAddr string
CDCUser string
CDCPassword string
Schema string
WalletSchema string
MaxOpenConns int
MaxIdleConns int
}
type RedisConfig struct {
Enabled bool
Addr string
Username string
Password string
DB int
}
type CDCConfig struct {
ServerID uint32
StartMode string
DryRun bool
IncludeTables []string
FlushInterval time.Duration
HeartbeatEvery time.Duration
}
type DashboardConfig struct {
SysOrigins []string
StatTimezones []string
StorageTimezone string
RedisKeyPrefix string
RealtimeRedisTTL time.Duration
UnknownCountryCode string
}
type HealthConfig struct {
Addr string
}
func Load() (Config, error) {
cfg := Config{
MySQL: MySQLConfig{
AppDSN: env("DASHBOARD_MYSQL_DSN", env("MYSQL_DSN", "")),
CDCAddr: env("CDC_MYSQL_ADDR", ""),
CDCUser: env("CDC_MYSQL_USER", ""),
CDCPassword: env("CDC_MYSQL_PASSWORD", ""),
Schema: env("CDC_MYSQL_SCHEMA", "likei"),
WalletSchema: env("CDC_WALLET_SCHEMA", "likei_wallet"),
MaxOpenConns: envInt("MYSQL_MAX_OPEN_CONNS", 8),
MaxIdleConns: envInt("MYSQL_MAX_IDLE_CONNS", 4),
},
Redis: RedisConfig{
Enabled: envBool("REDIS_ENABLED", false),
Addr: env("REDIS_ADDR", ""),
Username: env("REDIS_USERNAME", ""),
Password: env("REDIS_PASSWORD", ""),
DB: envInt("REDIS_DB", 0),
},
CDC: CDCConfig{
ServerID: uint32(envInt("CDC_SERVER_ID", 29301)),
StartMode: strings.ToLower(env("CDC_START_MODE", "checkpoint")),
DryRun: envBool("CDC_DRY_RUN", true),
IncludeTables: csv(env("CDC_INCLUDE_TABLES", defaultTables())),
FlushInterval: envDuration("CDC_FLUSH_INTERVAL", 3*time.Second),
HeartbeatEvery: envDuration("CDC_HEARTBEAT_EVERY", 30*time.Second),
},
Dashboard: DashboardConfig{
SysOrigins: csv(env("DASHBOARD_SYS_ORIGINS", "LIKEI")),
StatTimezones: csv(env("DASHBOARD_STAT_TIMEZONES", "Asia/Riyadh")),
StorageTimezone: env("DASHBOARD_STORAGE_TIMEZONE", "Asia/Riyadh"),
RedisKeyPrefix: env("DASHBOARD_REDIS_KEY_PREFIX", "country_dashboard"),
RealtimeRedisTTL: envDuration("DASHBOARD_REALTIME_REDIS_TTL", 48*time.Hour),
UnknownCountryCode: env("DASHBOARD_UNKNOWN_COUNTRY_CODE", "UNKNOWN"),
},
Health: HealthConfig{
Addr: env("HEALTH_ADDR", ":2910"),
},
}
if cfg.MySQL.AppDSN == "" {
return Config{}, errors.New("DASHBOARD_MYSQL_DSN or MYSQL_DSN is required")
}
if cfg.MySQL.CDCAddr == "" || cfg.MySQL.CDCUser == "" {
return Config{}, errors.New("CDC_MYSQL_ADDR and CDC_MYSQL_USER are required")
}
if cfg.CDC.CDCPasswordRequired() && cfg.MySQL.CDCPassword == "" {
return Config{}, errors.New("CDC_MYSQL_PASSWORD is required")
}
if cfg.Redis.Enabled && cfg.Redis.Addr == "" {
return Config{}, errors.New("REDIS_ADDR is required when REDIS_ENABLED=true")
}
if cfg.CDC.ServerID == 0 {
return Config{}, errors.New("CDC_SERVER_ID must be greater than 0")
}
if cfg.CDC.StartMode != "checkpoint" && cfg.CDC.StartMode != "current" {
return Config{}, fmt.Errorf("unsupported CDC_START_MODE=%s", cfg.CDC.StartMode)
}
if len(cfg.Dashboard.SysOrigins) == 0 {
return Config{}, errors.New("DASHBOARD_SYS_ORIGINS is empty")
}
if len(cfg.Dashboard.StatTimezones) == 0 {
return Config{}, errors.New("DASHBOARD_STAT_TIMEZONES is empty")
}
return cfg, nil
}
func (c CDCConfig) CDCPasswordRequired() bool {
return true
}
func defaultTables() string {
return strings.Join([]string{
"likei.user_base_info",
"likei.order_purchase_history",
"likei.order_user_purchase_pay",
"likei.game_lucky_gift_count",
"likei.game_lucky_box_count",
"likei.baishun_order_idempotency",
"likei.game_open_callback_log",
"likei.baishun_game_catalog",
"likei.lingxian_game_catalog",
"likei.sys_game_list_config",
"likei_wallet.wallet_gold_asset_record_1",
"likei_wallet.wallet_gold_asset_record_2",
"likei_wallet.wallet_gold_asset_record_3",
"likei_wallet.wallet_gold_asset_record_4",
"likei_wallet.wallet_gold_asset_record_5",
"likei_wallet.wallet_gold_asset_record_6",
"likei_wallet.wallet_gold_asset_record_7",
"likei_wallet.wallet_gold_asset_record_8",
"likei_wallet.wallet_gold_asset_record_9",
"likei_wallet.wallet_gold_asset_record_10",
"likei_wallet.wallet_gold_asset_record_11",
"likei_wallet.wallet_gold_asset_record_12",
"likei_wallet.user_freight_balance_running_water",
"likei_wallet.user_salary_account_running_water",
"likei_wallet.user_salary_diamond_running_water",
}, ",")
}
func env(key string, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func envBool(key string, fallback bool) bool {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return fallback
}
return parsed
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}
func envDuration(key string, fallback time.Duration) time.Duration {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err == nil {
return parsed
}
seconds, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return time.Duration(seconds) * time.Second
}
func csv(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
items := strings.Split(value, ",")
out := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, exists := seen[item]; exists {
continue
}
seen[item] = struct{}{}
out = append(out, item)
}
return out
}
func RedactedDSN(dsn string) string {
parsed, err := url.Parse("mysql://" + dsn)
if err != nil || parsed.User == nil {
return "mysql://***"
}
parsed.User = url.UserPassword(parsed.User.Username(), "***")
return strings.TrimPrefix(parsed.String(), "mysql://")
}