fix: 修掉十路审查发现的 9 个 P0 与一批 P1(数据正确性 / 越权 / 现场可用性)

P0(9 条全修):
- 多通道项目奖牌重复计数:domain 层按 (EventID,CategoryID,EntryID/Subject) 去重,
  通道优先级 athlete→team→id
- 「只排名不计分」项目(scored:false)不再进奖牌榜,REST/WS/导出口径统一
- aggregate 流水 entry_id 恒 NULL 导致重算后分数翻倍:作废范围改按 event+category 定位
- 已发布成绩仍可被「存草稿」覆盖并清空 rank:服务端 409 STATE_INVALID + 前端禁编辑
- voidResult / 规则导入重算后名次不回写:接住 Ranked 调 applyComputedRanks
- 赛事包覆盖导入可能「旧项目已删、新项目没成」:先改名成功再清理,失败回滚
- 导入赛事包可覆盖正在运行的赛事目录:name==当前项目直接 400
- Console spawn 失败后守护器永久卡死:清 child/pid + 调度重启 + stop 超时兜底
- /ws 快照向非 admin 下发 admin-only 配置:按角色裁剪 settings

P1(计分口径 / 契约 / 现场):
- standings.tieBreak 三处接线;profiles[].recordBonusMode 生效;破纪录校验
  RecordEligible 与 entry 归属;破纪录登记校验已发布成绩
- export.loadRanked 与主路径对齐;ListBackups reason 解析;CLI backup 前缀修正
  (原来只增不减会撑爆磁盘);单位积分明细/导出统一权重折算;停用单位展示
- 规则导入原子化(重算并入同一事务)+ 返回 compatible/affectedEventCount/
  recalculatedLedgerRows;登录审计 user_id 修正
- WS 策略性断开用 Close(code,reason);HelloMessage 补 page/instanceId;
  实例注册表清理;settings/单位/项目写操作补 change_log 触发大屏刷新
- 大屏:全量快照不再被版本守卫丢弃;个人积分榜刷新;公告返回页/轮播;行数截断;
  REST 竞态;1008 停止重连;握手超时;时区补 Z;401 仅 AUTH_REQUIRED 才登出
- 管理端:published 行禁编辑;saveDraft 补 unit(1:06.33 显示);前端预览改用服务端
  points;空成绩行不再恒「已修改」;修订详情按 id 拉取;切项目请求序号与 dirty 保护;
  发布后刷新项目状态;NAV/路由按角色过滤;recorder 不再打 admin-only 接口
- Console:大屏窗口销毁后重建;大屏 URL 复用 host 归一化;backup.bat 改纯 ASCII
  (已在 Meowhome 实跑验证)

自检:服务端 gofmt/build/vet 通过、12 包测试全绿;前端 vue-tsc + build 通过、
node --test 24/24;console check-syntax 12 文件通过。
This commit is contained in:
AskaEth committed 2026-09-09 22:16:58 +08:00
1 parent 96cf181ae3
commit 13f1f03cd0
124 files changed
+5658 -1820

No files matched your search

