根因:数据目录是 <安装目录>\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。
113 lines
2.8 KiB
Go
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
|
|
}
|
|
}
|