Files
Scoreboard/server/internal/config/config.go
T
AskaEth 2c9ff65fc4 feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)
- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对)
  与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的
  重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端
- export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep),
  自动、手动、发布前三类备份共用同一把互斥锁与滚动清理
- realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行,
  跨站需命中 server.allowedOrigins)
- console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀),
  服务管理器新增「已连接的大屏实例」卡片
- deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空,
  导致最近这次构建的 exe 里根本没注入署名
- docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
2026-09-09 21:41:08 +08:00

102 lines
2.4 KiB
Go

// Package config 负责加载服务端启动配置。
package config
import (
"fmt"
"log/slog"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// 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"`
}
// 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"},
}
}
// 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
}
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
}
}