+30 -24
View File
@@ -1,15 +1,20 @@
@echo off
REM ============================================================================
REM Scoreboard 一键备份(Windows)
REM Scoreboard one-click backup (Windows)
REM
REM 调用 scoreboard.exe 的 backup 子命令(内置 VACUUM INTO),服务运行中也能
REM 安全执行,不需要停服务、不依赖外部 sqlite3.exe。
REM 产物落在 data\backups\,服务端按库(system / 各赛事)各自滚动保留 20 份。
REM 注意:只快照 .db,不含 event_projs\<项目>\uploads\ 里的背景图/会标等资源;
REM 需要连资源一起留档时,请另行拷贝 data\event_projs\<项目>\uploads\。
REM Calls scoreboard.exe's backup subcommand (VACUUM INTO), so it is safe to
REM run while the service is up and does not need an external sqlite3.exe.
REM Output goes to data\backups\; the server keeps the newest 20 per DB
REM (system / each event).
REM NOTE: only .db snapshots are taken. Background images / emblems under
REM data\event_projs\<project>\uploads\ are NOT included; copy that
REM directory separately if you need them on the archive.
REM
REM 用法:deploy\backup.bat 手动备份(原因记 manual)
REM deploy\backup.bat publish 发布前备份(原因记 publish)
REM Usage: deploy\backup.bat manual backup (reason=manual)
REM deploy\backup.bat publish pre-publish backup (reason=publish)
REM
REM Keep this file ASCII-only. cmd.exe mis-parses UTF-8 non-ASCII text inside
REM batch files (especially inside if(...) blocks). See deploy\build.bat.
REM ============================================================================
setlocal enabledelayedexpansion
chcp 65001 >nul 2>&1
@@ -26,55 +31,56 @@ set "REASON=%REASON:\=-%"
set "REASON=%REASON:"=%"
if not exist "%DATA%" (
echo [错误] 找不到数据目录:%DATA%
echo 请在项目根目录执行本脚本。
echo [ERROR] data directory not found: %DATA%
echo Run this script from the project root.
exit /b 1
)
REM ---- 定位 scoreboard.exe ----------------------------------------------------
REM ---- locate scoreboard.exe -------------------------------------------------
set "EXE="
if exist "%~dp0..\dist\scoreboard.exe" set "EXE=%~dp0..\dist\scoreboard.exe"
if not defined EXE if exist "%ROOT%\scoreboard.exe" set "EXE=%ROOT%\scoreboard.exe"
if not defined EXE if exist "%~dp0scoreboard.exe" set "EXE=%~dp0scoreboard.exe"
if not defined EXE for %%I in (scoreboard.exe) do if exist "%%~$PATH:I" set "EXE=%%~$PATH:I"
if not defined EXE (
echo [错误] 找不到 scoreboard.exe。
echo 请先执行 deploy\build.bat,或把它放到项目根目录 / 加入 PATH。
echo [ERROR] scoreboard.exe not found.
echo Run deploy\build.bat first, or put it in the project root / PATH.
exit /b 1
)
REM dataDir 是相对路径,服务端按当前工作目录解析,所以必须在项目根目录调用
REM dataDir is relative and resolved against the current directory by the server,
REM so this script must run from the project root.
if not exist "%ROOT%\config.yaml" (
echo [错误] 找不到 %ROOT%\config.yaml,无法定位 dataDir。
echo [ERROR] %ROOT%\config.yaml not found, cannot locate dataDir.
exit /b 1
)
echo === Scoreboard 备份 ===
echo 数据目录:%DATA%
echo 服务端 :%EXE%
echo 原因 :%REASON%
echo === Scoreboard backup ===
echo Data dir : %DATA%
echo Server : %EXE%
echo Reason : %REASON%
echo.
set /a OK=0
set /a FAIL=0
REM 服务端一次只备份一个赛事库(system.db 每次都会一并快照),按目录逐个调用;
REM 部署形态通常只有一个 default 项目,这里就只跑一次。
REM The server backs up one event DB at a time (system.db is always included),
REM so loop over the project directories. A typical deployment has one project.
set "FOUND="
for /d %%D in ("%DATA%\event_projs\*") do (
set "FOUND=1"
echo 备份赛事 %%~nxD
echo backup event %%~nxD
"%EXE%" backup -config config.yaml -project "%%~nxD" -reason "%REASON%"
if errorlevel 1 (set /a FAIL+=1) else (set /a OK+=1)
)
if not defined FOUND (
echo 备份默认赛事
echo backup default event
"%EXE%" backup -config config.yaml -reason "%REASON%"
if errorlevel 1 (set /a FAIL+=1) else (set /a OK+=1)
)
echo.
echo === 备份完成:成功 !OK! 个,失败 !FAIL! 个 ===
echo === backup done: ok=!OK! fail=!FAIL! ===
dir /b "%DATA%\backups\*.db" 2>nul
if !FAIL! GTR 0 (
+6 -2
View File
@@ -31,8 +31,9 @@ cd server
if not exist ..\dist mkdir ..\dist
REM internal signature is injected from a machine-local file (gitignored, never in repo).
REM set /p returns empty under chcp 65001, so read the first line with for /f instead.
REM PowerShell reads the file as text and strips a BOM; for /f file-read does not.
set "INTERNAL_SIGNATURE="
if exist "..\deploy\signature.local.txt" for /f "usebackq delims= eol=" %%A in ("..\deploy\signature.local.txt") do if not defined INTERNAL_SIGNATURE set "INTERNAL_SIGNATURE=%%A"
if exist "..\deploy\signature.local.txt" for /f "usebackq delims= eol=" %%A in (`powershell -NoProfile -Command "Get-Content -LiteralPath '..\deploy\signature.local.txt' -TotalCount 1"`) do if not defined INTERNAL_SIGNATURE set "INTERNAL_SIGNATURE=%%A"
if defined INTERNAL_SIGNATURE (
REM single quotes keep spaces inside the value intact for go's ldflags parser
go build -ldflags "-X 'git.yeij.top/AskaEth/Scoreboard/server/internal/version.InternalSignature=%INTERNAL_SIGNATURE%'" -o ..\dist\scoreboard.exe . || exit /b 1
@@ -48,7 +49,10 @@ REM win.icon is written into the PE resources by electron-builder (via rcedit);
REM it needs winCodeSign / nsis, fetched through the gh proxy above.
taskkill /im "Scoreboard Console.exe" /f >nul 2>&1
taskkill /im scoreboard.exe /f >nul 2>&1
if not exist "rules" echo [warn] rules dir missing
if not exist "rules" (
echo [ERROR] rules dir missing - electron-builder extraFiles needs it
exit /b 1
)
node deploy\prepare-extra.mjs || exit /b 1
cd console
REM console\pnpm-workspace.yaml uses allowBuilds so electron's install script is