1077 lines
42 KiB
Go
1077 lines
42 KiB
Go
package config
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/platform/logging"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
type Config struct {
|
||
ServiceName string `yaml:"service_name"`
|
||
NodeID string `yaml:"node_id"`
|
||
Environment string `yaml:"environment"`
|
||
Log logging.Config `yaml:"log"`
|
||
HTTPAddr string `yaml:"http_addr"`
|
||
MySQLDSN string `yaml:"mysql_dsn"`
|
||
UserMySQLDSN string `yaml:"user_mysql_dsn"`
|
||
WalletMySQLDSN string `yaml:"wallet_mysql_dsn"`
|
||
RobotProfileSource RobotProfileSourceConfig `yaml:"robot_profile_source"`
|
||
MoneyRegionSources []MoneyRegionSourceConfig `yaml:"money_region_sources"`
|
||
FinanceBillSources []FinanceBillSourceConfig `yaml:"finance_bill_sources"`
|
||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||
Migrations MigrationConfig `yaml:"migrations"`
|
||
JWTSecret string `yaml:"jwt_secret"`
|
||
AccessTokenTTL time.Duration `yaml:"access_token_ttl"`
|
||
RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl"`
|
||
CORSAllowedOrigins []string `yaml:"cors_allowed_origins"`
|
||
RefreshCookieSecure bool `yaml:"refresh_cookie_secure"`
|
||
RefreshCookieSameSite string `yaml:"refresh_cookie_same_site"`
|
||
Bootstrap BootstrapConfig `yaml:"bootstrap"`
|
||
BootstrapPassword string `yaml:"bootstrap_password"`
|
||
UserService UserServiceConfig `yaml:"user_service"`
|
||
TencentCOS TencentCOSConfig `yaml:"tencent-cos"`
|
||
Redis RedisConfig `yaml:"redis"`
|
||
Jobs JobsConfig `yaml:"jobs"`
|
||
WalletService WalletServiceConfig `yaml:"wallet_service"`
|
||
RoomService RoomServiceConfig `yaml:"room_service"`
|
||
RobotService RobotServiceConfig `yaml:"robot_service"`
|
||
ActivityService ActivityServiceConfig `yaml:"activity_service"`
|
||
GameService GameServiceConfig `yaml:"game_service"`
|
||
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
|
||
DashboardExternalSources []DashboardExternalSourceConfig `yaml:"dashboard_external_sources"`
|
||
LegacyCoinSellerRechargeSources []LegacyCoinSellerRechargeSourceConfig `yaml:"legacy_coin_seller_recharge_sources"`
|
||
FinanceNotifications FinanceNotificationsConfig `yaml:"finance_notifications"`
|
||
}
|
||
|
||
type MigrationConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Dir string `yaml:"dir"`
|
||
AllowChecksumRepair bool `yaml:"allow_checksum_repair"`
|
||
}
|
||
|
||
type UserServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type WalletServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type RoomServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type RobotServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type ActivityServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type GameServiceConfig struct {
|
||
Addr string `yaml:"addr"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type StatisticsServiceConfig struct {
|
||
BaseURL string `yaml:"base_url"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type DashboardExternalSourceConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
SourceType string `yaml:"type"`
|
||
AppCode string `yaml:"app_code"`
|
||
AppName string `yaml:"app_name"`
|
||
SysOrigin string `yaml:"sys_origin"`
|
||
MySQLDSN string `yaml:"mysql_dsn"`
|
||
LegacyMySQLDSN string `yaml:"legacy_mysql_dsn"`
|
||
LegacyWalletDatabase string `yaml:"legacy_wallet_database"`
|
||
MongoURI string `yaml:"mongo_uri"`
|
||
MongoDatabase string `yaml:"mongo_database"`
|
||
BaseURL string `yaml:"base_url"`
|
||
OverviewPath string `yaml:"overview_path"`
|
||
StatTimezone string `yaml:"stat_timezone"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
// LegacyCoinSellerRechargeSourceConfig 是 Aslan/Yumi 等独立账套的币商进货写入源。
|
||
// 它与 dashboard_external_sources 分离,避免把只读报表连接误当成可写钱包连接。
|
||
type LegacyCoinSellerRechargeSourceConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AppCode string `yaml:"app_code"`
|
||
AppName string `yaml:"app_name"`
|
||
SysOrigin string `yaml:"sys_origin"`
|
||
AppMySQLDSN string `yaml:"app_mysql_dsn"`
|
||
WalletMySQLDSN string `yaml:"wallet_mysql_dsn"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type FinanceNotificationsConfig struct {
|
||
DingTalk DingTalkRobotConfig `yaml:"dingtalk"`
|
||
}
|
||
|
||
type DingTalkRobotConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
WebhookURL string `yaml:"webhook_url"`
|
||
Secret string `yaml:"secret"`
|
||
AtMobiles []string `yaml:"at_mobiles"`
|
||
AtAll bool `yaml:"at_all"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type RobotProfileSourceConfig struct {
|
||
MySQLDSN string `yaml:"mysql_dsn"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
type MoneyRegionSourceConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AppCode string `yaml:"app_code"`
|
||
AppName string `yaml:"app_name"`
|
||
SysOrigin string `yaml:"sys_origin"`
|
||
MongoURI string `yaml:"mongo_uri"`
|
||
MongoDatabase string `yaml:"mongo_database"`
|
||
MongoCollection string `yaml:"mongo_collection"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
// FinanceBillSourceConfig 描述 legacy App(Yumi/Aslan 等 likei 平台)的充值账单外部 Mongo 源;
|
||
// 财务系统 APP 充值详情按 app_code 命中后直接读 in_app_purchase_details,不经过 wallet-service。
|
||
type FinanceBillSourceConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AppCode string `yaml:"app_code"`
|
||
AppName string `yaml:"app_name"`
|
||
SysOrigin string `yaml:"sys_origin"`
|
||
MongoURI string `yaml:"mongo_uri"`
|
||
MongoDatabase string `yaml:"mongo_database"`
|
||
MongoCollection string `yaml:"mongo_collection"`
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
// Google 实付同步(可选):配置该 App 在 Play Console 的包名与服务账号后,
|
||
// 财务页“查询谷歌实付”按钮即可对 legacy 谷歌账单拉取实付币种/总额/税/净收入。
|
||
// 服务账号需要对应 Play Console 的“查看财务数据”权限;JSON 优先于文件路径。
|
||
GooglePackageName string `yaml:"google_package_name"`
|
||
GoogleServiceAccountJSON string `yaml:"google_service_account_json"`
|
||
GoogleServiceAccountFile string `yaml:"google_service_account_file"`
|
||
}
|
||
|
||
type TencentCOSConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
SecretID string `yaml:"secret-id"`
|
||
SecretKey string `yaml:"secret-key"`
|
||
BucketName string `yaml:"bucket-name"`
|
||
Region string `yaml:"region"`
|
||
AccessURL string `yaml:"access-url"`
|
||
ObjectPrefix string `yaml:"object-prefix"`
|
||
}
|
||
|
||
type BootstrapConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
SeedDemoData bool `yaml:"seed_demo_data"`
|
||
Username string `yaml:"username"`
|
||
Name string `yaml:"name"`
|
||
Team string `yaml:"team"`
|
||
Password string `yaml:"password"`
|
||
}
|
||
|
||
type RedisConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Addr string `yaml:"addr"`
|
||
Password string `yaml:"password"`
|
||
DB int `yaml:"db"`
|
||
KeyPrefix string `yaml:"key_prefix"`
|
||
}
|
||
|
||
type JobsConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
WorkerInterval time.Duration `yaml:"worker_interval"`
|
||
LeaseTTL time.Duration `yaml:"lease_ttl"`
|
||
MaxAttempts int `yaml:"max_attempts"`
|
||
ArtifactDir string `yaml:"artifact_dir"`
|
||
}
|
||
|
||
func Default() Config {
|
||
return Config{
|
||
ServiceName: "admin-server",
|
||
NodeID: "admin-local",
|
||
Environment: "local",
|
||
Log: logging.DefaultConfig(),
|
||
HTTPAddr: ":13100",
|
||
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC",
|
||
UserMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC",
|
||
WalletMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC",
|
||
RobotProfileSource: RobotProfileSourceConfig{
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
MoneyRegionSources: []MoneyRegionSourceConfig{
|
||
{
|
||
Enabled: false,
|
||
AppCode: "yumi",
|
||
AppName: "Yumi",
|
||
SysOrigin: "LIKEI",
|
||
MongoDatabase: "tarab_all",
|
||
MongoCollection: "sys_region_config",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
{
|
||
Enabled: false,
|
||
AppCode: "aslan",
|
||
AppName: "Aslan",
|
||
SysOrigin: "ATYOU",
|
||
MongoDatabase: "tarab_all",
|
||
MongoCollection: "sys_region_config",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
},
|
||
FinanceBillSources: []FinanceBillSourceConfig{
|
||
{
|
||
Enabled: false,
|
||
AppCode: "yumi",
|
||
AppName: "Yumi",
|
||
SysOrigin: "LIKEI",
|
||
MongoDatabase: "tarab_all",
|
||
MongoCollection: "in_app_purchase_details",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
{
|
||
Enabled: false,
|
||
AppCode: "aslan",
|
||
AppName: "Aslan",
|
||
// Aslan(likei-services 项目)与 Yumi 同一套平台代码,但独立 Mongo 集群;业务库是 atyou。
|
||
SysOrigin: "ATYOU",
|
||
MongoDatabase: "atyou",
|
||
MongoCollection: "in_app_purchase_details",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
},
|
||
MySQLAutoMigrate: true,
|
||
Migrations: MigrationConfig{
|
||
Enabled: true,
|
||
Dir: "migrations",
|
||
AllowChecksumRepair: true,
|
||
},
|
||
JWTSecret: "dev-shared-secret",
|
||
AccessTokenTTL: 12 * time.Hour,
|
||
RefreshTokenTTL: 7 * 24 * time.Hour,
|
||
CORSAllowedOrigins: []string{
|
||
"http://localhost:7001",
|
||
"http://127.0.0.1:7001",
|
||
},
|
||
RefreshCookieSecure: false,
|
||
RefreshCookieSameSite: "lax",
|
||
Bootstrap: BootstrapConfig{
|
||
Enabled: true,
|
||
SeedDemoData: true,
|
||
Username: "admin",
|
||
Name: "admin",
|
||
Team: "运营团队",
|
||
Password: "admin123",
|
||
},
|
||
BootstrapPassword: "admin123",
|
||
UserService: UserServiceConfig{
|
||
Addr: "127.0.0.1:13005",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
WalletService: WalletServiceConfig{
|
||
Addr: "127.0.0.1:13004",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
RoomService: RoomServiceConfig{
|
||
Addr: "127.0.0.1:13001",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
RobotService: RobotServiceConfig{
|
||
Addr: "127.0.0.1:13011",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
ActivityService: ActivityServiceConfig{
|
||
Addr: "127.0.0.1:13006",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
GameService: GameServiceConfig{
|
||
Addr: "127.0.0.1:13008",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
StatisticsService: StatisticsServiceConfig{
|
||
BaseURL: "http://127.0.0.1:13010",
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
DashboardExternalSources: []DashboardExternalSourceConfig{
|
||
{
|
||
Enabled: false,
|
||
SourceType: "mysql",
|
||
AppCode: "yumi",
|
||
AppName: "Yumi",
|
||
SysOrigin: "LIKEI",
|
||
StatTimezone: "Asia/Riyadh",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
{
|
||
Enabled: false,
|
||
SourceType: "aslan_mongo",
|
||
AppCode: "aslan",
|
||
AppName: "Aslan",
|
||
SysOrigin: "ATYOU",
|
||
MongoDatabase: "test",
|
||
StatTimezone: "Asia/Shanghai",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
},
|
||
LegacyCoinSellerRechargeSources: []LegacyCoinSellerRechargeSourceConfig{
|
||
{
|
||
Enabled: false,
|
||
AppCode: "yumi",
|
||
AppName: "Yumi",
|
||
SysOrigin: "LIKEI",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
{
|
||
Enabled: false,
|
||
AppCode: "aslan",
|
||
AppName: "Aslan",
|
||
SysOrigin: "ATYOU",
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
},
|
||
FinanceNotifications: FinanceNotificationsConfig{
|
||
DingTalk: DingTalkRobotConfig{
|
||
Enabled: true,
|
||
RequestTimeout: 3 * time.Second,
|
||
},
|
||
},
|
||
TencentCOS: TencentCOSConfig{
|
||
Enabled: false,
|
||
ObjectPrefix: "admin",
|
||
},
|
||
Redis: RedisConfig{
|
||
Enabled: true,
|
||
Addr: "127.0.0.1:13379",
|
||
KeyPrefix: "admin:",
|
||
},
|
||
Jobs: JobsConfig{
|
||
Enabled: true,
|
||
WorkerInterval: 5 * time.Second,
|
||
LeaseTTL: 2 * time.Minute,
|
||
MaxAttempts: 3,
|
||
ArtifactDir: "storage/exports",
|
||
},
|
||
}
|
||
}
|
||
|
||
func Load(path string) (Config, error) {
|
||
cfg := Default()
|
||
if path == "" {
|
||
return cfg, nil
|
||
}
|
||
|
||
body, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return Config{}, err
|
||
}
|
||
|
||
if err := yaml.Unmarshal(body, &cfg); err != nil {
|
||
return Config{}, err
|
||
}
|
||
|
||
cfg.Normalize()
|
||
if err := cfg.Validate(); err != nil {
|
||
return Config{}, err
|
||
}
|
||
|
||
return cfg, nil
|
||
}
|
||
|
||
func (cfg *Config) Normalize() {
|
||
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
|
||
if cfg.Environment == "" {
|
||
cfg.Environment = "local"
|
||
}
|
||
if cfg.Log.Level == "" {
|
||
cfg.Log.Level = "info"
|
||
}
|
||
if cfg.Log.Format == "" {
|
||
cfg.Log.Format = "json"
|
||
}
|
||
if cfg.Log.MaxPayloadBytes <= 0 {
|
||
cfg.Log.MaxPayloadBytes = 2048
|
||
}
|
||
cfg.RefreshCookieSameSite = strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite))
|
||
if cfg.RefreshCookieSameSite == "" {
|
||
cfg.RefreshCookieSameSite = "lax"
|
||
}
|
||
cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
|
||
cfg.NodeID = strings.TrimSpace(cfg.NodeID)
|
||
if cfg.NodeID == "" {
|
||
cfg.NodeID = cfg.ServiceName
|
||
}
|
||
if cfg.Bootstrap.Password == "" && cfg.BootstrapPassword != "" {
|
||
cfg.Bootstrap.Password = cfg.BootstrapPassword
|
||
}
|
||
if cfg.Bootstrap.Username == "" {
|
||
cfg.Bootstrap.Username = "admin"
|
||
}
|
||
if cfg.Bootstrap.Name == "" {
|
||
cfg.Bootstrap.Name = cfg.Bootstrap.Username
|
||
}
|
||
if cfg.Bootstrap.Team == "" {
|
||
cfg.Bootstrap.Team = "运营团队"
|
||
}
|
||
if cfg.Jobs.LeaseTTL <= 0 {
|
||
cfg.Jobs.LeaseTTL = 2 * time.Minute
|
||
}
|
||
if cfg.Jobs.MaxAttempts <= 0 {
|
||
cfg.Jobs.MaxAttempts = 3
|
||
}
|
||
if cfg.Jobs.ArtifactDir == "" {
|
||
cfg.Jobs.ArtifactDir = "storage/exports"
|
||
}
|
||
cfg.UserService.Addr = strings.TrimSpace(cfg.UserService.Addr)
|
||
if cfg.UserService.RequestTimeout <= 0 {
|
||
cfg.UserService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.WalletService.Addr = strings.TrimSpace(cfg.WalletService.Addr)
|
||
if cfg.WalletService.RequestTimeout <= 0 {
|
||
cfg.WalletService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
|
||
if cfg.RoomService.Addr == "" {
|
||
cfg.RoomService.Addr = "127.0.0.1:13001"
|
||
}
|
||
if cfg.RoomService.RequestTimeout <= 0 {
|
||
cfg.RoomService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.RobotService.Addr = strings.TrimSpace(cfg.RobotService.Addr)
|
||
if cfg.RobotService.Addr == "" {
|
||
cfg.RobotService.Addr = "127.0.0.1:13011"
|
||
}
|
||
if cfg.RobotService.RequestTimeout <= 0 {
|
||
cfg.RobotService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
|
||
if cfg.ActivityService.RequestTimeout <= 0 {
|
||
cfg.ActivityService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.GameService.Addr = strings.TrimSpace(cfg.GameService.Addr)
|
||
if cfg.GameService.Addr == "" {
|
||
cfg.GameService.Addr = "127.0.0.1:13008"
|
||
}
|
||
if cfg.GameService.RequestTimeout <= 0 {
|
||
cfg.GameService.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.StatisticsService.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.StatisticsService.BaseURL), "/")
|
||
if cfg.StatisticsService.BaseURL == "" {
|
||
cfg.StatisticsService.BaseURL = "http://127.0.0.1:13010"
|
||
}
|
||
if cfg.StatisticsService.RequestTimeout <= 0 {
|
||
cfg.StatisticsService.RequestTimeout = 3 * time.Second
|
||
}
|
||
for index := range cfg.DashboardExternalSources {
|
||
source := &cfg.DashboardExternalSources[index]
|
||
// 外接 App 大屏的数据源类型在配置层归一化;查询层只根据类型读取对应的外部聚合事实,不回退到 hyapp statistics-service。
|
||
source.SourceType = strings.ToLower(strings.TrimSpace(source.SourceType))
|
||
source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
|
||
source.AppName = strings.TrimSpace(source.AppName)
|
||
source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
|
||
source.MySQLDSN = strings.TrimSpace(source.MySQLDSN)
|
||
source.LegacyMySQLDSN = strings.TrimSpace(source.LegacyMySQLDSN)
|
||
source.LegacyWalletDatabase = strings.Trim(strings.TrimSpace(source.LegacyWalletDatabase), "/")
|
||
source.MongoURI = strings.TrimSpace(source.MongoURI)
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
|
||
source.BaseURL = strings.TrimRight(strings.TrimSpace(source.BaseURL), "/")
|
||
source.OverviewPath = "/" + strings.TrimLeft(strings.TrimSpace(source.OverviewPath), "/")
|
||
if source.OverviewPath == "/" {
|
||
source.OverviewPath = ""
|
||
}
|
||
if source.SourceType == "" {
|
||
if source.MongoURI != "" || source.MongoDatabase != "" {
|
||
source.SourceType = "aslan_mongo"
|
||
} else if source.BaseURL != "" || source.OverviewPath != "" {
|
||
source.SourceType = "aslan_http"
|
||
} else {
|
||
source.SourceType = "mysql"
|
||
}
|
||
}
|
||
if source.SourceType == "aslan_mongo" && source.MongoDatabase == "" {
|
||
source.MongoDatabase = "test"
|
||
}
|
||
if source.SourceType == "aslan_mongo" && source.LegacyWalletDatabase == "" && source.MongoDatabase != "" {
|
||
// Aslan legacy RDS 把用户资料放在 atyou,把金币代理钱包流水放在 atyou_wallet;默认按主库名派生。
|
||
source.LegacyWalletDatabase = source.MongoDatabase + "_wallet"
|
||
}
|
||
source.StatTimezone = strings.TrimSpace(source.StatTimezone)
|
||
if source.StatTimezone == "" {
|
||
if source.SourceType == "aslan_http" || source.SourceType == "aslan_mongo" {
|
||
source.StatTimezone = "Asia/Shanghai"
|
||
} else {
|
||
source.StatTimezone = "Asia/Riyadh"
|
||
}
|
||
}
|
||
if source.RequestTimeout <= 0 {
|
||
source.RequestTimeout = 5 * time.Second
|
||
}
|
||
}
|
||
cfg.applyDashboardExternalSourceEnvOverrides()
|
||
for index := range cfg.LegacyCoinSellerRechargeSources {
|
||
source := &cfg.LegacyCoinSellerRechargeSources[index]
|
||
source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
|
||
source.AppName = strings.TrimSpace(source.AppName)
|
||
source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
|
||
source.AppMySQLDSN = strings.TrimSpace(source.AppMySQLDSN)
|
||
source.WalletMySQLDSN = strings.TrimSpace(source.WalletMySQLDSN)
|
||
if source.RequestTimeout <= 0 {
|
||
source.RequestTimeout = 5 * time.Second
|
||
}
|
||
}
|
||
cfg.applyLegacyCoinSellerRechargeSourceEnvOverrides()
|
||
cfg.applyFinanceNotificationEnvOverrides()
|
||
cfg.FinanceNotifications.DingTalk.WebhookURL = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.WebhookURL)
|
||
cfg.FinanceNotifications.DingTalk.Secret = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.Secret)
|
||
cfg.FinanceNotifications.DingTalk.AtMobiles = compactStrings(cfg.FinanceNotifications.DingTalk.AtMobiles)
|
||
if cfg.FinanceNotifications.DingTalk.RequestTimeout <= 0 {
|
||
cfg.FinanceNotifications.DingTalk.RequestTimeout = 3 * time.Second
|
||
}
|
||
cfg.RobotProfileSource.MySQLDSN = strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN)
|
||
if cfg.RobotProfileSource.RequestTimeout <= 0 {
|
||
cfg.RobotProfileSource.RequestTimeout = 5 * time.Second
|
||
}
|
||
for index := range cfg.MoneyRegionSources {
|
||
source := &cfg.MoneyRegionSources[index]
|
||
// legacy 区域来源只服务财务范围目录;这里先规整配置,handler 层只处理业务过滤,不重复处理大小写和默认集合名。
|
||
source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
|
||
source.AppName = strings.TrimSpace(source.AppName)
|
||
source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
|
||
source.MongoURI = strings.TrimSpace(source.MongoURI)
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
|
||
if source.MongoDatabase == "" {
|
||
source.MongoDatabase = "tarab_all"
|
||
}
|
||
source.MongoCollection = strings.TrimSpace(source.MongoCollection)
|
||
if source.MongoCollection == "" {
|
||
source.MongoCollection = "sys_region_config"
|
||
}
|
||
if source.RequestTimeout <= 0 {
|
||
source.RequestTimeout = 5 * time.Second
|
||
}
|
||
}
|
||
cfg.applyMoneyRegionSourceEnvOverrides()
|
||
for index := range cfg.FinanceBillSources {
|
||
source := &cfg.FinanceBillSources[index]
|
||
// legacy 账单源只服务财务充值明细;配置层统一大小写和默认集合,handler 只按 app_code 命中。
|
||
source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
|
||
source.AppName = strings.TrimSpace(source.AppName)
|
||
source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin))
|
||
source.MongoURI = strings.TrimSpace(source.MongoURI)
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/")
|
||
if source.MongoDatabase == "" {
|
||
// 线上 likei Mongo(10.2.21.9)的账单和区域目录都在 tarab_all 库,与 money_region_sources 保持一致。
|
||
source.MongoDatabase = "tarab_all"
|
||
}
|
||
source.MongoCollection = strings.TrimSpace(source.MongoCollection)
|
||
if source.MongoCollection == "" {
|
||
source.MongoCollection = "in_app_purchase_details"
|
||
}
|
||
source.GooglePackageName = strings.TrimSpace(source.GooglePackageName)
|
||
source.GoogleServiceAccountJSON = strings.TrimSpace(source.GoogleServiceAccountJSON)
|
||
source.GoogleServiceAccountFile = strings.TrimSpace(source.GoogleServiceAccountFile)
|
||
if source.RequestTimeout <= 0 {
|
||
source.RequestTimeout = 5 * time.Second
|
||
}
|
||
}
|
||
cfg.applyFinanceBillSourceEnvOverrides()
|
||
cfg.TencentCOS.SecretID = strings.TrimSpace(cfg.TencentCOS.SecretID)
|
||
cfg.TencentCOS.SecretKey = strings.TrimSpace(cfg.TencentCOS.SecretKey)
|
||
cfg.TencentCOS.BucketName = strings.TrimSpace(cfg.TencentCOS.BucketName)
|
||
cfg.TencentCOS.Region = strings.TrimSpace(cfg.TencentCOS.Region)
|
||
cfg.TencentCOS.AccessURL = strings.TrimRight(strings.TrimSpace(cfg.TencentCOS.AccessURL), "/")
|
||
cfg.TencentCOS.ObjectPrefix = strings.Trim(strings.TrimSpace(cfg.TencentCOS.ObjectPrefix), "/")
|
||
if cfg.TencentCOS.ObjectPrefix == "" {
|
||
cfg.TencentCOS.ObjectPrefix = "admin"
|
||
}
|
||
}
|
||
|
||
func (cfg Config) Validate() error {
|
||
if !validEnvironment(cfg.Environment) {
|
||
return errors.New("environment must be one of local, dev, staging, prod")
|
||
}
|
||
if strings.TrimSpace(cfg.ServiceName) == "" {
|
||
return errors.New("service_name is required")
|
||
}
|
||
if strings.TrimSpace(cfg.NodeID) == "" {
|
||
return errors.New("node_id is required")
|
||
}
|
||
if strings.TrimSpace(cfg.HTTPAddr) == "" {
|
||
return errors.New("http_addr is required")
|
||
}
|
||
if strings.TrimSpace(cfg.MySQLDSN) == "" {
|
||
return errors.New("mysql_dsn is required")
|
||
}
|
||
if strings.TrimSpace(cfg.UserMySQLDSN) == "" {
|
||
return errors.New("user_mysql_dsn is required")
|
||
}
|
||
if strings.TrimSpace(cfg.WalletMySQLDSN) == "" {
|
||
return errors.New("wallet_mysql_dsn is required")
|
||
}
|
||
if cfg.Migrations.Enabled && strings.TrimSpace(cfg.Migrations.Dir) == "" {
|
||
return errors.New("migrations.dir is required when migrations are enabled")
|
||
}
|
||
if isLockedEnvironment(cfg.Environment) && cfg.Migrations.AllowChecksumRepair {
|
||
return errors.New("migrations.allow_checksum_repair must be false outside local/dev")
|
||
}
|
||
if strings.TrimSpace(cfg.JWTSecret) == "" {
|
||
return errors.New("jwt_secret is required")
|
||
}
|
||
if cfg.AccessTokenTTL <= 0 {
|
||
return errors.New("access_token_ttl must be greater than 0")
|
||
}
|
||
if cfg.RefreshTokenTTL <= 0 {
|
||
return errors.New("refresh_token_ttl must be greater than 0")
|
||
}
|
||
switch strings.ToLower(strings.TrimSpace(cfg.RefreshCookieSameSite)) {
|
||
case "lax", "strict", "none":
|
||
default:
|
||
return errors.New("refresh_cookie_same_site must be one of lax, strict, none")
|
||
}
|
||
if strings.EqualFold(strings.TrimSpace(cfg.RefreshCookieSameSite), "none") && !cfg.RefreshCookieSecure {
|
||
return errors.New("refresh_cookie_secure must be true when refresh_cookie_same_site is none")
|
||
}
|
||
if isLockedEnvironment(cfg.Environment) {
|
||
if cfg.JWTSecret == "dev-shared-secret" || len(cfg.JWTSecret) < 32 {
|
||
return errors.New("jwt_secret must be a non-default value with at least 32 characters outside local/dev")
|
||
}
|
||
if cfg.MySQLAutoMigrate {
|
||
return errors.New("mysql_auto_migrate must be false outside local/dev")
|
||
}
|
||
if cfg.Bootstrap.Enabled && cfg.Bootstrap.Password == "admin123" {
|
||
return errors.New("bootstrap.password must not use the default value outside local/dev")
|
||
}
|
||
}
|
||
for _, source := range cfg.LegacyCoinSellerRechargeSources {
|
||
if !source.Enabled {
|
||
continue
|
||
}
|
||
if strings.TrimSpace(source.AppCode) == "" {
|
||
return errors.New("legacy_coin_seller_recharge_sources app_code is required when enabled")
|
||
}
|
||
if strings.TrimSpace(source.SysOrigin) == "" {
|
||
return errors.New("legacy_coin_seller_recharge_sources sys_origin is required when enabled")
|
||
}
|
||
if strings.TrimSpace(source.AppMySQLDSN) == "" || strings.TrimSpace(source.WalletMySQLDSN) == "" {
|
||
return errors.New("legacy_coin_seller_recharge_sources app_mysql_dsn and wallet_mysql_dsn are required when enabled")
|
||
}
|
||
}
|
||
if len(cfg.CORSAllowedOrigins) == 0 {
|
||
return errors.New("cors_allowed_origins must not be empty")
|
||
}
|
||
if cfg.Bootstrap.Enabled && strings.TrimSpace(cfg.Bootstrap.Password) == "" {
|
||
return errors.New("bootstrap.password is required when bootstrap is enabled")
|
||
}
|
||
if strings.TrimSpace(cfg.UserService.Addr) == "" {
|
||
return errors.New("user_service.addr is required")
|
||
}
|
||
if cfg.UserService.RequestTimeout <= 0 {
|
||
return errors.New("user_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.WalletService.Addr) == "" {
|
||
return errors.New("wallet_service.addr is required")
|
||
}
|
||
if cfg.WalletService.RequestTimeout <= 0 {
|
||
return errors.New("wallet_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.RoomService.Addr) == "" {
|
||
return errors.New("room_service.addr is required")
|
||
}
|
||
if cfg.RoomService.RequestTimeout <= 0 {
|
||
return errors.New("room_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.RobotService.Addr) == "" {
|
||
return errors.New("robot_service.addr is required")
|
||
}
|
||
if cfg.RobotService.RequestTimeout <= 0 {
|
||
return errors.New("robot_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
|
||
return errors.New("activity_service.addr is required")
|
||
}
|
||
if cfg.ActivityService.RequestTimeout <= 0 {
|
||
return errors.New("activity_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.GameService.Addr) == "" {
|
||
return errors.New("game_service.addr is required")
|
||
}
|
||
if cfg.GameService.RequestTimeout <= 0 {
|
||
return errors.New("game_service.request_timeout must be greater than 0")
|
||
}
|
||
if strings.TrimSpace(cfg.StatisticsService.BaseURL) == "" {
|
||
return errors.New("statistics_service.base_url is required")
|
||
}
|
||
if cfg.StatisticsService.RequestTimeout <= 0 {
|
||
return errors.New("statistics_service.request_timeout must be greater than 0")
|
||
}
|
||
for index, source := range cfg.DashboardExternalSources {
|
||
if !source.Enabled {
|
||
continue
|
||
}
|
||
if strings.TrimSpace(source.AppCode) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].app_code is required when enabled", index)
|
||
}
|
||
switch strings.ToLower(strings.TrimSpace(source.SourceType)) {
|
||
case "mysql":
|
||
if strings.TrimSpace(source.SysOrigin) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].sys_origin is required when mysql source is enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MySQLDSN) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].mysql_dsn is required when mysql source is enabled", index)
|
||
}
|
||
case "aslan_http":
|
||
if strings.TrimSpace(source.BaseURL) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].base_url is required when aslan_http source is enabled", index)
|
||
}
|
||
if !strings.HasPrefix(source.BaseURL, "http://") && !strings.HasPrefix(source.BaseURL, "https://") {
|
||
return fmt.Errorf("dashboard_external_sources[%d].base_url must be an absolute url", index)
|
||
}
|
||
if strings.TrimSpace(source.OverviewPath) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].overview_path is required when aslan_http source is enabled", index)
|
||
}
|
||
case "aslan_mongo":
|
||
if strings.TrimSpace(source.SysOrigin) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].sys_origin is required when aslan_mongo source is enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoURI) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].mongo_uri is required when aslan_mongo source is enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoDatabase) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].mongo_database is required when aslan_mongo source is enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.LegacyMySQLDSN) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].legacy_mysql_dsn is required when aslan_mongo source is enabled", index)
|
||
}
|
||
if containsConfigPlaceholder(source.LegacyMySQLDSN) {
|
||
return fmt.Errorf("dashboard_external_sources[%d].legacy_mysql_dsn must be injected by environment, not left as a placeholder", index)
|
||
}
|
||
if strings.TrimSpace(source.LegacyWalletDatabase) == "" {
|
||
return fmt.Errorf("dashboard_external_sources[%d].legacy_wallet_database is required when aslan_mongo source is enabled", index)
|
||
}
|
||
if !isMySQLIdentifier(source.LegacyWalletDatabase) {
|
||
return fmt.Errorf("dashboard_external_sources[%d].legacy_wallet_database must be a mysql identifier", index)
|
||
}
|
||
default:
|
||
return fmt.Errorf("dashboard_external_sources[%d].type must be one of mysql, aslan_http, aslan_mongo", index)
|
||
}
|
||
if source.RequestTimeout <= 0 {
|
||
return fmt.Errorf("dashboard_external_sources[%d].request_timeout must be greater than 0", index)
|
||
}
|
||
}
|
||
if cfg.FinanceNotifications.DingTalk.Enabled && cfg.FinanceNotifications.DingTalk.WebhookURL != "" {
|
||
if !strings.HasPrefix(cfg.FinanceNotifications.DingTalk.WebhookURL, "http://") && !strings.HasPrefix(cfg.FinanceNotifications.DingTalk.WebhookURL, "https://") {
|
||
return errors.New("finance_notifications.dingtalk.webhook_url must be an absolute url")
|
||
}
|
||
}
|
||
if cfg.FinanceNotifications.DingTalk.Enabled && cfg.FinanceNotifications.DingTalk.RequestTimeout <= 0 {
|
||
return errors.New("finance_notifications.dingtalk.request_timeout must be greater than 0")
|
||
}
|
||
if isLockedEnvironment(cfg.Environment) && strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN) == "" {
|
||
return errors.New("robot_profile_source.mysql_dsn is required outside local/dev")
|
||
}
|
||
if cfg.RobotProfileSource.RequestTimeout <= 0 {
|
||
return errors.New("robot_profile_source.request_timeout must be greater than 0")
|
||
}
|
||
for index, source := range cfg.MoneyRegionSources {
|
||
if !source.Enabled {
|
||
continue
|
||
}
|
||
// 线上 likei Mongo 是外部只读事实来源;启用但缺 URI/库/集合时必须启动失败,不能让财务范围静默漏掉 Yumi 区域。
|
||
if strings.TrimSpace(source.AppCode) == "" {
|
||
return fmt.Errorf("money_region_sources[%d].app_code is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.SysOrigin) == "" {
|
||
return fmt.Errorf("money_region_sources[%d].sys_origin is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoURI) == "" {
|
||
return fmt.Errorf("money_region_sources[%d].mongo_uri is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoDatabase) == "" {
|
||
return fmt.Errorf("money_region_sources[%d].mongo_database is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoCollection) == "" {
|
||
return fmt.Errorf("money_region_sources[%d].mongo_collection is required when enabled", index)
|
||
}
|
||
if source.RequestTimeout <= 0 {
|
||
return fmt.Errorf("money_region_sources[%d].request_timeout must be greater than 0", index)
|
||
}
|
||
}
|
||
for index, source := range cfg.FinanceBillSources {
|
||
if !source.Enabled {
|
||
continue
|
||
}
|
||
// legacy 账单源启用但配置不全时必须启动失败,避免财务充值明细静默漏掉 Yumi 账单。
|
||
if strings.TrimSpace(source.AppCode) == "" {
|
||
return fmt.Errorf("finance_bill_sources[%d].app_code is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.SysOrigin) == "" {
|
||
return fmt.Errorf("finance_bill_sources[%d].sys_origin is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoURI) == "" {
|
||
return fmt.Errorf("finance_bill_sources[%d].mongo_uri is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoDatabase) == "" {
|
||
return fmt.Errorf("finance_bill_sources[%d].mongo_database is required when enabled", index)
|
||
}
|
||
if strings.TrimSpace(source.MongoCollection) == "" {
|
||
return fmt.Errorf("finance_bill_sources[%d].mongo_collection is required when enabled", index)
|
||
}
|
||
if source.RequestTimeout <= 0 {
|
||
return fmt.Errorf("finance_bill_sources[%d].request_timeout must be greater than 0", index)
|
||
}
|
||
}
|
||
if cfg.TencentCOS.Enabled {
|
||
if cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" {
|
||
return errors.New("tencent-cos config is incomplete")
|
||
}
|
||
if !strings.HasPrefix(cfg.TencentCOS.AccessURL, "http://") && !strings.HasPrefix(cfg.TencentCOS.AccessURL, "https://") {
|
||
return errors.New("tencent-cos.access-url must be an absolute url")
|
||
}
|
||
}
|
||
if cfg.Redis.Enabled && strings.TrimSpace(cfg.Redis.Addr) == "" {
|
||
return errors.New("redis.addr is required when redis is enabled")
|
||
}
|
||
if cfg.Redis.DB < 0 {
|
||
return fmt.Errorf("redis.db must be greater than or equal to 0")
|
||
}
|
||
if cfg.Jobs.Enabled && cfg.Jobs.WorkerInterval <= 0 {
|
||
return errors.New("jobs.worker_interval must be greater than 0")
|
||
}
|
||
if cfg.Jobs.Enabled && cfg.Jobs.LeaseTTL <= 0 {
|
||
return errors.New("jobs.lease_ttl must be greater than 0")
|
||
}
|
||
if cfg.Jobs.Enabled && cfg.Jobs.MaxAttempts <= 0 {
|
||
return errors.New("jobs.max_attempts must be greater than 0")
|
||
}
|
||
if cfg.Jobs.Enabled && strings.TrimSpace(cfg.Jobs.ArtifactDir) == "" {
|
||
return errors.New("jobs.artifact_dir is required when jobs are enabled")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (cfg *Config) applyFinanceNotificationEnvOverrides() {
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_ENABLED")); value != "" {
|
||
cfg.FinanceNotifications.DingTalk.Enabled = parseBoolEnv(value)
|
||
}
|
||
if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_WEBHOOK_URL", "FINANCE_DINGTALK_WEBHOOK_URL")); value != "" {
|
||
// 机器人 Webhook 属于密钥级配置;生产可以只在运行环境注入,避免把真实 access_token 写进仓库里的 YAML。
|
||
cfg.FinanceNotifications.DingTalk.WebhookURL = value
|
||
cfg.FinanceNotifications.DingTalk.Enabled = true
|
||
}
|
||
if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_SECRET", "FINANCE_DINGTALK_SECRET")); value != "" {
|
||
cfg.FinanceNotifications.DingTalk.Secret = value
|
||
}
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_MOBILES")); value != "" {
|
||
cfg.FinanceNotifications.DingTalk.AtMobiles = strings.Split(value, ",")
|
||
}
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_ALL")); value != "" {
|
||
cfg.FinanceNotifications.DingTalk.AtAll = parseBoolEnv(value)
|
||
}
|
||
}
|
||
|
||
func (cfg *Config) applyDashboardExternalSourceEnvOverrides() {
|
||
for index := range cfg.DashboardExternalSources {
|
||
source := &cfg.DashboardExternalSources[index]
|
||
appKey := envAppKey(source.AppCode)
|
||
if appKey == "" {
|
||
continue
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_DASHBOARD_MONGO_URI",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
|
||
)); value != "" {
|
||
// Mongo URI 含线上密码;仓库 YAML 只放占位,真实值由运行环境注入后覆盖。
|
||
source.MongoURI = value
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_DASHBOARD_MONGO_DATABASE",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
|
||
)); value != "" {
|
||
previousMongoDatabase := source.MongoDatabase
|
||
usesDerivedWalletDatabase := source.LegacyWalletDatabase == "" || source.LegacyWalletDatabase == previousMongoDatabase+"_wallet"
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
|
||
if source.SourceType == "aslan_mongo" && usesDerivedWalletDatabase && source.MongoDatabase != "" {
|
||
source.LegacyWalletDatabase = source.MongoDatabase + "_wallet"
|
||
}
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_DASHBOARD_LEGACY_MYSQL_DSN",
|
||
"HYAPP_ADMIN_"+appKey+"_LEGACY_MYSQL_DSN",
|
||
)); value != "" {
|
||
// Aslan Mongo 大屏只把 legacy MySQL 作为补充事实源,真实 likei RDS 凭证由运行环境注入。
|
||
source.LegacyMySQLDSN = value
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_DASHBOARD_LEGACY_WALLET_DATABASE",
|
||
"HYAPP_ADMIN_"+appKey+"_LEGACY_WALLET_DATABASE",
|
||
)); value != "" {
|
||
source.LegacyWalletDatabase = strings.Trim(value, "/")
|
||
}
|
||
}
|
||
}
|
||
|
||
func (cfg *Config) applyMoneyRegionSourceEnvOverrides() {
|
||
for index := range cfg.MoneyRegionSources {
|
||
source := &cfg.MoneyRegionSources[index]
|
||
appKey := envAppKey(source.AppCode)
|
||
if appKey == "" {
|
||
continue
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_MONEY_REGION_MONGO_URI",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
|
||
)); value != "" {
|
||
// legacy 区域源和 Aslan 大屏可以共享同一个 Mongo 集群;只在运行环境注入真实密码。
|
||
source.MongoURI = value
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_MONEY_REGION_MONGO_DATABASE",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
|
||
)); value != "" {
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
|
||
}
|
||
}
|
||
}
|
||
|
||
func (cfg *Config) applyFinanceBillSourceEnvOverrides() {
|
||
for index := range cfg.FinanceBillSources {
|
||
source := &cfg.FinanceBillSources[index]
|
||
appKey := envAppKey(source.AppCode)
|
||
if appKey == "" {
|
||
continue
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_FINANCE_BILL_MONGO_URI",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_URI",
|
||
)); value != "" {
|
||
// legacy 账单源与区域源/大屏共享同一个 likei Mongo 集群;真实密码只由运行环境注入。
|
||
source.MongoURI = value
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_FINANCE_BILL_MONGO_DATABASE",
|
||
"HYAPP_ADMIN_"+appKey+"_MONGO_DATABASE",
|
||
)); value != "" {
|
||
source.MongoDatabase = strings.Trim(strings.TrimSpace(value), "/")
|
||
}
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_PACKAGE_NAME")); value != "" {
|
||
source.GooglePackageName = value
|
||
}
|
||
// 服务账号 JSON 含私钥;生产优先用环境变量注入,仓库 YAML 只放文件路径或占位。
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_SERVICE_ACCOUNT_JSON")); value != "" {
|
||
source.GoogleServiceAccountJSON = value
|
||
}
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_GOOGLE_SERVICE_ACCOUNT_FILE")); value != "" {
|
||
source.GoogleServiceAccountFile = value
|
||
}
|
||
}
|
||
}
|
||
|
||
func (cfg *Config) applyLegacyCoinSellerRechargeSourceEnvOverrides() {
|
||
for index := range cfg.LegacyCoinSellerRechargeSources {
|
||
source := &cfg.LegacyCoinSellerRechargeSources[index]
|
||
appKey := envAppKey(source.AppCode)
|
||
if appKey == "" {
|
||
continue
|
||
}
|
||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_" + appKey + "_LEGACY_COIN_SELLER_RECHARGE_ENABLED")); value != "" {
|
||
source.Enabled = parseBoolEnv(value)
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_COIN_SELLER_RECHARGE_APP_MYSQL_DSN",
|
||
"HYAPP_ADMIN_"+appKey+"_LEGACY_APP_MYSQL_DSN",
|
||
)); value != "" {
|
||
// legacy 用户库 DSN 带可写权限,只能由运行环境注入;仓库配置保持占位。
|
||
source.AppMySQLDSN = value
|
||
}
|
||
if value := strings.TrimSpace(firstEnv(
|
||
"HYAPP_ADMIN_"+appKey+"_COIN_SELLER_RECHARGE_WALLET_MYSQL_DSN",
|
||
"HYAPP_ADMIN_"+appKey+"_LEGACY_WALLET_MYSQL_DSN",
|
||
)); value != "" {
|
||
// legacy 钱包库 DSN 用于写 user_freight_balance_running_water,不能复用只读报表源。
|
||
source.WalletMySQLDSN = value
|
||
}
|
||
}
|
||
}
|
||
|
||
func envAppKey(appCode string) string {
|
||
appCode = strings.ToUpper(strings.TrimSpace(appCode))
|
||
if appCode == "" {
|
||
return ""
|
||
}
|
||
return strings.NewReplacer("-", "_", ".", "_").Replace(appCode)
|
||
}
|
||
|
||
func firstEnv(keys ...string) string {
|
||
for _, key := range keys {
|
||
if value := os.Getenv(key); strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func parseBoolEnv(value string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "1", "true", "yes", "y", "on":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func compactStrings(values []string) []string {
|
||
out := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||
out = append(out, trimmed)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func containsConfigPlaceholder(value string) bool {
|
||
upper := strings.ToUpper(strings.TrimSpace(value))
|
||
return strings.Contains(upper, "REPLACE_ME") || strings.Contains(upper, "CHANGEME") || strings.Contains(upper, "<")
|
||
}
|
||
|
||
func isMySQLIdentifier(value string) bool {
|
||
if value == "" {
|
||
return false
|
||
}
|
||
for _, r := range value {
|
||
if r == '_' || r == '$' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func validEnvironment(value string) bool {
|
||
switch value {
|
||
case "local", "dev", "staging", "prod":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isLockedEnvironment(value string) bool {
|
||
return value == "staging" || value == "prod"
|
||
}
|