Files
goodBaby/configs/config.go
T
meiandClaude Fable 5 79774070fc feat: 完善后端并新增 Vue3 WebUI
后端:
- 修复登录校验、DB 并发(单例+WAL+纯Go sqlite)、会话密钥持久化等核心缺陷
- 新增 Timer CRUD/签到/手动触发、执行日志、仪表盘、驱动元数据等 API
- 统一驱动接口: 带 context 的执行器、真实错误返回、敏感字段掩码、连通性测试
- 补全钉钉执行器; Timer 触发/提醒去重; 配置支持默认值与环境变量覆盖

前端:
- Vite+Vue3+TS+Pinia+Element Plus, 由驱动元数据动态渲染配置表单
- 登录/仪表盘(倒计时环)/定时器/规则/账号/日志/设置页面, 支持暗色模式
- 主题色 #66ccff, 信号波纹 logo, go:embed 单二进制部署

构建: Dockerfile 增加前端构建阶段; 补充各驱动配置文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:19:41 +08:00

196 lines
4.9 KiB
Go

package configs
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"strconv"
"sync"
)
// 默认配置文件路径,可通过环境变量 GOODBABY_CONFIG 覆盖
const defaultConfigPath = "config.json"
type Config struct {
// 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 数据库文件路径
DatabasePath string `json:"database_path"`
// 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"`
}
func defaultConfig() Config {
return Config{
ListenAddr: ":8088",
EnableRegistry: true,
TimeoutDurationHours: 6,
CheckIntervalMinutes: 10,
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()
if err != nil {
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)
}
changed := normalize(&config)
applyEnvOverrides(&config)
if changed {
if err := save(path, config); err != nil {
// 只读挂载等场景下写回失败不应阻断启动
log.Printf("警告: 写回配置文件失败: %v", err)
}
}
return config, nil
}
// 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.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_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)
}