AskaEth
|
a57df78f99
|
fix(installer): 安装位置识别与模式切换目录(真机四因根除)
主人真机复验:① 不能像原版那样自动识别「已安装程序的安装位置」;② 选「为所有用户安装」却装到
%LOCALAPPDATA% 而不是 Program Files。
真机打点(instrumented 副本 + WM_COMMAND/BM_CLICK/WM_SETTEXT 驱动)读出四个根因:
1. 切换安装模式单选钮只改状态位图,**不同步输入框**;
2. setInstallModePerAllUsers/PerUser 会按记忆/默认**重算 $INSTDIR**,把「框里定下的值」
(用户手改、浏览选择、记忆)**无条件冲掉** —— 这是两条症状的共同根因
(打点:after-normalize=[D:\SBCustom\Scoreboard Console] → after-mode=[C:\Program Files (x86)\…]);
3. 记忆读取 HKCU 优先、**不看安装模式** → 一条更早的用户级记忆顶掉刚装好的机器级目录;
4. 无人值守会话里 $LOCALAPPDATA 解析成 C:\ProgramData →「仅为我」默认成了 C:\ProgramData\Programs\…。
改法:
- 新增 ModeDefaultDir:「为所有用户」→ $PROGRAMFILES64\${APP_FILENAME}(本包是 x64);
「仅为我」→ SHGetKnownFolderPath(FOLDERID_UserProgramFiles),失败才退回 $LOCALAPPDATA\Programs;
- 新增 PathTouched(框里的文本 != 程序设定的值 = 用户手改过)与 SyncPathToMode:
切模式时**只有用户没改过**才把框与 $INSTDIR 同步成该模式默认目录(不冲掉用户刚填的);
- PageDirLeave 用 $pathKeep 保住路径,跑完上游两个宏后再恢复 —— 只让宏改模式与 SHELL_CONTEXT,不再改路径;
- .onInit 在 customInit 之后**按当前模式**重读自家记忆键(all → HKLM,否则 HKCU)。
真机六条全过(含落盘复核):进 P3 显示记忆目录;点「为所有用户」→ C:\Program Files\Scoreboard_Console;
点「仅为我」→ %LOCALAPPDATA%\Programs\…;点回去再正确切换;手塞自定义路径以它为准;改过后再切模式不被冲掉。
静默无回归:/S 6.6s exit=0 落记忆目录;/S /D= 6.4s exit=0 且 /D= 优先。
wine 回归:人手速度拖动、原生控件隐藏、P3→P5 自动翻页、✕ 退出无 StdUtils 报错、旧版在跑时 /S 关掉旧进程。
|
2026-09-13 23:32:00 +08:00 |
|
AskaEth
|
b1c6d5d54a
|
fix(installer): 定时器分辨率 1.5 秒 → 改用 nsDialogs 定时器(拖不动/进度跳/卡收尾三症状同根)
真机取证:StdUtils.TimerCreate 实测**每 ~1500ms 才跳一次**(真机 tick 计数 7 次/10.7 秒),
且请求 4/20/60/250ms 四个间隔出来的都是 1500ms 且完全同步 —— 等于与请求值无关的低分辨率实现。
上一版拿它做 60ms 轮询,于是:
- **窗口拖不动**:1.5 秒才采一次光标,而人手一次拖(按下→移动→松开)不到 1.5 秒,
永远采不到「左键按下」那一刻。判别性实验(wine,人手速度 ~0.6 秒):旧代码 dx=0/dy=0,
新代码 dx=200/dy=100。(上一轮 wine 测的是 6 秒慢速拖,慢到 1.5s 的 tick 也能采到,所以漏了。)
- **进度只显示 0% 与 100%**:真因不是 tick,而是分段耗时极不均匀(几乎全在解压,后 4 段每段 <1ms)
→ 每段加 400ms 下限,真机 trace 407/484/656ms,肉眼可见。
- **装完卡在「收尾」**:翻页那次 BM_CLICK 只发一次,丢了就永远停在第 4 页 → 改成装完后每 tick 重试
($instDone 门防止安装途中的 Sleep 泵出 tick 乱点)。
- 顺带修好 P5「完成」页:定时器改为每页重建,P5 有独立一份,悬停与拖动都恢复。
改法:${StdUtils.TimerCreate} → ${NSD_CreateTimer}(挂在当前页对话框上,每页 NewPage 重建、
KillTick 里 ${NSD_KillTimer},删掉 $timerId);Stage 加 STAGE_DWELL 400 + GetTickCount 补 Sleep。
真机验证:每页 tick ≈63ms 且 P1..P5 都活着;分段停留 ≥400ms;P4 自动翻到完成页;
静默 /S exit=0 / 7.8s、旧版在跑时 exit=0 / 10.5s 且 running_after=0。
wine 回归:人手速度拖动、原生控件隐藏、P3 翻页、✕ 退出无 StdUtils 报错、/S /currentuser /D= 正常。
真机无输入桌面,鼠标驱动的悬停/拖动只能靠 wine 几何证据 + 真机 tick 频率证据。
|
2026-09-13 21:13:02 +08:00 |
|
AskaEth
|
f09cb07a23
|
fix(installer): 静默安装挂死 —— 旧版卸载器在无人值守会话里弹了没人能点的模态框
真机挂死(/S 原地升级 600 秒超时)的根因不是探测方式,而是旧版本卸载器那一步:
CheckAdmin 修好后静默 /S 从「按用户」变成「为所有用户」,于是 DoInstall 里
uninstallOldVersion 真的去跑上次装的卸载器;那个旧卸载器自己又要一次 UAC,无人值守
会话里被系统取消(1223=ERROR_CANCELLED),重试 5 轮后上游 handleUninstallResult 弹出
MessageBox(无 /SD)→ 模态框没人点 → 永久挂住。
真机取证:枚举该进程窗口得到隐藏的 #32770「Failed to uninstall old application files…: 1223」
且线程处于 Wait: UserRequest;另用探针给 GetProcessInfo / nsProcess / 令牌查询 / netsh
逐步计时(真机全部 ≤234ms),排除「cmd|tasklist 无超时挂住」这条猜测。
改法:
- 接管上游的两个卸载失败钩子(放在 !include "installUtil.nsh" 之前):静默只 DetailPrint 并
继续覆盖安装;交互才 MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST + SetErrorLevel 2 + Quit
- 静默安装整段跳过旧卸载器(旧卸载器随时可能弹出无人能点的 UAC,唯一能保证有界行为的做法);
交互路径保留上游行为
- 保留上一轮的 nsProcess 化(不再起 cmd|tasklist|findstr)
真机实测(构建机 SSH):/S 原地升级 exit=0 / 7.2s(改前 600s 超时),目录 85 文件、卸载器重写且为 MZ;
先跑起旧版(含 scoreboard.exe 共 6 进程)再 /S → exit=0 / 9.9s、running_after=0;
交互窗口起得来不挂、原生控件可见性全 0。wine 回归全过(拖动/原生控件/翻页/退出无 StdUtils 报错/静默卸载)。
|
2026-09-13 19:55:21 +08:00 |
|
AskaEth
|
23ba8a1830
|
fix(installer): 管理员判定误报(真机)+ 安装时真正关闭旧版本进程
## 一、CheckAdmin 在真机永远判「非提权」
真凶:GetTokenInformation 的 ReturnLength 传了 NULL(`p 0`)。真机 Windows 直接返回
ERROR_INVALID_PARAMETER(80) → rc=0 → $gIsAdmin 恒为 0 → 明明已经 UAC 提权,P3 选
「为所有用户安装」仍弹「当前账户没有管理员权限…已改为仅为我安装」。wine 不校验该参数,
所以本地怎么点都复现不出来。
真机探针取证:NULL → rc=0 err=80 val=0;改传真实指针 → rc=1 val=1。
- ReturnLength 改用 System::Call 分配的真指针;取值寄存器写 .R9(写 .0 插件不认,
$0 会保持空串导致判定恒真);查询失败按非提权并把原始值落到 %TEMP% 便于取证
- un.onInit 里那句判定原先恒为真 → 每次卸载/升级白弹一次 UAC,一并修正
## 二、安装时没有关闭在跑的旧版本
根因两个:① 上游 CHECK_APP_RUNNING 的 MessageBox 被我们的自绘窗口压在下面(复现时可见
305×82 黑矩形),用户看不见但流程被阻塞,于是旧程序一直没被关掉;② 上游探测只按 $INSTDIR
前缀或 %USERNAME% 过滤,绿色版/别的账户实例一律漏掉。
改为自写 CloseRunningApp(安装/卸载共用):tasklist+findstr 按映像名找 → 温和 taskkill →
强制 /F /T /IM(连带 scoreboard.exe 子进程)→ 兜底 /F /IM → 仍关不掉时交互给置顶重试框、
静默直接非 0 退出。两条路都绝不静默往下一路装完。
回归(wine):拖动精确;原生控件 IsWindowVisible=0;P3→P4→P5 翻页;退出无 StdUtils 报错;
/S /currentuser /D=... 落对目录且卸载器为 MZ。
|
2026-09-13 18:16:27 +08:00 |
|
AskaEth
|
67eadd7fe6
|
fix(installer): 真机四条 UI 问题 + 提权改为 highestAvailable
主人真机实测报出四个问题,全部只有在真 Windows 上才暴露(wine 渲染/移动循环/UAC/插件卸载时序都不同):
1. **窗口拖不动**:原来把移动交给 DefWindowProc 的移动循环(ReleaseCapture + WM_NCLBUTTONDOWN HTCAPTION),
我们摘掉了 WS_CAPTION,真机上那个循环不启动。改成自己算位移:按下时记锚点(GetCursorPos + GetWindowRect),
之后每个 tick 用 SetWindowPos 跟光标走。注意起拖后不能因为光标甩出标题栏就停(否则变成「拖一下就断」)。
2. **原生按钮透出**:内层对话框只「盖住」了原生控件,鼠标划过时按钮自己重绘就露出来。现在每页显式隐藏
11 个原生 ID(Next/Cancel/Back + 品牌字 + 页眉),并断言 IsWindowVisible 为假;隐藏后 BM_CLICK 翻页仍有效。
3. **P3 点下一步闪一下没反应**:不是目录校验,是提权失败分支末尾的 Abort(默认选中「为所有用户安装」时
真机必然走到)。现在不再 Abort,降级为按用户安装并弹一句说明后继续。
4. **退出时弹 StdUtils::TimerCreate「Plug-in unloaded before all timers were destroyed」**:
定时器从来没销毁过。加哨兵 $timerOn,在 .onGUIEnd 与 MUI_CUSTOMFUNCTION_ABORT 两条退出路径上幂等销毁;
定时器全程只建一次(翻页不叠加)。对照实验:把销毁段掏空即复现该报错窗口。
**提权策略**(主人拍板「一启动就提权」):RequestExecutionLevel highest。
原先没写这条指令时 NSIS 默认是 requireAdministrator —— 受限账户机器上根本起不来;highest 在管理员机器上
行为与 admin 完全一致(启动即 UAC、全程提权、防火墙必然放行),但给受限账户留了降级路径。
同时去掉 P3 的中途 UAC_RunElevated(那正是「闪一下没反应」的来源)。
验证(wine):五页走完 + 点完成/点 ✕ 退出均无 StdUtils 报错;原生按钮 vis=000;拖动位移精确;
/S /currentuser /D=... 与 /S /D=...(非提权时降级按用户)都 exit 0 且写出真卸载器。
UAC 本身与四条观感须真机复验。
|
2026-09-13 16:49:20 +08:00 |
|
AskaEth
|
a1aeed3c55
|
fix(installer): 静默安装落到 /D= 而不是记忆目录 + 静默路径绕开 UAC 插件
真机实测发现:`安装包.exe /S /currentuser /D=D:\tmp\sbsilent` 退出码 0、耗时 12 秒,
但 /D= 目录是空的 —— 安装其实**落到了「安装目录记忆」里的那个目录**。
(我最初的判断「DoInstall 从未被调用」是错的:给 Section/DoInstall 打 trace 后实测整条链
从头跑到尾,只是 $INSTDIR 被 customInit 用注册表记忆值盖掉了;空目录是 .onInit 的
SetOutPath 先建出来的假象。)
- **/D= 优先级最高**:.onInit 先取 GetDParameter,customInit 之后再盖回 $INSTDIR
(/D= 不进 ${GetParameters},必须走 StdUtils.GetAllParameters)
- **静默入口**:Section "install" 在 ${Silent} 时 Call DoInstall + quitSuccess;
DoInstall 加一次性守卫 $instStarted,Tick 与 Section 谁先到谁干活,绝不装两遍
- **静默不弹 UAC**:per-machine 分支在静默时改用查进程令牌,非管理员退回按用户安装
- **${UAC_IsAdmin} 全局换成 CheckAdmin**(按 BUILD_UNINSTALLER 分流成 un.CheckAdmin):
构建期生成的 installer-custom.nsh 里 netsh 放行前那句判断原来走 UAC 插件,
无页窗口时不返回,是静默跑不通的最后一关
- 脚本不带 BOM(BOM 会被拼进大脚本中间),本机用 -INPUTCHARSET UTF8 编译
验证(wine):非静默 P1→P5 走完且目录/卸载器齐全;静默 /S /currentuser /D=C:\v4 → exit 0、
文件与真卸载器都在 C:\v4;再静默卸载 → 目录消失、注册表零残留;/S /D=... 亦落在 /D=。
真机由主代理复验。
|
2026-09-13 15:55:40 +08:00 |
|
AskaEth
|
9fee6f2da5
|
fix(installer): 安装器脚本不能带 BOM —— 它会拼进大脚本中间让 makensis 报 Invalid command
构建机上真实失败:`Invalid command: "?;"` / `Error in script "<stdin>" on line 83`。
本机单编这个脚本一切正常,只有 electron-builder 出包才炸。
根因:electron-builder 把「它的共享头部 + 我们的脚本」拼成一个整脚本、经 stdin 喂给 makensis,
而 Node 的 readFileSync(…, 'utf8') **不会**吃掉文件开头的 BOM —— 那个 U+FEFF 落在拼接结果
中间某一行的行首,makensis 就把它当成命令名的一部分(控制台显示成问号)。
- 去掉 console/installer/installer.nsi 的 UTF-8 BOM
- deploy/prepare-extra.mjs 的检查反向:从「必须有 BOM」改成「必须没有 BOM」,并把原因写进注释,
避免下一个人再按直觉加回去;中文编码改由 makensis 的 -INPUTCHARSET UTF8 保证
(electron-builder 本来就会传这个参数,本机手工编译也要带)
验证:无 BOM → prepare-extra 退出 0;临时加回 BOM → 退出 1 且提示准确。
|
2026-09-13 15:25:01 +08:00 |
|
AskaEth
|
877ff15823
|
feat(0.5.1): 业务逻辑审查 9 条 P0 全修 + 自绘安装器 + 现场反馈四项
## 算分正确性(6 份只读审查 → 收敛成 5 个病根,均先复现失败再修)
- 「人工名次」与「自动名次回写」互斥:原来同一份成绩**发布两次分数不同**
(团体报名项 dual 通道 10/8/6 → 25/18/14),现在连发多次逐字节一致
- 删小项时在同一事务作废旧流水并推榜:原来分数**永久残留**在总榜、任何重算都打不到
- 规则包导入整体事务化:原来中途失败会清空名次表且补不回(奖牌全丢);
另加守卫「compare=none 的小项不得配 basis=result/aggregate」
- 末位归零只抹真正落在末位的那一档:原来跨末位的并列组会被整组抹 0
(7 人三人并列第 5 时第 5、6 名的分也一起没了)
- 模板「分值表档位超出录取名次」现在拒绝保存:原来静默截断成 0 分
## 现场反馈(主人实测报的)
- 已发布成绩可改:行内「修改本条」+ 成绩格/状态格可点(退回草稿→改→重新发布本组),
与「作废」语义分开;取消路径不产生任何副作用
- 单位积分明细补「运动员 / 来源」列,并显示「已修订 ×N(时间·原因)」:
管理端、大屏覆盖层、xlsx 备注列三处同源
- 单位奖牌榜改为按奖牌独立排名(金→银→铜→单位编码),与积分榜解耦
- 历史孤儿积分流水清理(迁移 0022,只动 result/record_bonus,人工调整分不受影响)
## 新功能
- 团体项目聚合口径二选一:平均(默认)/ 总和(迁移 0023,通道编辑器可切)
## 安装器
- 自绘五页安装界面:无边框、自绘标题栏、DPI 自适应(4K@200% 实测)、
安装/卸载逻辑复用 electron-builder 宏不变;默认「为所有用户安装」并自动放行防火墙 19540
- 构建期校验:安装器素材里的版本号必须与 VERSION 一致,否则构建失败
自检:服务端 14 包 ×2 / 前端 vue-tsc + build + 742 例 ×2 / Console 20 文件 + 68 例 ×2,全绿。
端到端对拍:修前 vs 修后,纯团体 / 双计 / 普通个人三形态**逐字节一致**,仅出 bug 的双通道形态按要求改变。
|
2026-09-13 15:20:18 +08:00 |
|
AskaEth
|
c97d3150c9
|
fix(admin): 局域网大屏卡片按「地址类别」判定 + 三层兜底 + 手填入口(dev_0.5.0)
## 判断失误的修正(主人现场实测出来的)
主人现场是 **169.254 自动配置网段**(没有 DHCP,设备靠链路本地地址互访),
平板实测能访问 `http://169.254.79.190:19540/display`;而卡片一开始把链路本地地址
**排除**了,于是显示「未检测到局域网地址」。
根因:`usableLanIPv4` 排除了 `IsLinkLocalUnicast()`,理由写的是「169.254 说明没拿到 IP」
——那是**对网络健不健康的猜测**,不是**地址能不能用的事实**。
现改为按**地址类别**判定(`ipv4Scope`,全部用标准库谓词,**代码里没有任何网段字面量**):
只排除「作为对端访问目标没有意义」的四类(回环 / 未指定 / 组播 / 广播),
链路本地与私网、全局一样进候选,只**排在最后**并带说明
「自动配置地址:本网段没有 DHCP,同网段设备可以互相访问」。
新增**反硬编码护栏用例**:函数体内不得出现 `x.x.x.x` 字面量,且必须用满 6 个谓词。
顺带修掉新用例抓出的真 bug:`ipv4Scope(nil)` 曾落进 default 被当成「可用的 global」,
而 IPv6 的 `To4()` 正是 nil → 「只有 IPv6 的网卡」会被误判成有可用地址。
## 三层兜底(离线现场必须拿得到地址)
1. **严格轮**:接口 up 且非回环;
2. **宽松轮**:严格轮为空时不再要求 up 位(某些驱动/虚拟网卡读不到),命中标 `confidence: low`;
3. **默认路由探测**:两轮皆空时用 `net.Dial("udp4", …)` 问出本机源地址
(UDP connect 只查路由表、不握手不发包,离线安全;TCP 会等 SYN 超时,不能用),标 `confidence: route`。
接口同时回 `scope`(private/global/link-local)与 `skipped`(被跳过的网卡 + 原因,
code 收敛为 no-address / only-ipv6 / not-up / loopback——link-local 不再是「被跳过」的理由)。
## 手填入口(最后一道保险)
自动检测不到、或不是现场要用的网段时,操作员把 `ipconfig` 里的地址填进卡片,
立刻生成链接 + 二维码;排在第一条、带「手动」徽标、可删除、localStorage 持久化、
四条就地校验文案(空/含空格/不像 IP/重复),IPv6 也支持。
## 自检工具
`debug/诊断/局域网地址自检.exe` + 说明:只读、不写文件、不需管理员,
打印系统给 Go 看的网卡列表(含类别)、默认路由探测结果与可用大屏地址。
(正是它把现场定性成 169.254 网段的。)
## 真浏览器验证(四场景,非源码断言)
链接本地地址出链接+二维码+说明句 / 空态列出被跳过网卡并折叠(5 条 + 等共 N 块)/
low 与 route 两种来源提示各占一行互不顶掉 / 手填地址排第一并刷新不丢 —— 全部通过,0 页面错误。
## 文档
使用手册 §9、部署指南 7.5/7.9、上线检查清单(**防火墙放行入站 19540** 与
「另一台设备能打开局域网大屏」两条新增)、常见问题 Q101、升级说明 dev_0.5.0;
devdocs 05/07/13 同步(含这次误判的来龙去脉与「Get-NetAdapter 显示 Up ≠ 有 IP」)。
**没有数据迁移**,旧赛事包照常导入。
自检:服务端 13 包 ×2;前端 vue-tsc + 680 例 ×2 + build;Console 20 文件 + 68 例。
|
2026-09-12 18:30:46 +08:00 |
|
AskaEth
|
9ef68c2589
|
feat(console,admin): kiosk 不再点击穿透 + 压住任务栏;大屏控制页新增「局域网大屏」(链接/复制/二维码)dev_0.4.0
## ① 现场问题:kiosk 窗口点击穿透(原设计方向反了)
`displays.js` 原来对多屏 kiosk 调 `setIgnoreMouseEvents(true)`,注释写的是
「防止现场误触」——但该 API 的语义是「本窗口忽略鼠标」,点击会**落到它后面的
窗口或桌面**,现场想防的误触正好变成误点别的东西;被忽略鼠标的窗口也点不活。
现在窗口**默认吞掉鼠标、不穿透**。
配置项**改名**为 `display.allowClickThrough`(缺省 false;显式 true 才穿透)。
**旧键 `display.ignoreMouse` 完全不读**——老机器的配置文件里还写着旧版模板留下的
`ignoreMouse: true`,而配置策略是「已存在不覆盖」,只改默认值等于老机器升级后
照样穿透;改名才能让默认值真正生效。启动时若发现旧键会记一条中文提示(只记一次),
并在合并后的配置里把它剥掉,防止将来有人再读到那个陈年 true。
(不是自动改写用户配置文件:无法区分「操作员刻意写的 true」与「旧模板留下的 true」。)
## ② 现场问题:任务栏盖住 kiosk 窗口、被盖住后抢不回前台
原顺序是 `setKiosk(true)` 之后又 `setBounds(...)`,在 Windows 上会把窗口从真正的
全屏态拽回「恰好铺满该屏的无边框窗口」→ 不是 topmost,任务栏与拖过去的窗口都盖得住。
现在固定为 **先 `setBounds` 定位 → 再 `setKiosk(true)` → 最后 `setAlwaysOnTop(true,
'screen-saver')`**(默认层级 `floating` 在 Windows 上仍画在任务栏下面),并加了
「已达标就不重复进出 kiosk」的守卫避免重排白闪;非 kiosk 分支会摘掉置顶。
Win 真机行为需现场复核,代码/桩级已验(含顺序与层级的断言)。
## ③ 新功能:大屏控制页「局域网大屏」卡片
- 服务端新增 `GET /api/system/network`(recorder 及以上):回本机可用局域网 IPv4
(排除回环/link-local/IPv6,去重,按 192.168/10/172.16-31 优先排序)+ 真实监听端口
+ 路径;无地址回 `[]` 不报错。
- 卡片:每个地址一条可点击链接 + 复制按钮,第一个地址配二维码(148px)。
- **二维码是离线自实现的纯 TS 编码器**(版本 1–40 自动、ECC L/M/Q/H、8 种掩码择优),
正确性用 **Python OpenCV 真解码**验收:29/29 条解出原文(含中文与边界),
SVG 渲染后再解码 4/4,并把验证过的矩阵冻结成仓库参照用例。
- 复制按**非安全上下文**处理:面板常从 `http://192.168.x.x` 打开,那里
`navigator.clipboard` 是 undefined,自动回退 `execCommand('copy')`。
## ④ 真机联调抓出并修掉一个时序缺陷(真浏览器 + 真服务端)
`DisplayPage` 的 `refreshLan()` 在挂载时跑,而 `auth` 是异步加载的——那一刻
`auth.readOnly` 必然为 true,卡片把「当前为只读访客」写进一个**普通 ref** 就返回了,
身份落定后没有任何东西再触发它:**管理员登录后卡片永远停在访客态,
`/api/system/network` 一次都没被请求**。
改为「gate 由 computed 直接渲染 + watch gate 变 null 时重跑加载」,并补回归钉子。
(同时修正了一条把该错误结构当成正确行为的源码断言用例。)
端到端证据:真服务 + 局域网 IP(真非安全上下文)→ 卡片列出两个地址、点击复制变
「已复制」(走 execCommand 回退)→ 浏览器渲染的二维码截图经 cv2 解出
`http://10.50.101.85:19861/display`。
## 文档
部署指南配置表与多屏行为、使用手册「大屏控制」与局域网设备段、devdocs 05/07/10/13、
升级说明新增 dev_0.4.0(含旧键废弃与启动提示)。**无数据迁移**,旧赛事包照常导入。
自检:服务端 13 包 ×2;前端 vue-tsc + 645 例 ×2 + build;Console 20 文件 + 68 例。
|
2026-09-12 16:25:04 +08:00 |
|
AskaEth
|
ed2c0b3f3c
|
feat: 主人拍板的 4 条全部落地(单条作废/重录、破纪录驳回、历史纪录、仪表盘卡片)+ 迁移 0021(dev_0.3.0)
## ① 成绩「作废本条 / 重新录入」——手滑填错了怎么办
- 新增 `POST /api/results/{id}/reopen`:把已发布的那条退回草稿(state=draft、
status=ok、rank=NULL、note=原因)并重算推榜;口径与既有 `void` 完全一致
(回滚类动作,不受 `standings.live_on_publish` 约束)。
- 成绩录入页新增「操作」列:已发布行可「作废本条」,已作废行可「重新录入」,
两者都必填原因、确认后立即重算+推大屏。**验收用例**:作废→重录→改→重发,
名次/流水/榜单与「一开始就填对」逐字节一致。
## ② 破纪录的驳回 / 作废
- 迁移 `0021_record_breaks_note.sql`:`record_breaks` 加一列可空 `note`。
- 新增 `GET /api/record-breaks?state=`(recorder 起可读,guest 403)与
`POST /api/record-breaks/{id}/reject`(必填原因):已批准的登记被作废时
**撤回该笔加分、重算推榜、并把该级别当前纪录恢复为上一条**;`approve`
放开为可对 rejected 再次批准。
- 破纪录页新增「已批准的破纪录」卡片(作废入口);原先只存在浏览器内存里的
「拒绝」改为持久化(先登记再驳回,刷新不再复现)。
## ③ 历史纪录的写入路径(`records.source` 以前恒为「本届」)
- 新增 `POST /api/records`(source 只收 history)与 `DELETE /api/records/{id}`
(删当前纪录时恢复上一条)。纪录库卡片新增「新增历史纪录」与「删除」。
- 至此《使用手册》6.2 承诺的「来源(本届/历史)」这一列才真正有意义。
## ④ 仪表盘补齐承诺的卡片
今天的小项(按 scheduledAt 的本地时区当天 + 进度)、草稿待发布、待确认破纪录、
大屏在线数;数据源读不到显示 `—` 而不是 0。文档里「管理端在线数」服务端没有
接口,按实际改写成本机管理端连接状态。
## 旧版数据包兼容(主人特别交代,三处验证)
迁移只加**可空**列、不重建表、不动数据行;旧赛事包导入后由 `main.go` 的
`store.MigrateEvent` 在启动时补跑迁移。证据:`migration_0021_test.go`
(旧库升级 + 幂等 + 老行取 NULL)、`migration_legacy_test.go` 扩到 0021、
`legacy_pack_upgrade_test.go`(真实旧 event.db 走 main 同一条路径后装配 API,
驳回老登记 → 加分撤回、纪录回退、旧数据逐表一行不差),另有真二进制冒烟。
未迁移的库上不存在任何新列读取。
## 文档
升级说明新增 dev_0.3.0(含「旧包仍可导入、自动补跑迁移」);使用手册新增
§4.10 与 §6.1/§6.2 明细;常见问题新增 Q98/Q99/Q100;devdocs 04/05/07/13/02
同步(含新接口表与错误分级)。
自检:服务端 13 包 ×2 全绿(httpapi 新增 reopen/reject/records/旧包 四个用例文件)、
前端 vue-tsc + 604 例 ×2 + build、Console 19 文件 + 54 例。
|
2026-09-12 01:27:09 +08:00 |
|
AskaEth
|
6841cf49aa
|
fix(p2): 按审查清单修 103 条「建议修」+ 奖牌榜改名 + 两个守卫(dev_0.2.0)
## 奖牌榜改名(主人要求)
「团体奖牌榜」→「单位奖牌榜」,界面 / 大屏渲染页 / 导出件(工作表名与
下载文件名 `单位奖牌明细.xlsx`)/ 用户文档 全部统一;`DISPLAY_PAGES` 是
大屏控制页与大屏页的共同来源,改一处两边同时生效。旧名保留为别名,
老链接与老配置(`display.pages`)照常有效。真 Chromium 验证:14 个管理端
路由 + 7 个基础数据分区 0 报错,大屏页标题已是「单位奖牌榜」。
## P2 清单(123 条,103 条标「建议修」)
按 A/B/C/D 分组、以「文件所有权互斥」分 10 个代理并行推进,另有独立复核
代理逐条核对:61 条声称已修 / 55 条证实 / 4 条部分 / 0 条不成立 / 0 垃圾。
主要交付(按风险从高到低):
- 写入侧校验:update 路径补必填与 TrimSpace、模板 rules 重复 rank、
「小项不存在」的四种回答、`defaultProfileId` 外键、decodeJSON 的 8MB
上限与 413(A01–A06、D02、D03 批量上限 500)
- 危险操作:一键恢复加互斥(第二个请求当场 409)、恢复超时不再说
「未执行」、`ConfirmDialog` pending 时锁死 ESC/遮罩/关闭
- 认证:改密与撤会话进同一事务、滑动续期写库失败不再假装成功、
CSRF Cookie 丢失自动补发、密码长度按字符(原按字节)、
反代 403 只在可信代理透传原 Host 时采信 `X-Forwarded-Host`(全等比较)、
过期会话定期清理、登出补审计、登录限流可配
- 数据可见性:导出「已发布未结算」行改为 `名次 — / 积分 — / 状态未结算`、
单位积分明细成绩列与其它导出件逐字一致、空单位也打合计行、
基础数据包命中「已彻底删除的单位」整行跳过并报界面路径、
报名导入换单位给明确提示、勾选报名有冲突不再假报成功
- 导入/导出:赛事包上限 512MB(**它是唯一带字体/图片的包**,字体上限
20MB)而 multipart 内存档位独立钉在 16MB、工作簿表名尾随空格、
导出件路由与文件名契约补全
- 前端:一次写请求只生成一个 requestId(幂等缓存此前从未命中)、
基础数据包补 `X-Request-Id`、示例数据模式禁写扩到撤销发布/存草稿、
访客 401 不再是裸错误、模板 `{{else if}}` 不再静默吞内容
- Console:按设计退出(切赛事/恢复)不再判成崩溃、旧数据目录读不出来
会记日志、渲染层日志按上限截断、`service.js` 补 17 例测试
## 测试
服务端用例 610 → 674(13 包全绿,新增老库跨九条迁移升级、panic 形状、
429 落 interceptions、三态对照、avg_mode 分母、会话失效、请求体上限等)
前端 449 → 531(vue-tsc + build 通过);Console 28 → 54(19 个文件)。
另修复一条会让整包挂死 600s 的用例(测试主协程堵在 client.Do,defer
轮不到执行 → 改为可取消 + 探针超时,改坏也 7.5s 快速失败)。
## 两个新守卫(防止再出现「组件 setup 抛异常」这类盲区)
- `web/scripts/check-declaration-order.mjs` 已进测试命令:沿 watch/
watchEffect 的 const 依赖链查 TDZ(正是「运动员页整块空白」的根因)
- `web/scripts/smoke-admin.mjs`(`pnpm smoke`):真 Chromium 点遍 14 个
管理端路由 + 7 个基础数据分区 + 大屏页,任何 console 错误即失败
## 文档
- `docs/升级说明.md` 新增 dev_0.2.0 一节(三件升级必知:改名、审计前缀
与历史记录、密码口径;现场变化;赛事包上限;反代与 CSRF)
- 使用手册 / 常见问题 / 部署指南 / 上线检查清单按实现同步
|
2026-09-11 23:46:59 +08:00 |
|
AskaEth
|
8a27f5a4fb
|
fix(admin): 修复基础数据「运动员」页整块空白(TDZ 崩溃)
AthletesSection 里 filteredIds 在第 73 行引用了第 272 行才声明的 filtered,
而第 84 行的 watch(filteredIds, ...) 在注册时会同步求值一次源(Vue 行为),
setup 阶段就访问到 TDZ 中的 filtered → ReferenceError:
Cannot access 'filtered' before initialization → 整个分区渲染失败,
页面只剩标签栏、下面全白。自 631c8dd(上一轮)起 dev_0.1.6/0.1.7/0.1.8 均带此缺陷。
修法:把 filtered 的定义上移到 filteredIds 之前(只依赖三个筛选 ref,上移安全),
并加注释说明为什么不能挪回去。
验证:真 Chromium(Playwright)逐个打开 14 个管理端路由 + 7 个基础数据标签,
console 错误与 pageerror 均为 0;「运动员」标签正常渲染出表格与空态。
版本 dev_0.1.8 → dev_0.1.9(热修,P2 批次另行发布)。
|
2026-09-11 22:51:11 +08:00 |
|
AskaEth
|
45edfbeb71
|
fix: P0 计分根因 + 奖牌结算/奖牌通道 + 身份口径改回证件号 + 19 条 P1(dev_0.1.8)
## P0:双通道项目重复发布跳分(45→85→收敛)
results.rank 一列同时充当「个人名次输出」「手工名次输入」「榜单数据源」,
团体手工通道把个人名次当团体名次又按团体表发一遍分。
- 新增 channel_ranks(每个计分通道各自持久化名次,发布时全量重建)
与 channel_manual_ranks(按单位录入的团体手工名次,故意不挂通道外键,
避免规则包重导时级联删掉录入数据)
- results.rank 语义收窄为「只表示个人名次」,只由 scope=athlete 通道写回;
团体手工名次的来源改为 TeamRanks 优先、团体报名项兜底,
个人报名项的名次绝不进团体通道
- writeLedgerTx 内重建 channel_ranks;buildRanked / export.loadRanked /
realtime.loadRanked 三份同源 SQL 一起改读新表
- 回归:发布两次流水与名次逐字节一致(domain + httpapi 各一条,含变异验证)
## 奖牌结算开关与奖牌通道(P1(5),新功能)
- events.award_medals + 通道 count_to_medals / count_to_team_medals
(迁移默认 1,升级不改变既有榜单)
- 单位榜入选 = 小项结算奖牌 && 通道发奖牌 && 前三名 &&(团体项目 || 个人奖牌并入单位)
- 个人榜只取个人通道;金牌榜与积分榜同源同数据、按通道过滤独立计算
- 前端:小项「该项启用奖牌结算」、通道「发奖牌/个人奖牌并入单位金牌榜」、
录入页新增「团体名次」区块(含新接口 GET/PUT .../team-ranks)
## 身份口径改回证件号(P1(15),推翻第三十六轮)
- 匹配顺序恢复 证件号 → 号码布 → 姓名+单位;新增 athleteByIDCard
- 同表重复证件号 = 同一人报多个组别(notices 提示,不报错);
只有「同证件号 + 同小项同组别」才是真重复(conflicts[].duplicate)
- 补齐后端缺失但前端/文档已在依赖的三处契约
## 其余 P1
- 安全:会话撤销断开 ws、实时层按角色裁剪、整库快照限流(含 hello 路径,
被限流的请求挂起并由心跳兜底补发,不丢刷新)
- 错误分级:手填名次非正数 / gender / 组别年龄 / 导出不存在的小项与单位 /
积分模板 awardRanks → 400 或 404,不再 500
- 算分一致性:破纪录加分看得失状态;批量退赛·恢复·移出、作废成绩、
报名导入改状态一律无条件重算推榜(不受延迟结算约束,与实时模式逐字节等价)
- 配置改动触发重算:通道 / 小项参数 / 积分模板
- 升级自愈:channel_ranks 空而已有已发布成绩时,启动时一次性全量重算
(服务对外监听之前完成,避免升级后奖牌榜全 0)
- 示例数据模式下破坏性操作被阻断,不再假成功
- Console:修复「只剩一块屏」后屏幕监听器被永久摘掉
- 大屏模板引擎:嵌套深度与体积安全阀,任何输入都不抛异常
## 文档
- docs/:使用手册(奖牌开关、奖牌通道、团体名次、枚举列留空口径)、
常见问题(奖牌排查、升级后奖牌榜为 0、历史重放已知限制)、
部署指南、上线检查清单
- 版本 dev_0.1.7 → dev_0.1.8(console/package.json 同步)
自检:服务端 13 包 ×2 全绿;前端 vue-tsc + 449 例 ×2 + pnpm build;
Console 35 例 + 语法检查。
|
2026-09-11 21:47:07 +08:00 |
|
AskaEth
|
08a5740fb2
|
feat: 分批发布 + 实时赛况页 + 显式批次层 + 成绩框实时提示 + 窄屏适配 + 版本号单一真相
## 分批发布(现场:100 米跑道只有 8 条,不能等所有人跑完才发)
- 空成绩从"错误"降级为"提示",不再阻塞发布;真会算错(写法无法识别、秒位 ≥ 60、
手动名次缺名次)仍挡住。发布确认框说明"只发已填的 M 人,其余保持未发布"。
- 服务端核实:publishResults/computeEventScore 本就不会因缺成绩报错
(collectCandidates 直接跳过没有 results 的报名项),根因在前端防呆。
## 实时赛况(新大屏页 + 定向接口)
- 新增 `GET /api/display/live`(guest 可读):返回「当前项目」判定 + 该项目**全量**已发布成绩
(不受快照 latestResults 的 20 行截断)。回退链:manual 指定 > 排期命中 > 最近发布。
设置键 display.live_events / display.live_source。
- 大屏新页 `live`「实时赛况」:1 个项目单栏、2 个自动左右分栏;失败保留上一次画面
(下一场开始前持续显示上一场);30s 轮询(判定依赖当前时间)+ 快照变更触发。
- 管理端「大屏控制」新增「实时赛况」卡;该卡走 PUT /api/settings(admin-only),
故对非管理员整组置灰 + 提示条(不新增角色)。
## 显式批次层(组别之下)
- 迁移 0019 `entries.heat`;批次仅是录入/展示分组,**不参与算分**(有"分批与不分批结果
完全一致"的断言守住)。批量设置接口 + 导入 CSV 可选列 heat + 录入页批次下拉 +
一键均分(按 N 人切片,已有批次号接着往后排)。
## 成绩输入框实时悬浮提示
- 聚焦格旁的浮动气泡:pointer-events:none、不占布局不改行高、贴底自动上翻;
合法显示解析结果(1:06 → = 66 秒)、可疑/不合法显示原因并带那一格原文;
与发布防呆共用同一套纯函数(entryGuards.ts),口径不会分叉。
## 窄屏 / 手机端布局
- 真浏览器(Playwright + chromium)在 1440/1024/768/390 四宽度逐页审计 + 溢出探针,
四轮迭代修复;桌面端 1440 前后像素差异 < 0.25%(无回归);修掉如「修改密码」输入框
塌成 22px 这类真问题。
## 版本号单一真相
- 新增仓库根 `VERSION`:构建时注入二进制(-X main.version)并同步 console/package.json
的版本(产物名跟随)。源码里不再有人工维护的版本号(兜底值改为 `dev`,故意不写数字)。
测试:服务端 13 包全绿;前端 vue-tsc 通过、249 例全过;Console 18 文件。
docs/ 与 devdocs/ 同步(FAQ 到 Q80、新增 devdocs/16 自定义大屏模板设计文档)。
|
2026-09-11 04:05:02 +08:00 |
|
AskaEth
|
793d2ac064
|
fix(console): 修「全屏时反而画标题栏」——窗口模式必须与 URL 的 windowed 一致
用户实机反馈:单屏窗口化窗口按 F11 全屏后标题栏不消失,退回窗口化反而没标题栏,
表现与预期完全相反。
根因不在标题栏,而在 Console 侧的窗口模式管理:entry 没记录窗口是以什么模式建的,
layout() 每次排布都拿「整机是否单屏」的 single 去覆盖每个已存在窗口的几何与模式
(!single 时会 win.setKiosk(true) + 铺满),而页面 URL 里的 windowed=1 是建窗时定死的、
loadPage 只在页码变化时才重载 —— 于是窗口的真实模式与页面认知脱钩:
窗口化窗口被改成 kiosk 全屏而 URL 仍带 windowed=1(全屏却画标题栏),反向亦然。
修复:建立并保证「窗口几何/模式 ↔ URL 的 windowed 必须永远一致」
- entry 记 { win, page, single, manual, reloadTimer }
- createWindow(..., manual=false);openOn() 传 manual=true(手动开的窗口不被排布改模式)
- layout() 的 existing 分支:target = manual ? entry.single : single,applyGeometry 与
loadPage 都用同一个 target,且 modeChanged || page 变了都要重载 URL
- 删掉 isKioskWindow()(用 isFullScreen() 反推会把「窗口化窗口被 F11 全屏」误报成 kiosk),
换成 modeOf(entry)
- F11 通信时序:enter/leave-full-screen 回调里不再回读 win.isFullScreen()(切换瞬间可能读到
旧值,页面会收到相反状态),改为下发事件本身的确定值;F11 直接下发算出来的 next
回归测试 console/main/displays.test.js(8 例,assertModeConsistent 四处自洽)。
反向对照:修复前的 displays.js 跑同一份测试是 1 pass / 7 fail。
Console 语法检查 18 个文件、node --test 28 例全过。
|
2026-09-11 00:13:09 +08:00 |
|
AskaEth
|
9d4ee0946c
|
feat: 运动员彻底删除(真物理删除 + 重算积分)+ 报名表 status 容错 + 部署包带导入规范
## 运动员「彻底删除」= 真删除
`DELETE /api/athletes/{id}?purge=1`(不带参数仍是「停用」软删除)。在一个事务里:
删积分流水(含已作废历史流水)→ 删破纪录登记 → 删成绩 → 删报名 → 删人员 →
对受影响的 (小项, 组别) 调 recomputeLedgerTx + applyComputedRanks 重算流水与名次 →
bumpEvent/bumpStandings 推大屏 → 审计 athlete.purge(含删除规模)。
响应带 {deletedEntries, deletedResults, deletedLedger, deletedBreaks, recomputedEvents}。
必须先删 results 再删 entries(results.entry_id 无 ON DELETE);流水按 athlete_id / entry_id /
result_id 三种引用一把清,漏一种就留悬挂。
管理端「运动员」行加「彻底删除」按钮 + 危险确认框(如实写明报名与成绩一并删除、
相关小项排名与积分会重算),成功 toast 报出删除规模。
## 报名表 status 列容错
新增 entry_status.go 的 normalizeImportStatus:大小写不敏感(DNS/Registered 都认),
认「正常/退赛/弃赛/请假/取消资格/犯规/未出场/未到/缺席」等中文别名,留空即 registered,
认不出则给中文行级错误并跳过该行。此前原样入库,撞 entries.status 的 CHECK 约束后
抛原始 SQLite 错误,填表人看不懂。
## 修 isUniqueViolation 把 CHECK 失败误报成「已存在」
SQLite 的 CHECK / NOT NULL 失败也带 "constraint failed",旧匹配把「小项 status 取值非法」
报成 409「小项编码已存在」,现场会去找一个根本不存在的重名编码。收紧为只认
"unique constraint failed"。实测:重复编码仍 409;非法 status 不再误报。
## 安装包平铺《报名名单导入规范》
deploy/prepare-extra.mjs 改清单式,部署包根目录除 使用说明.txt 外新增
报名名单导入规范.txt。
测试:服务端 12 包全绿(新增 athlete_purge_test.go、entry_status_test.go);
前端 vue-tsc 通过、166 例全过;Console 语法检查 17 文件。
另在真二进制 + 真 SQLite 上跑通破坏性路径:删人后本人痕迹全为 0、他人不受影响、
榜单按新排序重算、foreign_key_check 无悬挂。docs/ 与 devdocs/ 同步更新。
|
2026-09-10 22:37:25 +08:00 |
|
AskaEth
|
fc96d17660
|
feat: 字体可配并随赛事包打包 + 大屏调试标识收敛 + 报名表 CSV 编码兼容
三块改动,均为现场反馈:
一、字体配置与随包打包(此前「没有入口可以配置字体」,导出包也不带字体)
- 服务端新增 httpapi/fonts.go 与 4 条路由:GET /api/fonts(recorder)、
POST /api/fonts(admin,multipart file)、DELETE /api/fonts/{name}(admin)、
GET /uploads/fonts/{name}(匿名——大屏 @font-face 不带会话)
- 上传限 ttf/otf/woff/woff2、单个 20 MiB;文件名经 safeFontName 收敛且需与
fontContentType 往返一致,挡住路径穿越;重名 409、删除被引用字体 409
- 新增设置键 display.font_title / display.font_body(DTO fontTitle/fontBody,
随快照下发给大屏),写入时校验取值合法
- 导出:接上一直没被赋值的 IncludeFonts/FontsDir;包内落点用 uploads/fonts/
(运行时只读这里,落顶层 fonts/ 等于没人读)
- 大屏按配置动态注册字体并挂 --display-font-title/body,加载失败静默回落默认字体
- 管理端「赛事信息」新增字体区块:标题/正文字体下拉、上传、字体库、实时预览
二、大屏显示收敛
- 数据版本号角标 vN 彻底移除(原先只有窗口化窗口会显示,两处显隐口径不一致)
- 窗口化窗口按 F11 全屏后自绘标题栏不让位:根因是 Console 用 win.setFullScreen()
做窗口级全屏,而页面用 document.fullscreenElement 判断,后者恒为 null;
新增 display-preload.js 下发窗口级全屏状态,页面侧 shell.ts 把两级全屏取或,
无 preload 时(浏览器直连 /display)回落到文档级监听
三、报名表 CSV 编码兼容(用户报「解析中文字符遇到编码问题」)
- 服务端不再把原始字节直接喂给 csv.Reader:先摘 UTF-8 BOM,再认 UTF-16 LE/BE
BOM,其余非合法 UTF-8 按 GB18030 解(GBK 是其子集)
- 修复前症状:GBK 文件(Excel 另存为 CSV 的默认编码)中文变乱码;带 BOM 的文件
(Excel「CSV UTF-8」,也正是我们自己模板的格式)把 BOM 拼进第一列表头,
报「报名表缺少列 event_code」——连「下载模板→填→传回来」都走不通
- golang.org/x/text 由 indirect 提升为直接依赖,未新增外部依赖
测试:服务端 12 包全绿(新增 fonts_test.go / fonts_internal_test.go /
handlers_import_encoding_test.go);前端 vue-tsc 通过、pnpm build 通过、157 例全过;
Console 语法检查 17 个文件通过。docs/ 与 devdocs/ 同步更新。
|
2026-09-10 17:45:11 +08:00 |
|
AskaEth
|
407abec3b0
|
fix: 大屏背景图被兜底渐变盖住 + 配置/规则也搬到用户数据目录
1) 大屏背景图不生效:App.vue 里 .bg 与 .bg-fallback 都是 absolute inset:0,
而兜底渐变是不透明的且在图片之后绘制 → 配了背景图也只看得到渐变。
改成 v-if/v-else 互斥(用无头 Chromium 真渲染对照验证:修复后取到图片像素,
回退版本只剩渐变)。新增 background-layer.test.ts 锁住层次关系。
2) 规则/config 仍在安装目录,覆盖安装/卸载会连配置一起删:现在三样都落在
%LOCALAPPDATA%\ScoreboardConsole\(与数据目录同一基座):
- console\config.json(Console 读写都用这份)
- config.yaml(Console 用绝对路径 -config 传给服务端与 passwd)
- rules\(首次从安装目录复制过去,之后只认用户目录那份)
首次运行复制迁移、已存在不覆盖、不可写则回落安装目录并记日志(不影响启动);
开发态行为不变。备份脚本与文档同步。
注意 resolveExecutable 必须显式传部署包根目录,否则会去用户目录找 scoreboard.exe。
自检:服务端 11 包全绿;console 语法 16 文件 + node:test 20/20;
前端 vue-tsc + build、node --test 69/69。
|
2026-09-10 10:06:21 +08:00 |
|
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 |
|
AskaEth
|
6483acf9ad
|
feat: 人员年龄/号码布、图片上传、多显示器菜单、Console 两栏、exe 改名等一批改动
人员与报名:
- 人员唯一标识改为「证件号 或 号码布」二选一(0013/0014 迁移 + 导入匹配/回填/冲突)
- 人员新增「年龄」(0015 迁移):表单/列表/导入模板表头 11 列;冲突比较只在两边
都有值且不同时才算差异,旧数据不误报
- 冲突决策键改为「证件号优先 → 号码布」(前端原先只用 idCard,号码布行会互相覆盖)
- 报名选择器支持无证件号但有号码布的运动员
大屏与 Console:
- 大屏控制新增「显示器」菜单:可给指定显示器重建全屏窗口;单屏/主显示器强制窗口化,
限制在 Console 侧判定,不信任页面传参
- Console 服务管理窗口改左右两栏,运行日志独占右栏满高;窗口默认尺寸 1280×860
- 大屏右上角时钟精确到秒(同时把刷新间隔 15s 改为 1s)
图片与配置:
- 新增图片上传 POST /api/uploads(recorder+,png/jpg/webp/gif,≤5MiB)与匿名静态
访问 GET /uploads/{name}(防目录穿越);文件落赛事项目目录 uploads/,随赛事包
一起导出/导入
- 「赛事信息」新增赛事会标 / 大屏会标 / 大屏背景图三个上传项
- 参赛单位 logo 输入框右侧加上传按钮
关于页与构建:
- 构建时间打通:version.BuildTime + /api/system/about 返回 + build.bat 注入
(用 PowerShell 取时间戳,避免本地化 %DATE% 的非 ASCII 星期)
安装包:
- 主程序改名 Scoreboard_Console.exe(win.executableName;productName/显示名不变)
- 记住上次安装目录:自定义 NSIS 宏优先沿用自存路径(卸载不清除),并在 customInit
里补写回 electron-builder 的键,避免安装模式页把 $INSTDIR 打回默认
自检:服务端 12 包全绿;前端 vue-tsc + build 通过、node --test 58/58;console 12 文件通过。
|
2026-09-10 09:05:06 +08:00 |
|
AskaEth
|
0c9a6b6a27
|
fix(console): 开机自启改为「注册但不启用」,开关切换 enabled
上一版把「关闭」实现成不写登录项。实际在 Windows 上实测 Electron:
setLoginItemSettings({ openAtLogin: true, enabled: false })
→ openAtLogin=true、launchItems[0].enabled=false、executableWillLaunchAtLogin=false
即登录项已注册(任务管理器「启动」里可见、显示为已禁用)但不会随系统拉起。
所以改成始终注册登录项、只用 enabled 控制是否真正启动,语义与用户要求一致:
- applyOpenAtLogin / setOpenAtLogin 都走 loginItemSettings(wanted)
- macOS 无 enabled 参数,仍按注册/注销处理
- 状态快照的 openAtLogin 以系统实际状态为准(打包后读 executableWillLaunchAtLogin)
- 文档同步:默认「已注册但未启用」
|
2026-09-10 00:09:49 +08:00 |
|
AskaEth
|
1b78c38bb8
|
feat(console): 开机自启默认关闭,服务管理窗口加手动开关
- DEFAULTS.startup.openAtLogin 与随包 config.json 默认改为 false;
applyOpenAtLogin 只有显式 true 才注册登录项(缺省/null/false 都不注册)
- config.js 新增 updateConfig():只写回文件里已有的键 + 目标键,不把 DEFAULTS 落盘
- 主进程 setOpenAtLogin():改内存 → 写回 config.json → setLoginItemSettings,
任一失败都回滚,返回 {ok, openAtLogin};状态快照新增 openAtLogin / openAtLoginSupported
- 渲染层:服务端状态卡片新增「开机自启」滑块(纯 CSS,无依赖),
切换即时生效并有本地意图守卫,避免被随后到达的陈旧快照冲掉
- 文档:console/README.md 与 docs/部署指南.md 同步
注意:旧包 config.json 里已显式写 true 的用户升级后仍会自启(不悄悄改既有行为),
在窗口里关一次即可;全新安装默认不注册。
|
2026-09-10 00:06:59 +08:00 |
|
AskaEth
|
13f1f03cd0
|
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 文件通过。
|
2026-09-09 22:16:58 +08:00 |
|
AskaEth
|
2c9ff65fc4
|
feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)
- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对)
与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的
重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端
- export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep),
自动、手动、发布前三类备份共用同一把互斥锁与滚动清理
- realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行,
跨站需命中 server.allowedOrigins)
- console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀),
服务管理器新增「已连接的大屏实例」卡片
- deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空,
导致最近这次构建的 exe 里根本没注入署名
- docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
|
2026-09-09 21:41:08 +08:00 |
|
AskaEth
|
33d4b39eb9
|
fix(build): pnpm 11 用 allowBuilds 放行 electron 安装脚本
pnpm 11 起 onlyBuiltDependencies 列表形式被静默忽略,导致
ERR_PNPM_IGNORED_BUILDS 让 pnpm run 的依赖校验直接失败(pnpm run dist 都起不来)。
改为 pnpm 自己写入的 allowBuilds 映射形式,并去掉 build.bat 里的 --ignore-scripts。
另外修复 set VAR=value && ... 的尾随空格:它会把 ELECTRON_MIRROR 变成
'.../electron/ v38.8.6/...' 从而 404。
|
2026-09-09 21:35:21 +08:00 |
|
AskaEth
|
52edecfa4c
|
fix(build): 钉 @electron/get@3.1.0 修 electron-builder 打包崩溃
app-builder-lib@26.15.3 声明 @electron/get ^3.0.0,但代码用了 3.1.0 才导出的
ElectronDownloadCacheMode,导致 pnpm run dist 报
TypeError: Cannot read properties of undefined (reading 'ReadWrite')。
pnpm-workspace.yaml 里 override 到 3.1.0 并重新生成锁文件。
|
2026-09-09 21:30:50 +08:00 |
|
AskaEth
|
c3d2c1416f
|
fix(build): console 安装用 --ignore-scripts,绕开 pnpm 11 的 ERR_PNPM_IGNORED_BUILDS
|
2026-09-09 21:10:21 +08:00 |
|
AskaEth
|
1399bae59e
|
fix(build): build.bat 全 ASCII + Node 脚本处理中文路径 + pnpm-workspace.yaml 构建脚本白名单
|
2026-09-09 21:09:38 +08:00 |
|
AskaEth
|
de4bd63594
|
fix(build): build.bat 块内改 ASCII(cmd 对 UTF-8 中文块解析错乱)+ pnpm 允许 electron 构建脚本
|
2026-09-09 21:09:02 +08:00 |
|
AskaEth
|
7f9701c465
|
fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力
- realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份
- httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表
- web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换
- console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复
- docs: 用户文档三份 + README
- 主代理: 破纪录加分接线(levels 未加载导致恒 0)
|
2026-09-09 21:06:40 +08:00 |
|
AskaEth
|
b80c64a115
|
fix: 补 .gitattributes(CRLF for .bat) + README 端口 19540 + 提交 console 锁文件
|
2026-09-09 20:57:58 +08:00 |
|
AskaEth
|
c121111f51
|
build: 快捷方式名改为中文「赛事积分榜」
|
2026-09-09 20:41:39 +08:00 |
|
AskaEth
|
7f183d5327
|
build: 安装器许可页顶部文案自动注入内部署名(构建时生成 .nsh)
|
2026-09-09 20:39:02 +08:00 |
|
AskaEth
|
1bc439d8e1
|
build: 补充 console/package.json author,修正 exe CompanyName
|
2026-09-09 20:25:33 +08:00 |
|
AskaEth
|
2f75c31509
|
build: 回归 electron-builder 打包(win.icon 写入 PE 图标),下载走 gh-proxy
|
2026-09-09 20:20:04 +08:00 |
|
AskaEth
|
ad3770cfc9
|
build: 用 rcedit 给 Scoreboard Console.exe 嵌入图标与版本信息
|
2026-09-09 20:19:02 +08:00 |
|
AskaEth
|
5ae55b8057
|
fix(server): 赛事包导出传入项目名,修复 GET /api/export/project.zip 恒 500
|
2026-09-09 19:57:23 +08:00 |
|
AskaEth
|
72462792ec
|
feat(server): 用户管理与变更历史接口;修复 result_revisions.parent_id 从未写入
|
2026-09-09 19:51:50 +08:00 |
|
AskaEth
|
f3813171d3
|
fix(console): 停止服务时同步关闭扩展屏大屏窗口
|
2026-09-09 19:38:09 +08:00 |
|
AskaEth
|
ab2f4d1cec
|
feat(admin): 修改密码页 + 首次登录强制改密;自绘标题栏与管理面板 Electron 窗口
|
2026-09-09 19:30:59 +08:00 |
|
AskaEth
|
4cbf5a74f0
|
feat(server): 新增修改密码接口 POST /api/auth/password(验原密码、作废会话)
|
2026-09-09 19:26:35 +08:00 |
|
AskaEth
|
e47251658b
|
fix(console): 默认端口改 19540;打包改为手动组装便携版(不依赖 GitHub 下载)
|
2026-09-09 19:15:13 +08:00 |
|
AskaEth
|
c92b404f6e
|
chore: 服务端端口 8080 → 19540
|
2026-09-09 19:09:05 +08:00 |
|
AskaEth
|
7d5d94f815
|
feat: M0-M2 骨架落地(服务端/前端/宿主/数据库/积分引擎)
- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由
- store: system.db + event.db 两套迁移,业务表 + 种子数据
- domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试
- web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端)
- console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
|
2026-09-09 18:36:40 +08:00 |
|