Files
digital-psychology/apps/api/internal/config/config.go
T
jackyu66gitandCursor 7155b8b53a feat(api): 接入微信登录并原生实现咨询域(ECR-049/050)
小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 00:26:29 +08:00

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) != ""
}