小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。 Co-authored-by: Cursor <cursoragent@cursor.com>
301 lines
6.8 KiB
Go
301 lines
6.8 KiB
Go
// 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) != ""
|
|
}
|