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。
This commit is contained in:
AskaEth committed 2026-09-10 09:17:56 +08:00
1 parent 6483acf9ad
commit 005b51b2ec
18 files changed
+626 -64

No files matched your search

+39 -3
View File
@@ -13,6 +13,7 @@ const {
} = require('electron')
const { loadConfig, updateConfig, resolveExecutable } = require('./config')
const { resolveDataDir, migrateLegacyData, isInside } = require('./datadir')
const { LogBus } = require('./log-bus')
const { ServiceSupervisor, healthHost } = require('./service')
const { DisplayManager } = require('./displays')
@@ -40,6 +41,8 @@ function main() {
let quitting = false
let shutdownDone = false
let exePath = null
// 解析后的数据目录信息(见 datadir.js),拉起服务端和 passwd 都用它
let dataDirInfo = null
// 大屏实例看板只在管理窗口可见时轮询,窗口隐藏即停表
let instancesTimer = null
let instancesLastReason = null
@@ -431,6 +434,11 @@ function main() {
return 'config.yaml'
}
// 数据目录同理:不带上同一个 -data,passwd 会改到另一个库里的账号上
function passwdDataArg() {
return dataDirInfo && dataDirInfo.passToServer ? dataDirInfo.dir : ''
}
// 账号重置走同目录的 scoreboard.exe passwd 子命令:本机进程、不经 HTTP,
// 所以不需要任何登录态,这就是面板能「免验证」的原因。
async function resetAdminAccount(event, payload) {
@@ -454,14 +462,16 @@ function main() {
}
const configArg = passwdConfigArg()
const args = buildPasswdArgs({ configArg, currentUsername, newUsername, password })
const dataDirArg = passwdDataArg()
const args = buildPasswdArgs({ configArg, dataDir: dataDirArg, currentUsername, newUsername, password })
const sender = event.sender
accountBusy = true
// 日志里不能带明文密码
log.push(
'console',
`重置管理员账号:${exe} passwd -config ${configArg} -user ${currentUsername}` +
`重置管理员账号:${exe} passwd -config ${configArg}` +
`${dataDirArg ? ` -data ${dataDirArg}` : ''} -user ${currentUsername}` +
`${newUsername ? ` -new-username ${newUsername}` : ''}(密码已隐藏)`
)
@@ -565,10 +575,36 @@ function main() {
exePath = resolved.path
log.push('console', `服务端程序:${resolved.path}`)
const installDir = path.dirname(resolved.path)
dataDirInfo = resolveDataDir({
values: cfg.values,
installDir,
env: process.env,
isPackaged: app.isPackaged
})
// 老版本把数据放在安装目录里,而覆盖安装会先静默跑旧卸载器把安装目录整个删掉,
// 所以先做一次性迁移。只在打包态做,开发态不动仓库里的 data/。
if (app.isPackaged && dataDirInfo.passToServer) {
migrateLegacyData({
targetDir: dataDirInfo.dir,
legacyDir: path.join(installDir, 'data'),
log: (message) => log.push('console', message)
})
}
log.push(
'console',
`数据目录:${dataDirInfo.dir}${dataDirInfo.passToServer ? '' : '(沿用服务端默认)'}`
)
if (dataDirInfo.passToServer && isInside(installDir, dataDirInfo.dir)) {
log.push('console', `警告:数据目录仍在安装目录内(${dataDirInfo.dir}),覆盖安装时会被删除`)
}
service = new ServiceSupervisor({
values: cfg.values,
exePath: resolved.path,
log
log,
dataDir: dataDirInfo.passToServer ? dataDirInfo.dir : null
})
displays = new DisplayManager({