Files
Scoreboard/console/main/config.js
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

123 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict'
const fs = require('fs')
const path = require('path')
// 默认值写死在代码里:config.json 只覆盖要改的项。
// 这样升级新增字段时老配置依然能用,现场也不容易配出半成品。
const DEFAULTS = {
server: {
exe: 'scoreboard.exe',
args: ['-config', 'config.yaml'],
// 数据目录。空串 = 用 Console 选的默认位置(打包态是
// %LOCALAPPDATA%\ScoreboardConsole\data,装在安装目录之外,覆盖安装不会丢数据);
// 填了就按它来,相对路径以安装目录为基准(详见 datadir.js)
dataDir: '',
host: '127.0.0.1',
port: 19540,
autoStart: true,
healthIntervalMs: 5000,
restartDelayMs: 3000,
// 0 表示无限重启;服务端是现场唯一进程,崩了必须拉起来
maxRestarts: 0,
// 优雅关闭握手:服务端需实现该回环接口并校验 X-Shutdown-Token,
// 未实现(404/连接失败)时回退到进程信号
shutdownPath: '/api/shutdown',
// 服务端受理关闭请求后等待其自行退出的上限,超过则强杀
shutdownTimeoutMs: 5000,
// 连续健康这么久后清零崩溃计数,避免历史崩溃耗尽 maxRestarts
healthyResetMs: 60000
},
display: {
// all-secondary | all | specified
selection: 'all-secondary',
displays: [],
// 只能写大屏实际支持的页名(见 web/src/display/types.ts 的 PAGE_ALIASES)
pages: ['standings', 'medals', 'individual', 'results'],
pageParam: 'page',
kiosk: true,
ignoreMouse: true,
reloadEveryHours: 24,
// 服务端迟迟没就绪时的兜底:超时后也把大屏开出来,避免现场一片黑
waitForServerMs: 20000,
backgroundColor: '#0b0f1a'
},
startup: {
// 默认注册登录项但不启用(任务管理器「启动」里可见);服务管理窗口的开关控制是否真的随系统启动
openAtLogin: false
},
admin: {
width: 1280,
height: 860
},
log: {
maxLines: 2000,
file: 'logs/console.log',
maxFileBytes: 4 * 1024 * 1024
}
}
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function merge(base, override) {
const out = isPlainObject(base) ? { ...base } : {}
if (!isPlainObject(override)) return out
for (const [key, value] of Object.entries(override)) {
if (value === undefined) continue
out[key] = isPlainObject(value) && isPlainObject(base[key]) ? merge(base[key], value) : value
}
return out
}
function readJson(file) {
try {
const raw = fs.readFileSync(file, 'utf8')
return { values: JSON.parse(raw), warning: null }
} catch (err) {
if (err.code === 'ENOENT') return { values: {}, warning: `配置文件不存在,使用内置默认值:${file}` }
return { values: {}, warning: `配置文件无法解析,使用内置默认值:${file}(${err.message})` }
}
}
function loadConfig(configPath) {
const { values, warning } = readJson(configPath)
return {
path: configPath,
values: merge(DEFAULTS, values),
warning
}
}
// 就地改配置并写回文件:只动 raw 里已有的键,不把 DEFAULTS 全量落盘,
// 否则升级新增的默认值会被老文件冻结成显式值。
function updateConfig(configPath, mutate) {
const { values } = readJson(configPath)
const raw = isPlainObject(values) ? values : {}
mutate(raw)
fs.mkdirSync(path.dirname(configPath), { recursive: true })
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf8')
return raw
}
// 相对路径的 exe 需要按「部署包根目录」理解,而部署包根目录在不同形态下不同:
// 开发时是 console/ 的上级,打包后是 exe 所在目录。逐个候选位置探测,避免写死。
function resolveExecutable(exe, configPath) {
if (!exe) return { path: null, candidates: [] }
if (path.isAbsolute(exe)) return { path: exe, candidates: [exe] }
const configDir = path.dirname(configPath)
const candidates = [
configDir,
process.cwd(),
path.resolve(configDir, '..'),
path.resolve(configDir, '..', 'dist')
].map((dir) => path.resolve(dir, exe))
const found = candidates.find((candidate) => fs.existsSync(candidate))
return { path: found || candidates[0], candidates: [...new Set(candidates)] }
}
module.exports = { loadConfig, updateConfig, resolveExecutable }