package config import ( "net/url" "os" "path/filepath" "strconv" "strings" "github.com/joho/godotenv" ) // Config 全部由环境变量填充(可通过工作目录下 .env.development 或 ENV_FILE 设置,见 Load)。 type Config struct { AppEnv string AdminToken string AuthAPIURL string DatabaseDSN string // 进程与对外访问(管理后台「系统状态」展示) HTTPListenAddr string PublicAPIBaseURL string // Redis(可选,用于缓存时配置;未启用则不连接) RedisEnabled bool RedisAddr string RedisPassword string RedisDB int RedisEnv string RabbitMQEnabled bool RabbitMQURL string RabbitMQEnv string WebhookMengyaSecret string PaymentPendingTTLSecs int // GinDebug:为 true 时启用 Gin Debug(打印路由表等);默认 false,日志更干净。 GinDebug bool // EnableSwagger:为 true 时注册 /swagger UI;默认在 GIN_DEBUG 或 ENABLE_SWAGGER 为真时为 true。 EnableSwagger bool } // 应用环境:在未设置 DATABASE_DSN / RABBITMQ_ENV 时决定默认值。 // APP_ENV=production → 生产库 DSN、RABBITMQ_ENV 默认 prod。 // 否则 → 开发默认(测试库、RabbitMQ 开发环境)。 const ( EnvDevelopment = "development" EnvProduction = "production" ) // 内建 DSN:当 DATABASE_DSN 为空时使用。 const ( TestDSN = "mengyastore-test:mengyastore-test@tcp(10.1.1.100:3306)/mengyastore-test?charset=utf8mb4&parseTime=True&loc=Local" ProdDSN = "mengyastore:mengyastore@tcp(192.168.1.100:3306)/mengyastore?charset=utf8mb4&parseTime=True&loc=Local" DefaultRabbitMQHost = "10.1.1.233" DefaultRabbitMQPort = "5672" DefaultRabbitVHost = "mengyastore-dev" DefaultRabbitMQUser = "admin" ProdRabbitMQHost = "192.168.1.100" DefaultRabbitVHostProd = "mengyastore-prod" // Redis:与 MySQL 同网段;未设置 REDIS_ADDR / REDIS_PASSWORD 时按 APP_ENV 填入 TestRedisAddr = "10.1.1.100:6379" ProdRedisAddr = "192.168.1.100:6379" // 内网默认口令(可用环境变量 REDIS_PASSWORD 覆盖) DefaultRedisPassword = "tyh@19900420" ) // Load 读取可选的 env 文件,再从进程环境组装 Config。 // // 解析 env 文件的顺序: // 1. 环境变量 ENV_FILE 指定的文件(若设置了) // 2. 当前工作目录下的 .env.development(不存在则忽略) // // 环境变量说明(除非注明否则均可选): // // APP_ENV — development | production(默认 development) // ADMIN_TOKEN — 管理端 API 令牌(默认 changeme) // AUTH_API_URL — 萌芽认证中心 SproutGate 根 URL // DATABASE_DSN — MySQL DSN;为空则按 APP_ENV 选用 TestDSN / ProdDSN // RABBITMQ_ENABLED — true/false // RABBITMQ_URL — 完整 amqp URL // RABBITMQ_ENV — dev | prod(默认随 APP_ENV) // RABBITMQ_PASSWORD — 若已启用 MQ 但未设 RABBITMQ_URL,则用默认主机/vhost 拼 URL 时需要口令 // // HTTP_LISTEN_ADDR — 进程监听,默认 :8080 // PUBLIC_API_BASE_URL — 对外 API 基地址(反向代理场景) // // WEBHOOK_MENGYA_SECRET — 若设置,到账 Webhook 须带相同请求头 X-Webhook-Secret // PAYMENT_PENDING_SECONDS — 萌芽支付待支付超时(秒,默认 60) // // GIN_DEBUG — 1/true/yes/on:Gin Debug;默认关闭;为真时同时开启 Swagger UI(/swagger) // ENABLE_SWAGGER — 1/true/yes/on:启用 Swagger UI;与 GIN_DEBUG 任一为真即注册 /swagger // WEBHOOK_LOG_VERBOSE — 1/true:萌芽 Webhook 日志附带 notice/body 摘要(排查渠道用) // // REDIS_ENABLED — 默认 true;显式 false/0/off 则关闭 // REDIS_ADDR — 默认 development→TestRedisAddr,production→ProdRedisAddr // REDIS_PASSWORD — 默认 DefaultRedisPassword(建议生产用环境变量覆盖) // REDIS_DB — 逻辑库编号,默认 1 // REDIS_ENV — dev|prod(展示用 Key 前缀环境) func Load() (*Config, error) { loadDotenv() appEnv := normalizeAppEnv(os.Getenv("APP_ENV")) cfg := &Config{ AppEnv: appEnv, AdminToken: strings.TrimSpace(os.Getenv("ADMIN_TOKEN")), AuthAPIURL: strings.TrimSpace(os.Getenv("AUTH_API_URL")), DatabaseDSN: strings.TrimSpace(os.Getenv("DATABASE_DSN")), RabbitMQURL: strings.TrimSpace(os.Getenv("RABBITMQ_URL")), } if cfg.AdminToken == "" { cfg.AdminToken = "changeme" } if cfg.DatabaseDSN == "" { if appEnv == EnvProduction { cfg.DatabaseDSN = ProdDSN } else { cfg.DatabaseDSN = TestDSN } } if v := os.Getenv("RABBITMQ_ENABLED"); v != "" { cfg.RabbitMQEnabled = v == "true" || v == "1" } cfg.RabbitMQEnv = strings.TrimSpace(os.Getenv("RABBITMQ_ENV")) if cfg.RabbitMQEnv == "" { if appEnv == EnvProduction { cfg.RabbitMQEnv = "prod" } else { cfg.RabbitMQEnv = "dev" } } if cfg.RabbitMQURL == "" && cfg.RabbitMQEnabled { cfg.RabbitMQURL = composeRabbitMQURL(cfg) } if v := os.Getenv("HTTP_LISTEN_ADDR"); v != "" { cfg.HTTPListenAddr = strings.TrimSpace(v) } if cfg.HTTPListenAddr == "" { cfg.HTTPListenAddr = ":8080" } cfg.PublicAPIBaseURL = strings.TrimSpace(os.Getenv("PUBLIC_API_BASE_URL")) cfg.RedisEnabled = redisEnabledFromEnv(os.Getenv("REDIS_ENABLED")) cfg.RedisAddr = strings.TrimSpace(os.Getenv("REDIS_ADDR")) cfg.RedisPassword = os.Getenv("REDIS_PASSWORD") if v := os.Getenv("REDIS_DB"); v != "" { if n, err := strconv.Atoi(v); err == nil { cfg.RedisDB = n } } cfg.RedisEnv = strings.TrimSpace(os.Getenv("REDIS_ENV")) if cfg.RedisEnv == "" { if appEnv == EnvProduction { cfg.RedisEnv = "prod" } else { cfg.RedisEnv = "dev" } } if cfg.RedisEnabled { if cfg.RedisAddr == "" { if appEnv == EnvProduction { cfg.RedisAddr = ProdRedisAddr } else { cfg.RedisAddr = TestRedisAddr } } if cfg.RedisPassword == "" { cfg.RedisPassword = DefaultRedisPassword } if cfg.RedisDB == 0 { cfg.RedisDB = 1 } } cfg.WebhookMengyaSecret = strings.TrimSpace(os.Getenv("WEBHOOK_MENGYA_SECRET")) if sec := strings.TrimSpace(os.Getenv("PAYMENT_PENDING_SECONDS")); sec != "" { if n, err := strconv.Atoi(sec); err == nil && n > 0 { cfg.PaymentPendingTTLSecs = n } } if cfg.PaymentPendingTTLSecs <= 0 { cfg.PaymentPendingTTLSecs = 60 } cfg.GinDebug = envTruthy(os.Getenv("GIN_DEBUG")) cfg.EnableSwagger = cfg.GinDebug || envTruthy(os.Getenv("ENABLE_SWAGGER")) return cfg, nil } // envTruthy:将 1、true、yes、on 视为真(不区分大小写)。 func envTruthy(s string) bool { switch strings.ToLower(strings.TrimSpace(s)) { case "1", "true", "yes", "on": return true default: return false } } // redisEnabledFromEnv:除非显式关闭,否则默认启用 Redis。 func redisEnabledFromEnv(v string) bool { s := strings.ToLower(strings.TrimSpace(v)) if s == "false" || s == "0" || s == "no" || s == "off" { return false } if s == "true" || s == "1" || s == "yes" || s == "on" { return true } return true } func loadDotenv() { if f := strings.TrimSpace(os.Getenv("ENV_FILE")); f != "" { _ = godotenv.Load(f) return } // 本地开发默认文件名;不存在则忽略。 _ = godotenv.Load(filepath.Clean(".env.development")) } func normalizeAppEnv(s string) string { switch strings.ToLower(strings.TrimSpace(s)) { case "production", "prod": return EnvProduction default: return EnvDevelopment } } func composeRabbitMQURL(cfg *Config) string { pass := os.Getenv("RABBITMQ_PASSWORD") if pass == "" { return "" } host := os.Getenv("RABBITMQ_HOST") port := os.Getenv("RABBITMQ_PORT") user := os.Getenv("RABBITMQ_USER") vhost := os.Getenv("RABBITMQ_VHOST") if host == "" { if cfg.RabbitMQEnv == "prod" { host = ProdRabbitMQHost } else { host = DefaultRabbitMQHost } } if port == "" { port = DefaultRabbitMQPort } if user == "" { user = DefaultRabbitMQUser } if vhost == "" { if cfg.RabbitMQEnv == "prod" { vhost = DefaultRabbitVHostProd } else { vhost = DefaultRabbitVHost } } var path string if vhost == "/" { path = "/%2F" } else { path = "/" + url.PathEscape(vhost) } u := url.URL{ Scheme: "amqp", User: url.UserPassword(user, pass), Host: host + ":" + port, Path: path, } return u.String() }