Files
digital-psychology/apps/api/internal/config/config.go
T
jackyu66gitandCursor 879bf70cb7
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ECR-006): 落地运营后台 Phase A(admin API + admin-h5)
新增独立鉴权的 /api/v1/admin 与 Vue 控制台;会员授予与审计同事务,并补集成/单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 18:35:53 +08:00

216 lines
4.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
}
// 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"`
}
// 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
}
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
}
}
// Enabled reports whether DeepSeek can be called.
func (d DeepSeekConfig) Enabled() bool {
return strings.TrimSpace(d.APIKey) != ""
}