Commit Graph
6 Commits
Author SHA1 Message Date
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