Files
Scoreboard/server/internal/config/config.go
T
AskaEth 005b51b2ec fix: 数据目录移出安装目录,修掉「覆盖安装/卸载即丢数据」
根因:数据目录是 <安装目录>\data(config.yaml 的 dataDir 相对服务端工作目录),
而 electron-builder 的卸载器结尾执行 RMDir /r $INSTDIR,且覆盖安装会先跑旧卸载器
(installSection.nsh:52 的 uninstallOldVersion)→ 赛事库/规则/用户/审计/上传图片
连同安装目录被整个删除,再装新版。卸载一次同样如此。

- 服务端新增 -data <dir>(主命令、backup、passwd 三处),非空时覆盖 config.yaml 的
  dataDir;不传时行为完全不变(兼容便携/手工运行)
- Console 把数据指到当前用户目录 %LOCALAPPDATA%\ScoreboardConsole\data(卸载器不会碰
  这里;没有用 app.getPath('userData'),因为 %APPDATA%\<AppName> 正是被删的那个)
- 一次性迁移:目标为空且 <安装目录>\data 有数据时整体改名过去;两边都有数据时不动作
  只记警告;任何失败都保留原地数据、绝不删除
- passwd 也带 -data(否则重置的是安装目录下那个空库里的账号)
- deploy/backup.bat 指向同一数据目录
- 文档:部署指南新增「数据放在哪里(升级/卸载都不会删)」+ 备份/恢复/迁移路径改写,
  上线检查清单与使用手册同步

自检:服务端 12 包全绿;console 语法 14 文件 + datadir 单测 11/11;backup.bat 保持纯 ASCII。
2026-09-10 09:17:56 +08:00

113 lines
2.8 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
}
// 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
}
}