// Package config 负责加载服务端启动配置。 package config import ( "fmt" "log/slog" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" "git.yeij.top/AskaEth/Scoreboard/server/internal/logging" ) // Config 是 config.yaml 的映射。 type Config struct { Server Server `yaml:"server"` DataDir string `yaml:"dataDir"` Backup Backup `yaml:"backup"` Log Log `yaml:"log"` } // Backup 是自动备份配置。 type Backup struct { // IntervalMinutes 是自动备份间隔(分钟);<=0 表示关闭自动备份。 IntervalMinutes int `yaml:"intervalMinutes"` // Keep 是每个库滚动保留的份数;<=0 时用 export.DefaultBackupKeep。 Keep int `yaml:"keep"` } // Server 是 HTTP 监听配置。 type Server struct { Host string `yaml:"host"` Port int `yaml:"port"` // AllowedOrigins 是 WebSocket 跨站 Origin 白名单;为空时只放行同源与不带 Origin 的客户端。 AllowedOrigins []string `yaml:"allowedOrigins"` } // Log 是日志配置。 type Log struct { Level string `yaml:"level"` // Dir 是日志根目录,其下分 operations/(放行类)与 interceptions/(拦截类); // 留空时用 /logs。 Dir string `yaml:"dir"` // MaxSizeMB 是单个日志文件的大小上限(MB);<=0 用 logging.DefaultMaxSizeMB。 MaxSizeMB int `yaml:"maxSizeMB"` // KeepFiles 是每个类别保留的份数(含当前文件);<=0 用 logging.DefaultKeepFiles。 KeepFiles int `yaml:"keepFiles"` } // Default 返回一份可直接使用的默认配置。 func Default() *Config { return &Config{ Server: Server{Host: "0.0.0.0", Port: 19540}, DataDir: "data", Backup: Backup{IntervalMinutes: 5, Keep: 20}, Log: Log{ Level: "info", MaxSizeMB: logging.DefaultMaxSizeMB, KeepFiles: logging.DefaultKeepFiles, }, } } // DirPath 返回日志根目录:配了 log.dir 就用它,否则落在数据目录下的 logs/。 // 相对路径保持相对(与 dataDir 的处理一致),现场怎么填就怎么用。 func (l Log) DirPath(dataDir string) string { if dir := strings.TrimSpace(l.Dir); dir != "" { return dir } return filepath.Join(dataDir, "logs") } // Load 读取配置文件;文件不存在时返回默认配置。 func Load(path string) (*Config, error) { cfg := Default() raw, err := os.ReadFile(path) switch { case err == nil: if err := yaml.Unmarshal(raw, cfg); err != nil { return nil, fmt.Errorf("解析配置文件 %s: %w", path, err) } case os.IsNotExist(err): // 没有配置文件时使用默认值 default: return nil, fmt.Errorf("读取配置文件 %s: %w", path, err) } if err := cfg.validate(); err != nil { return nil, err } return cfg, nil } // ApplyDataDir 用命令行显式指定的数据目录覆盖配置里的 dataDir:dir 为空或只有空白时保持原样。 // 返回是否发生了覆盖,调用方据此记录数据目录来源。 func (c *Config) ApplyDataDir(dir string) bool { dir = strings.TrimSpace(dir) if dir == "" { return false } c.DataDir = dir return true } func (c *Config) validate() error { if c.Server.Port <= 0 || c.Server.Port > 65535 { return fmt.Errorf("server.port 非法: %d", c.Server.Port) } if strings.TrimSpace(c.DataDir) == "" { return fmt.Errorf("dataDir 不能为空") } return nil } // Addr 返回 http.Server 使用的监听地址。 func (s Server) Addr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } // SlogLevel 把配置里的日志级别映射为 slog 级别。 func (l Log) SlogLevel() slog.Level { switch strings.ToLower(strings.TrimSpace(l.Level)) { case "debug": return slog.LevelDebug case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }