// Package config loads process configuration from YAML + optional env overrides. package config import ( "fmt" "log" "net/url" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) // Config holds runtime settings for the API server. type Config struct { HTTPAddr string DatabaseURL string AppEnv string DeepSeek DeepSeekConfig Admin AdminConfig WeChat WeChatConfig Java JavaConfig Pay PayConfig } // PayConfig is WeChat JSAPI merchant settings. type PayConfig struct { AppID string MchID string APIKey string NotifyURL string } // WeChatConfig is mini-program jscode2session credentials. type WeChatConfig struct { AppID string Secret string } // JavaConfig is the consult stack bridge (internal issue + proxy). type JavaConfig struct { BaseURL string InternalKey string } // AdminConfig for ops console bootstrap (ECR-006). type AdminConfig struct { BootstrapUsername string BootstrapPassword string } // DeepSeekConfig for Ask LLM. type DeepSeekConfig struct { APIKey string BaseURL string Model string TimeoutSec int } type fileConfig struct { App struct { Env string `yaml:"env"` HTTPAddr string `yaml:"http_addr"` } `yaml:"app"` Database struct { Host string `yaml:"host"` Port int `yaml:"port"` User string `yaml:"user"` Password string `yaml:"password"` Name string `yaml:"name"` SSLMode string `yaml:"sslmode"` } `yaml:"database"` DeepSeek struct { APIKey string `yaml:"api_key"` BaseURL string `yaml:"base_url"` Model string `yaml:"model"` TimeoutSec int `yaml:"timeout_sec"` } `yaml:"deepseek"` Admin struct { BootstrapUsername string `yaml:"bootstrap_username"` BootstrapPassword string `yaml:"bootstrap_password"` } `yaml:"admin"` WeChat struct { AppID string `yaml:"app_id"` Secret string `yaml:"secret"` } `yaml:"wechat"` Java struct { BaseURL string `yaml:"base_url"` InternalKey string `yaml:"internal_key"` } `yaml:"java"` Pay struct { AppID string `yaml:"app_id"` MchID string `yaml:"mch_id"` APIKey string `yaml:"api_key"` NotifyURL string `yaml:"notify_url"` } `yaml:"pay"` } // Load reads config.local.yaml (or CONFIG_PATH), then applies env overrides. func Load() Config { cfg := Config{ HTTPAddr: ":8080", DatabaseURL: "postgres://yuxingu:yuxingu@127.0.0.1:5432/yuxingu?sslmode=disable", AppEnv: "dev", DeepSeek: DeepSeekConfig{ BaseURL: "https://api.deepseek.com", Model: "deepseek-chat", TimeoutSec: 60, }, } path := resolveConfigPath() if path != "" { if err := mergeFile(&cfg, path); err != nil { log.Printf("config: load %s: %v (using defaults/env)", path, err) } else { log.Printf("config: loaded %s", path) } } else { log.Printf("config: no config.local.yaml found — copy config.example.yaml → config.local.yaml") } applyEnv(&cfg) return cfg } func resolveConfigPath() string { if p := os.Getenv("CONFIG_PATH"); p != "" { return p } candidates := []string{ "config.local.yaml", "apps/api/config.local.yaml", } for _, c := range candidates { if st, err := os.Stat(c); err == nil && !st.IsDir() { return c } } return "" } func mergeFile(cfg *Config, path string) error { raw, err := os.ReadFile(path) if err != nil { return err } var f fileConfig if err := yaml.Unmarshal(raw, &f); err != nil { return err } if f.App.Env != "" { cfg.AppEnv = f.App.Env } if f.App.HTTPAddr != "" { cfg.HTTPAddr = f.App.HTTPAddr } if f.Database.Host != "" || f.Database.User != "" || f.Database.Name != "" { cfg.DatabaseURL = buildDatabaseURL(f) } if f.DeepSeek.APIKey != "" { cfg.DeepSeek.APIKey = f.DeepSeek.APIKey } if f.DeepSeek.BaseURL != "" { cfg.DeepSeek.BaseURL = strings.TrimRight(f.DeepSeek.BaseURL, "/") } if f.DeepSeek.Model != "" { cfg.DeepSeek.Model = f.DeepSeek.Model } if f.DeepSeek.TimeoutSec > 0 { cfg.DeepSeek.TimeoutSec = f.DeepSeek.TimeoutSec } if f.Admin.BootstrapUsername != "" { cfg.Admin.BootstrapUsername = f.Admin.BootstrapUsername } if f.Admin.BootstrapPassword != "" { cfg.Admin.BootstrapPassword = f.Admin.BootstrapPassword } if f.WeChat.AppID != "" { cfg.WeChat.AppID = f.WeChat.AppID } if f.WeChat.Secret != "" { cfg.WeChat.Secret = f.WeChat.Secret } if f.Java.BaseURL != "" { cfg.Java.BaseURL = strings.TrimRight(f.Java.BaseURL, "/") } if f.Java.InternalKey != "" { cfg.Java.InternalKey = f.Java.InternalKey } if f.Pay.AppID != "" { cfg.Pay.AppID = f.Pay.AppID } if f.Pay.MchID != "" { cfg.Pay.MchID = f.Pay.MchID } if f.Pay.APIKey != "" { cfg.Pay.APIKey = f.Pay.APIKey } if f.Pay.NotifyURL != "" { cfg.Pay.NotifyURL = f.Pay.NotifyURL } return nil } func buildDatabaseURL(f fileConfig) string { host := f.Database.Host if host == "" { host = "127.0.0.1" } port := f.Database.Port if port == 0 { port = 5432 } user := f.Database.User if user == "" { user = "yuxingu" } pass := f.Database.Password name := f.Database.Name if name == "" { name = "yuxingu" } ssl := f.Database.SSLMode if ssl == "" { ssl = "disable" } u := url.URL{ Scheme: "postgres", User: url.UserPassword(user, pass), Host: fmt.Sprintf("%s:%d", host, port), Path: "/" + name, } q := u.Query() q.Set("sslmode", ssl) u.RawQuery = q.Encode() return u.String() } func applyEnv(cfg *Config) { if v := os.Getenv("HTTP_ADDR"); v != "" { cfg.HTTPAddr = v } if v := os.Getenv("DATABASE_URL"); v != "" { cfg.DatabaseURL = v } if v := os.Getenv("APP_ENV"); v != "" { cfg.AppEnv = v } if v := os.Getenv("DEEPSEEK_API_KEY"); v != "" { cfg.DeepSeek.APIKey = v } if v := os.Getenv("DEEPSEEK_BASE_URL"); v != "" { cfg.DeepSeek.BaseURL = strings.TrimRight(v, "/") } if v := os.Getenv("DEEPSEEK_MODEL"); v != "" { cfg.DeepSeek.Model = v } if v := os.Getenv("DEEPSEEK_TIMEOUT_SEC"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { cfg.DeepSeek.TimeoutSec = n } } if v := os.Getenv("ADMIN_BOOTSTRAP_USERNAME"); v != "" { cfg.Admin.BootstrapUsername = v } if v := os.Getenv("ADMIN_BOOTSTRAP_PASSWORD"); v != "" { cfg.Admin.BootstrapPassword = v } if v := os.Getenv("WECHAT_MINI_APP_ID"); v != "" { cfg.WeChat.AppID = v } if v := os.Getenv("WECHAT_MINI_APP_SECRET"); v != "" { cfg.WeChat.Secret = v } if v := os.Getenv("JAVA_CONSULT_BASE_URL"); v != "" { cfg.Java.BaseURL = strings.TrimRight(v, "/") } if v := os.Getenv("JAVA_INTERNAL_KEY"); v != "" { cfg.Java.InternalKey = v } if v := os.Getenv("PAY_APP_ID"); v != "" { cfg.Pay.AppID = v } if v := os.Getenv("PAY_MCH_ID"); v != "" { cfg.Pay.MchID = v } if v := os.Getenv("PAY_API_KEY"); v != "" { cfg.Pay.APIKey = v } if v := os.Getenv("PAY_NOTIFY_URL"); v != "" { cfg.Pay.NotifyURL = v } } // Enabled reports whether DeepSeek can be called. func (d DeepSeekConfig) Enabled() bool { return strings.TrimSpace(d.APIKey) != "" }