Files
goodBaby/configs/config.go
T

221 lines
5.7 KiB
Go
Raw Normal View History

2025-10-06 16:23:56 +08:00
package configs
import (
2026-07-26 22:19:41 +08:00
"crypto/rand"
"encoding/hex"
2025-10-06 16:23:56 +08:00
"encoding/json"
2026-07-26 22:19:41 +08:00
"errors"
"fmt"
"io/fs"
"log"
2025-10-06 16:23:56 +08:00
"os"
2026-07-26 22:19:41 +08:00
"strconv"
"sync"
2025-10-06 16:23:56 +08:00
)
2026-07-26 22:19:41 +08:00
// 默认配置文件路径,可通过环境变量 GOODBABY_CONFIG 覆盖
const defaultConfigPath = "config.json"
2026-04-05 10:36:22 +08:00
2025-10-06 16:23:56 +08:00
type Config struct {
2026-07-26 22:19:41 +08:00
// HTTP 监听地址,如 ":8088"
ListenAddr string `json:"listen_addr"`
// 是否开放注册
EnableRegistry bool `json:"enable_registry"`
// 规则执行的最长重试时间(小时),用于指数退避的整体超时
TimeoutDurationHours int `json:"timeout_duration_hours"`
// 检查 timer 的间隔(分钟)
CheckIntervalMinutes int `json:"check_interval_minutes"`
// 数据库驱动,"sqlite"(默认) 或 "postgres"
DatabaseDriver string `json:"database_driver"`
// sqlite 数据库文件路径,DatabaseDriver 为 sqlite 时使用
2026-07-26 22:19:41 +08:00
DatabasePath string `json:"database_path"`
// postgres 连接串,DatabaseDriver 为 postgres 时使用
// 例如 postgres://user:pass@localhost:5432/goodbaby?sslmode=disable
DatabaseDSN string `json:"database_dsn"`
2026-07-26 22:19:41 +08:00
// session 加密密钥,为空时自动生成并写回配置文件
SessionSecret string `json:"session_secret"`
// session 有效期(小时)
SessionMaxAgeHours int `json:"session_max_age_hours"`
// 允许跨域访问的来源,开发前端时使用,如 ["http://localhost:5173"]
AllowedOrigins []string `json:"allowed_origins"`
// 每个用户保留的执行日志条数,<=0 表示不限制
LogRetainCount int `json:"log_retain_count"`
}
// 支持的数据库驱动
const (
DriverSQLite = "sqlite"
DriverPostgres = "postgres"
)
2026-07-26 22:19:41 +08:00
func defaultConfig() Config {
return Config{
ListenAddr: ":8088",
EnableRegistry: true,
TimeoutDurationHours: 6,
CheckIntervalMinutes: 10,
DatabaseDriver: DriverSQLite,
2026-07-26 22:19:41 +08:00
DatabasePath: "data.db",
SessionMaxAgeHours: 24 * 7,
AllowedOrigins: []string{},
LogRetainCount: 500,
}
}
var (
once sync.Once
cached Config
loadErr error
cacheMux sync.RWMutex
)
// GetConfig 读取配置,只在首次调用时读盘,之后返回缓存。
//
// 配置文件不存在时会以默认值创建一份,缺失的字段会被补齐并写回。
func GetConfig() (Config, error) {
once.Do(func() {
cfg, err := load()
cacheMux.Lock()
cached, loadErr = cfg, err
cacheMux.Unlock()
})
cacheMux.RLock()
defer cacheMux.RUnlock()
return cached, loadErr
}
// MustGetConfig 与 GetConfig 相同,但在出错时直接退出,用于启动阶段
func MustGetConfig() Config {
cfg, err := GetConfig()
2025-10-06 16:23:56 +08:00
if err != nil {
2026-07-26 22:19:41 +08:00
log.Fatalf("加载配置失败: %v", err)
}
return cfg
}
func configPath() string {
if p := os.Getenv("GOODBABY_CONFIG"); p != "" {
return p
}
return defaultConfigPath
}
func load() (Config, error) {
path := configPath()
config := defaultConfig()
content, err := os.ReadFile(path)
switch {
case err == nil:
if err := json.Unmarshal(content, &config); err != nil {
return config, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
}
case errors.Is(err, fs.ErrNotExist):
log.Printf("配置文件 %s 不存在,使用默认配置创建", path)
default:
return config, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
2025-10-06 16:23:56 +08:00
}
2026-07-26 22:19:41 +08:00
changed := normalize(&config)
applyEnvOverrides(&config)
if changed {
if err := save(path, config); err != nil {
// 只读挂载等场景下写回失败不应阻断启动
log.Printf("警告: 写回配置文件失败: %v", err)
}
2025-10-06 16:23:56 +08:00
}
return config, nil
}
2026-07-26 22:19:41 +08:00
// normalize 补齐缺失/非法的配置项,返回是否发生了修改
func normalize(c *Config) bool {
def := defaultConfig()
changed := false
if c.ListenAddr == "" {
c.ListenAddr, changed = def.ListenAddr, true
}
if c.TimeoutDurationHours <= 0 {
c.TimeoutDurationHours, changed = def.TimeoutDurationHours, true
}
if c.CheckIntervalMinutes <= 0 {
c.CheckIntervalMinutes, changed = def.CheckIntervalMinutes, true
}
if c.DatabasePath == "" {
c.DatabasePath, changed = def.DatabasePath, true
}
if c.DatabaseDriver == "" {
c.DatabaseDriver, changed = def.DatabaseDriver, true
}
2026-07-26 22:19:41 +08:00
if c.SessionMaxAgeHours <= 0 {
c.SessionMaxAgeHours, changed = def.SessionMaxAgeHours, true
}
if c.SessionSecret == "" {
// 随机生成一次并持久化,避免每次重启都让所有用户掉线
c.SessionSecret, changed = randomHex(32), true
}
if c.AllowedOrigins == nil {
c.AllowedOrigins, changed = def.AllowedOrigins, true
}
return changed
}
func applyEnvOverrides(c *Config) {
if v := os.Getenv("GOODBABY_LISTEN_ADDR"); v != "" {
c.ListenAddr = v
}
if v := os.Getenv("GOODBABY_DB_PATH"); v != "" {
c.DatabasePath = v
}
if v := os.Getenv("GOODBABY_DB_DRIVER"); v != "" {
c.DatabaseDriver = v
}
if v := os.Getenv("GOODBABY_DB_DSN"); v != "" {
c.DatabaseDSN = v
// 给了连接串却没显式指定驱动时,按 postgres 处理
if os.Getenv("GOODBABY_DB_DRIVER") == "" {
c.DatabaseDriver = DriverPostgres
}
}
2026-07-26 22:19:41 +08:00
if v := os.Getenv("GOODBABY_SESSION_SECRET"); v != "" {
c.SessionSecret = v
}
if v := os.Getenv("GOODBABY_ENABLE_REGISTRY"); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
c.EnableRegistry = b
}
}
}
func save(path string, c Config) error {
content, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, content, 0o600)
}
// Save 持久化配置并刷新缓存,供管理接口修改运行时配置使用
func Save(c Config) error {
normalize(&c)
if err := save(configPath(), c); err != nil {
return err
}
cacheMux.Lock()
cached, loadErr = c, nil
cacheMux.Unlock()
return nil
}
func randomHex(n int) string {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
panic(fmt.Sprintf("无法生成随机密钥: %v", err))
}
return hex.EncodeToString(buf)
}