368 lines
11 KiB
Go
368 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
MySQL MySQLConfig
|
|
Mongo MongoConfig
|
|
Redis RedisConfig
|
|
CDC CDCConfig
|
|
Dashboard DashboardConfig
|
|
Recharge RechargeReconcileConfig
|
|
Gift GiftReconcileConfig
|
|
Game GameReconcileConfig
|
|
User UserReconcileConfig
|
|
Active ActiveReconcileConfig
|
|
Health HealthConfig
|
|
}
|
|
|
|
type MySQLConfig struct {
|
|
AppDSN string
|
|
CDCAddr string
|
|
CDCUser string
|
|
CDCPassword string
|
|
Schema string
|
|
WalletSchema string
|
|
MaxOpenConns int
|
|
MaxIdleConns int
|
|
}
|
|
|
|
type MongoConfig struct {
|
|
Enabled bool
|
|
URI string
|
|
Database string
|
|
}
|
|
|
|
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
|
|
BatchSize int
|
|
BackfillLockWait time.Duration
|
|
MasterPollEvery time.Duration
|
|
}
|
|
|
|
type DashboardConfig struct {
|
|
SysOrigins []string
|
|
StatTimezones []string
|
|
StorageTimezone string
|
|
RedisKeyPrefix string
|
|
RealtimeRedisTTL time.Duration
|
|
UnknownCountryCode string
|
|
CleanupInterval time.Duration
|
|
DedupRetention time.Duration
|
|
UserMetricDays int
|
|
CleanupBatchSize int
|
|
}
|
|
|
|
type RechargeReconcileConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
CurrentDayLag time.Duration
|
|
BatchLimit int
|
|
}
|
|
|
|
type GiftReconcileConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
CurrentDayLag time.Duration
|
|
BatchLimit int
|
|
}
|
|
|
|
type GameReconcileConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
CurrentDayLag time.Duration
|
|
}
|
|
|
|
type UserReconcileConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
LookbackDays int
|
|
CurrentDayLag time.Duration
|
|
BatchLimit int
|
|
CreateTimeDisplayOffset time.Duration
|
|
}
|
|
|
|
type ActiveReconcileConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
LookbackDays int
|
|
BatchLimit int
|
|
}
|
|
|
|
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),
|
|
},
|
|
Mongo: MongoConfig{
|
|
Enabled: envBool("MONGO_ENABLED", false),
|
|
URI: env("MONGO_URI", env("LIKEI_MONGO_URI", "")),
|
|
Database: env("MONGO_DATABASE", env("LIKEI_MONGO_DB", "")),
|
|
},
|
|
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),
|
|
BatchSize: envInt("CDC_BATCH_SIZE", 1000),
|
|
BackfillLockWait: envDuration("CDC_BACKFILL_LOCK_WAIT", time.Second),
|
|
MasterPollEvery: envDuration("CDC_MASTER_POSITION_POLL_INTERVAL", time.Minute),
|
|
},
|
|
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"),
|
|
CleanupInterval: envDuration("DASHBOARD_CLEANUP_INTERVAL", 30*time.Minute),
|
|
DedupRetention: envDuration("DASHBOARD_DEDUP_RETENTION", 72*time.Hour),
|
|
UserMetricDays: envInt("DASHBOARD_USER_METRIC_RETENTION_DAYS", 120),
|
|
CleanupBatchSize: envInt("DASHBOARD_CLEANUP_BATCH_SIZE", 5000),
|
|
},
|
|
Recharge: RechargeReconcileConfig{
|
|
Enabled: envBool("DASHBOARD_RECHARGE_RECONCILE_ENABLED", true),
|
|
Interval: envDuration("DASHBOARD_RECHARGE_RECONCILE_INTERVAL", 10*time.Minute),
|
|
CurrentDayLag: envDuration("DASHBOARD_RECHARGE_RECONCILE_CURRENT_DAY_LAG", 15*time.Minute),
|
|
BatchLimit: envInt("DASHBOARD_RECHARGE_RECONCILE_BATCH_LIMIT", 5000),
|
|
},
|
|
Gift: GiftReconcileConfig{
|
|
Enabled: envBool("DASHBOARD_GIFT_RECONCILE_ENABLED", true),
|
|
Interval: envDuration("DASHBOARD_GIFT_RECONCILE_INTERVAL", 10*time.Minute),
|
|
CurrentDayLag: envDuration("DASHBOARD_GIFT_RECONCILE_CURRENT_DAY_LAG", 5*time.Minute),
|
|
BatchLimit: envInt("DASHBOARD_GIFT_RECONCILE_BATCH_LIMIT", 50000),
|
|
},
|
|
Game: GameReconcileConfig{
|
|
Enabled: envBool("DASHBOARD_GAME_RECONCILE_ENABLED", true),
|
|
Interval: envDuration("DASHBOARD_GAME_RECONCILE_INTERVAL", 10*time.Minute),
|
|
CurrentDayLag: envDuration("DASHBOARD_GAME_RECONCILE_CURRENT_DAY_LAG", 5*time.Minute),
|
|
},
|
|
User: UserReconcileConfig{
|
|
Enabled: envBool("DASHBOARD_USER_RECONCILE_ENABLED", true),
|
|
Interval: envDuration("DASHBOARD_USER_RECONCILE_INTERVAL", 10*time.Minute),
|
|
LookbackDays: envInt("DASHBOARD_USER_RECONCILE_LOOKBACK_DAYS", 3),
|
|
CurrentDayLag: envDuration("DASHBOARD_USER_RECONCILE_CURRENT_DAY_LAG", time.Minute),
|
|
BatchLimit: envInt("DASHBOARD_USER_RECONCILE_BATCH_LIMIT", 10000),
|
|
CreateTimeDisplayOffset: envDuration("DASHBOARD_USER_CREATE_TIME_DISPLAY_OFFSET", 8*time.Hour),
|
|
},
|
|
Active: ActiveReconcileConfig{
|
|
Enabled: envBool("DASHBOARD_ACTIVE_RECONCILE_ENABLED", false),
|
|
Interval: envDuration("DASHBOARD_ACTIVE_RECONCILE_INTERVAL", 10*time.Minute),
|
|
LookbackDays: envInt("DASHBOARD_ACTIVE_RECONCILE_LOOKBACK_DAYS", 35),
|
|
BatchLimit: envInt("DASHBOARD_ACTIVE_RECONCILE_BATCH_LIMIT", 200000),
|
|
},
|
|
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.Mongo.Enabled && cfg.Mongo.URI == "" {
|
|
return Config{}, errors.New("MONGO_URI or LIKEI_MONGO_URI is required when MONGO_ENABLED=true")
|
|
}
|
|
if cfg.Mongo.Enabled && cfg.Mongo.Database == "" {
|
|
return Config{}, errors.New("MONGO_DATABASE or LIKEI_MONGO_DB is required when MONGO_ENABLED=true")
|
|
}
|
|
if cfg.CDC.ServerID == 0 {
|
|
return Config{}, errors.New("CDC_SERVER_ID must be greater than 0")
|
|
}
|
|
if cfg.CDC.BatchSize <= 0 {
|
|
cfg.CDC.BatchSize = 1000
|
|
}
|
|
if cfg.Dashboard.CleanupBatchSize <= 0 {
|
|
cfg.Dashboard.CleanupBatchSize = 5000
|
|
}
|
|
if cfg.Recharge.BatchLimit <= 0 {
|
|
cfg.Recharge.BatchLimit = 5000
|
|
}
|
|
if cfg.Gift.BatchLimit <= 0 {
|
|
cfg.Gift.BatchLimit = 50000
|
|
}
|
|
if cfg.User.LookbackDays <= 0 {
|
|
cfg.User.LookbackDays = 3
|
|
}
|
|
if cfg.User.BatchLimit <= 0 {
|
|
cfg.User.BatchLimit = 10000
|
|
}
|
|
if cfg.Active.LookbackDays <= 0 {
|
|
cfg.Active.LookbackDays = 35
|
|
}
|
|
if cfg.Active.BatchLimit <= 0 {
|
|
cfg.Active.BatchLimit = 200000
|
|
}
|
|
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://")
|
|
}
|