AskaEth
|
1b8a37ff8c
|
feat(import): 报名模板按「小项 × 组别」分工作表;号码布判重改认证件号;备份改原子落盘
三件事合成一次提交(其中两件都落在 handlers_config.go,无法按文件拆开):
一、分工作表报名模板(新增能力)
- 新增 GET /api/entries/template.xlsx:按当前赛事的「小项 × 组别」生成模板,
一个小项组别一张工作表,工作表名 = <小项>@<组别>(31 字符上限、重名自动去重)。
- 导入解析器认工作表名还原小项 + 组别;若与表内列冲突则报错,不静默取其一。
- 小项名与组别名禁用 @,基础数据界面 / 规则包导入 / 服务端校验三处一并拦截。
- 单表 CSV/XLSX 导入路径不回归。
二、号码布判重(修误判)
- 旧逻辑依赖证件号本身,而证件号是可选字段 → 同一个人报多个项目会被整块拦下。
- 改为:号码布相同且证件号相同 → 判定为同一人报多项,放行;
号码布相同且证件号都能填且不同 → 仍报错(真抢号);一方证件号为空 → 放行并给提示。
- 团体行的号码布一并纳入查重。
三、自动备份的关停与落盘(修数据安全隐患)
- export.Service 增加 Wait():StartAuto 的协程进 WaitGroup,关停时 join(10 秒上限,
超时只记 WARN 不拖死关闭),不再在备份写到一半时退出。
- vacuumInto 改为先写 <最终名>.tmp-<随机> 再 rename,失败则删临时文件;
Backup / SafetySnapshot / ExportProjectZip 三个调用方一并受益。
- 新增 isFinalBackupName,ListBackups / safetyCopies / KindOfBackupName 统一过滤中间文件,
避免半截 .db 被列进备份列表、进而被「一键恢复」选中。
文档:docs/ 五份(使用手册 / 报名名单导入规范 / 升级说明 / 部署指南 / 上线检查清单)同步。
|
2026-09-14 01:10:16 +08:00 |
|
AskaEth
|
b74768b7d5
|
fix(display): 实时赛况不再挤压省略 —— 单位列宽由内容算,字号把单位列一并纳入预算
主人报:大屏「实时赛况」页在两栏显示时,单位名较长、成绩较长(如 1:12.66)会被挤压显示省略号。
实测根因:**单位列宽写死**(`.boards-2 .row` 的 `60px minmax(0,1fr) 190px …`)而单位名墨迹实测
206~343px → 必然进省略态;单栏 524 vs 694、split 166 vs 437 同理。弹性宽度全给了运动员列。
更关键的是 `fitFont.ts` 的 `fitLiveRowFontPx` **只把成绩列纳入字号计算**,单位列从未进过这笔账 ——
所以字号自适应对它一直没生效。
改法:
- `fitFont.ts` 新增 `LiveRowGeometry`(各版式行宽 1710/836/815、名次列、间距、单位列下限与预算)、
`liveTeamColumnPx`(按单位名墨迹定列宽,含 logo 1.28em)、`liveNameColumnPx`;
`fitLiveRowFontPx` 增加 teams/teamsWithLogo 参数,按「名次+名字(≥4 汉字)+单位+成绩装得进一行」逐像素收字号;
`resultTextEm` 更名 `textEm`(成绩与单位名共用同一套字符宽度表)。
- `LivePage.vue` 把单位名喂进字号计算并导出 `--team-width`,网格改用该变量。
- **取舍**:以收字号换完整显示(split 最狠 56→41px);单位列封顶 380/430/900px,超过才由单位名省略
—— **成绩与名次在任何情况下都完整**(观众最关心的先保住)。
肉眼验收(1920×1080,ReadMediaFile 逐张看过):两栏 + 10 字单位名 + 12:34.56 → 全完整,43px(改前 44px);
单栏同样全完整,88px;**对照组短单位名网格改前改后完全一致、0 省略**(常态数据零变化)。
测试:fitFont +5、live-page +1(单位列宽由内容算、网格不得再有写死单位列、必须把单位名喂进 fitFont),
并修正 2 条因网格写法变化而失真的既有断言;改前跑这些用例会红。前端 767/767、vue-tsc 通过。
未验证:真机 4K/投影观感与真实服务端数据(本次数据为接口桩)。
|
2026-09-13 23:52:52 +08:00 |
|
AskaEth
|
2888200045
|
feat(entry): 录入界面加「补报名」入口 —— 移出报名后可以重新指定项目/组别
需求(主人):「导入时给运动员指定了绑定的项目和组别,但在录入界面把该项报名取消之后,
没有任何地方可以把已录入的运动员重新指定项目报名。」
其实功能一直有(`EntryPicker`「从已有名单勾选报名」,支持小项+组别+个人/团体),
只是它只出现在「基础数据 → 报名导入」页里 —— 用户在哪儿取消的、就去哪儿找,这是入口位置问题。
- `EntryPage.vue`:工具栏右侧(组别页签同一排、表格正上方)加「补报名」按钮;
只读/示例数据模式禁用;走与批量操作同口径的三道拦截(示例数据/只读/有未保存修改);
点击时按需加载基础数据这份名单;提交后若提交的是本页当前小项则整表重拉并提示。
- `EntryPicker.vue`:新增 `dialog` / `presetEventId` / `presetCategoryId` 三个 props 与 `submitted` emit,
预选当前小项与组别(组别对不上时回落到第一个)。**不传 props 时渲染与行为与以前完全一致**,
「基础数据 → 报名导入」页的内联用法零改动(有测试钉着 `<EntryPicker />` 仍在)。
- 移出报名后的提示补一句指路「如需重新报名,点上方『补报名』…」;本组空了的空态同样指向「补报名」。
- 文档:使用手册 4.8 / 10.7 补操作路径,常见问题 Q91(批量移出那节)补「移出后想加回来」。
验证:`vue-tsc` 通过;前端 761 例全过(基线 754,新增 7)。
接口级 E2E(独立数据目录)走通「勾选报名 → 批量移出 → 补报名」,名单恢复 1 人、无重复建档、未写进别的组别。
浏览器 E2E(真实 Chromium + 临时同源代理)走通「按钮可见 → 打开同一 picker 且预选 100米/男子组 →
加入后出现在成绩表 → 移出提示 → 空态指路」,无 console 报错。
未验证:弹窗里「团体报名」页签的实际提交、窄屏布局、只读/示例数据两道拦截(仅代码核对)。
|
2026-09-13 23:46:52 +08:00 |
|
AskaEth
|
9951a69938
|
feat(import): 报名导入支持中文表头,并给出中/英两套模板下载
需求:主人要「CSV 模板的表头能改一下,或者提供两个按钮,一个中文表头一个英文表头」。
- 解析器加**中文别名**(英文仍是规范名:取值、报错、纯英文模板、勾选导出都用它)。
12 列,顺序即模板列序:项目编码 / 组别 / 单位编码 / 姓名 / 证件号 / 性别 / 年龄 /
号码布 / 种子 / 批次 / 状态 / 队员名单;另认一批同义写法(小项编码、参赛单位编码、
运动员姓名、身份证号、道次、批次号、报名状态…)。
Go 别名表 == 前端 COLUMN_ALIASES == 导入规范文档表格,三方逐列逐名一致(脚本核对)。
- **向后兼容**:纯英文表头一字不改照常导入;回归用**字面量**(不复用常量)钉死老 11 列表头
与大写/空格变体,避免有人改常量把回归一起改掉。
- **中英可任意混排**,但**同一列出现两种写法(或写两遍)整份拦下**并说明——两列都可能带数据,
静默取其一等于丢另一列。容错沿用既有先例:BOM(含格子中间的 U+FEFF)、全角空格/全角 ASCII、
首尾空格、大小写。
- 缺列报错**中英并列**(「缺少列 athlete_name(姓名)」),现场拿中文模板的人一眼看懂。
- 前端两个下载按钮:「下载模板(中文表头)」「下载模板(英文表头)」,列序与列数完全一致,
文件名可区分;示例数据模式(mock)同步,避免两条路径产出不同模板。
- 文档 `docs/报名名单导入规范.md` 补两套表头与「两套都能导入」的说明(原内容未删)。
端到端(真服务端 + curl):中文模板填两行 → {"created":2} 且库中值取自中文列;
英文模板同一份数据 → {"updated":2} 不重复建档;缺列与表头重复分别给出上述人话报错。
自检:gofmt / go build / go vet / go test 全绿;vue-tsc + 前端 754 例全过。
|
2026-09-13 21:55:37 +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
|
db94d9c119
|
test(web): 补两类守卫,专抓「组件 setup 期抛异常」这类既有测试抓不到的问题
背景:基础数据「运动员」页整块空白的根因是 setup 期 TDZ(watch 注册时同步求值,
间接引用到后面才声明的 const),vue-tsc 与 449 条单元测试都没抓到——组件从不真正挂载。
1) scripts/check-declaration-order.mjs + src/shared/tests/declaration-order.test.ts
静态检查:从 watch/watchEffect 的源出发沿 const 依赖链做传递闭包,
任何声明行晚于求值行的 const 即判违规。已随既有测试命令运行(纯 node,无需浏览器)。
变异验证:把 AthletesSection 退回 ab7662b 的写法 → 精确报
「AthletesSection.vue:84 会求值到 filtered,但它声明在第 272 行」。
2) scripts/smoke-admin.mjs(pnpm smoke)
真 Chromium 冒烟:14 个管理端路由 + 7 个基础数据分区 + 大屏页,
任何 console 错误或未捕获异常即失败并给出路由名。
变异验证:同一份代码下修复版通过、退回版报
「/base-data › 运动员 ← ReferenceError: Cannot access 'Y' before initialization」。
playwright 不是本仓库依赖(离线开发装不上),脚本缺依赖时会明确报错退出而不是假装通过。
|
2026-09-11 22:58:19 +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
|
53a0cb225c
|
feat: 备份恢复 / 图片清理 / 单位恢复 / 审计过滤 / 模板引擎 M1 + 两个导入缺陷修复
用户点名的三件事(模板功能、删死代码、剩余缺口)这一批全部落地,另修掉两个刚暴露的导入缺陷:
**新增能力**
- 备份恢复:`POST /api/backup/restore`(admin)。先把校验做在拍快照之前(只读打开 + integrity_check +
关键表校验 + 异类库标志表互斥),再用 VACUUM INTO 拍「恢复前安全副本」,然后关连接、整文件替换、
触发优雅重启;`-wal` 还在时直接拒绝(宁可失败也不让旧库 WAL 套到新库上);恢复后审计直写"恢复之后
还在的那份库"(不走通用 Auditor,否则记录只会落在被替换掉的旧文件上)。顺带修掉:裸服务端连续恢复
第二次会在拍副本处崩出一句看不懂的 500,现在回 503「上次恢复后尚未重启,请先重启服务端」。
- 安全副本保留策略:每类库保留最近 5 份,独立于 .db 的 20 份滚动清理。
- 图片管理:`DELETE /api/uploads/{name}`(被引用 409 并列出引用点)、`GET /api/uploads/orphans`、
`POST /api/uploads/orphans/delete`(只删列出的孤儿,宁漏勿误),全部 admin(与字体删除同档);
`deploy/backup.bat` 补上 uploads/ 归档(logs 不归档:可再生的诊断且会无上限增长)。
- 已彻底删除单位的恢复:`POST /api/teams/{id}/restore`(与 hideTeam 同档)。幂等;恢复不会把「停用」
变成「启用」;真机确认恢复前后榜单逐字节一致、change_log 最大值不变(数据一行没动,不需要重算)。
界面加「显示已彻底删除的单位」(仅 admin)与恢复按钮;非 admin 的删除确认框里写明"本账号无法自行恢复"。
- 审计日志按 `action` 过滤(子串模糊,输入里的 %/_/\ 转义成字面量)。
- 删除死表 recent_projects(新增 system 迁移 0006,未动已应用的迁移)。
- points_override 不再"设了没效果":写入侧 400 + 引擎侧守卫,提示改用「积分调整」。
- 联队份额空壳清理(UnionAttribution/UnionSplit/Entry.TeamShares/resolvedShares 等;TeamShare 类型保留,
因为 export/realtime/奖牌流水三处在生产路径上构造它)。
- 自定义大屏模板 M1:插值引擎(parseTemplate/renderTemplate),任何情况都不抛异常、未定义→空串+警告、
警告按 (code,path) 合并计数、无 eval/innerHTML,57 个新用例。
**两个真缺陷(前两个已随 0.1.6 出包)**
- 基础数据包导入「排序」列口径不一致:小项/组别留空保持原值,单位/大项留空被写成 0(plan 没从库里
现值起手)→ 四张表统一为「留空保持原值」。
- 同类第二处:布尔列「整列被删掉」被当成"取默认值",会把库里所有停用的单位/大项静默改回启用 →
改为「缺列 = 不动」(写侧与解析侧都收口,补了"缺列保持/空格子=是/显式否=停用"三组对照用例)。
**删死代码**
- createEntry / updateEntry / deleteEntry / entryEventID / checkCategoryInEvent / getEntry /
normalizeEntryStatus / ExportBaseDataXLSX(全仓 grep 零引用)。⚠️ 随之记入文档:「组别必须属于该小项」
这道校验现在只由导入路径"按小项+组别名解析"天然保证,将来若新增从请求体接收 categoryId 的接口,
必须自己补校验(库层没有复合外键)。
**文档**
- devdocs 全面同步(01/02/03/04/05/06/07/09/11/12/13/README;含身份口径、延迟结算、大屏单栏新版式、
新增路由与设置键、R18/R19 风险);docs 复核 + 第二轮(身份口径三个易写反的分支、图片删除、备份覆盖、
审计过滤、单位恢复、去掉硬编码版本号、上线检查清单 +5 项);debug/使用说明 6 份逐份 diff 一致。
测试:服务端 13 包(连跑两遍)、前端 406 例、Console 28 例 + 18 文件语法检查 —— 全绿。
版本号 dev_0.1.6 → dev_0.1.7。
|
2026-09-11 19:26:36 +08:00 |
|
AskaEth
|
631c8dd8e1
|
feat: 8 条功能 + 身份口径改为号码布为准(含批量退赛/移出、基础数据导入导出、登录有效期)
用户给出的 8 条功能 + 一条口径变更,全部带真机/真浏览器验证:
① 录入页姓名后直接显示号码布(原「号码」列保留)
② 录入页批次进度条:`第 2 批 已录 5/8 · 3 条有错`,录完变绿;已录判据复用
entryGuards.judgeEntryRow(与发布防呆同源,不会出现"进度条说录完、发布说缺 8 条")
⑦ 小项列表双击 → 跳到该小项的组别(编辑/删除按钮不误触)
③ 赛事开关 `standings.live_on_publish`(默认开=保持原行为):关闭后分批发布只上屏成绩,
名次/流水/榜单一起等该组录完再结算 —— 代理实读代码后纠正了我的前提:
奖牌榜读的是 results.rank(buildRanked)而非流水汇总,只延迟流水会出现
「有金牌但 0 分」。另提供 POST /api/results/settle 人工兜底 + GET /api/results/settlement 未结算清单
④ 组别「就地更新」PUT /api/events/{id}/categories/{categoryId}:有报名数据时也能改名/改年龄/
改性别/改权重(旧路径是整表替换,一改就被 409 拦下);改名不动报名/成绩/流水(真机逐字节比对);
权重真的变了才触发该组重算(容差 1e-9)
④辅 运动员列表多选+全选,POST /api/athletes/batch 一次请求完成整批停用/彻底删除(逐条回报,
dryRun 预览走同一段 SQL 但不提交、不写审计)——这才是绕开限流的正解
新增 报名项批量:POST /api/entries/batch(scratch/restore/remove)。退赛只动 registered、
跳过 dq/dns 并说明原因;移出是物理删除,必须显式清 score_ledger(该表 entry_id 是
ON DELETE SET NULL,不显式删会留下"人没了分还在"的孤儿流水,验证基线 orphan=0);
退赛同时清 results.rank(否则出现"0 分还挂着金牌")
⑤ GET /api/export/entries.xlsx 报名名单导出(13 列含批次,状态中文与录入页逐字一致)
⑥ 基础数据包:GET /api/export/basedata.xlsx + POST /api/export/basedata/import
(四张表、表头只有一份定义、表头行自动定位、列缺席≠写空、按编码 upsert、绝不删除、
行级中文错误带真实 Excel 行号、枚举走既有校验、新增单位一定是启用)
⑧ 登录有效期面板可配:系统设置新页签,1–720 小时,默认 12h → 24h;滑动续期 +
Cookie 同步刷新(只服务端续期是假的,浏览器侧的 Max-Age 到点仍会丢 Cookie)
身份口径:号码布为唯一标识、证件号退出匹配(号码布不同=同名的两个人,新建并单独报告);
同一份表重复证件号不再报错;两个不同的人共用证件号仍报行级错误(唯一索引保留)
其它对齐:分项成绩表草稿行不再消失(成绩列「分数未录入」、名次/积分 `—`,没成绩的行同占位);
大屏实时赛况单栏铺满(留白 370→78px)、字号上限 56→96px、≥7 行自动拆左右两块;
删除报名项/退赛/权重等改动都会重算并推榜单。
测试:服务端 13 包(连跑两遍)、前端 335 例、vue-tsc + build、Console 18 文件 —— 全绿。
文档:README、docs/ 四份(含 debug/使用说明 副本)、devdocs 05/08/04/13。
版本号 dev_0.1.5 → dev_0.1.6。
|
2026-09-11 17:14:02 +08:00 |
|
AskaEth
|
8dd599bd97
|
fix: 实机复测六批发现的 16 条缺陷(导入编码静默错误 / 导出观感 / 错误分级 / 大屏裁切)
先把「实机复测」能在这台 Linux 机器上验的全部自己验掉(真服务端 + 真 Chromium + 真样本),
六批全部通过,另挖出 16 条缺陷,逐条修完并复验:
导入
- 坏字节被静默替换成 U+FFFD 却报「组别不存在」→ 检测 RuneError 回 400 + 可操作中文文案
- UTF-16 无 BOM 误报「缺少列 event_code」→ 保守判据识别并正确解码
- CSV 语法错只有英文 → 中文提示,英文原文保留在 detail
- 表头缺列补「第 1 行应是表头」的指向
导出
- 名次列宽 8 → 11(放得下「3(并列)」,Excel 里不再硬裁)
- 运动员/队列宽 18 → 24(含奖牌明细的同一列)
- 聚合流水行补组别(仅单位在该小项只参加一个组别时回推;多组别宁可留空也不猜)
- docProps 作者从 excelize 默认的 xuri 改成中性名 + 真实导出时间
- Content-Disposition 补 ASCII 退化 filename(9 个下载端点,含此前没有测试的 project.zip)
错误分级与限流
- 未知大屏指令 500 → 400 VALIDATION_FAILED(传输层真失败仍走 5xx)
- 5xx 的 ERROR 日志补 role / ip(4xx 的 WARN 一直有)
- 写限流 30 → 120 次/分钟/用户(登录 5 次/分/IP 不动,性质不同)
- 赛事名含非法字符时导出给 400 + 可操作提示(原来永远 500「服务端异常」)
大屏与基础设施
- 实时赛况 2 栏成绩末位被裁(1:09.00 显示成 1:09.0)→ 成绩列按内容定宽 + nowrap;
字号不缩,让位给可省略号截断的运动员列。真 Chromium 实测 slack −45px → +6px、0 裁切、
scrollWidth 恒 1920(无横向滚动条);顺带修掉 1 栏长成绩同样被裁 8px 的问题
- logging 启动即预建两个日志目录(原来 interceptions/ 懒创建,现场会以为日志坏了)
- C→S 裸帧由静默忽略改为 WARN,并把「要套 {event,data} 外壳」写进日志
- 建单位/大项漏传 active 不再静默建成「停用」(新增 decodeJSONPresence 按键是否存在判意图)
文档
- README 单位 logo 措辞收窄到「大屏端」(管理端只负责上传配置)
- 导入规范补三类编码报错说明 + 现场另存结论 + 限流提示
- 部署指南补「导出前先看赛事名称」与「切换后怎么确认真的切过去了」
- devdocs 05/08/13 同步(含第三十五轮的完整记录与教训)
自检:服务端 13 包连跑两遍全绿;前端 257 例;vue-tsc + build 通过;Console 18 文件。
版本号 dev_0.1.4 → dev_0.1.5。
|
2026-09-11 15:35:42 +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
|
e5e0944d26
|
feat: 分级拦截日志 + 两类日志文件;成绩录入防呆/解析/宽度修复
## 日志(用户要求:分级 + 每个拦截都要有指出代码行的调试日志 + 两类文件)
- 新增 internal/logging:拦截点捕获(file:line + 函数名,相对路径)、两类文件轮转、
按级别/类别分流。httpapi 与 middleware 各自薄封装,无循环依赖。
- 两类目录(数据目录下):
operations/ 放行类(INFO「操作已放行」+ 常规运行日志,按 log.level)
interceptions/ 拦截类(WARN 中文原因 + 拦截点 + method/path/actor/role/requestId、
DEBUG 上下文、ERROR 服务端异常)—— **固定 debug 采集**,
否则默认 info 下"每次拦截的调试日志"根本看不见
- 命名 operations-YYYYMMDD[.N].log,按天+按大小轮转,每类保留 keepFiles 份;写盘失败
降级只写控制台且只 Warn 一次。
- 所有业务错误统一走 newAPIError 捕获拦截点(11 处内联 &apiError{} 已收进构造函数,
防止绕过);中间件层拦截(401/403/CSRF/未改初始密码/429)也留痕,响应体与状态码一字未变。
- config.yaml 的 log 段新增 dir / maxSizeMB / keepFiles(留空即默认)。
- 服务端包数 12 → 13(新增 internal/logging)。
## 成绩录入(用户实机踩坑)
- 真因一:成绩格 placeholder 是光秃秃的 `1:06.33`,而全仓库没有任何 ::placeholder 样式,
示例文本与真实输入长得一样 → 用户把灰字当成已填内容,那 13 格其实是空的。
- 真因二:2 条报错是秒位 ≥ 60(1:70)或全角冒号(1:06);1:06 与 1:06.00 在解析器里
本来就等价(都 = 66),格式不是问题。
- 真因三:成绩格没有宽度定义(旁边名次 72px、状态 110px 都有),被挤到第 4 位数字看不见。
修复:parseSecondsDetail 区分 empty/seconds-overflow/unrecognized;新增 entryGuards.ts
纯函数(录入页与审核页共用),报错**打出那一格的实际内容**并说明原因;解析器新认全角
冒号/全角数字/1分06秒 等写法;成绩格 min-width 112px(按 fontTools 实测文本宽度定);
示例文本改 `例 1:06.33` 等并给录入表格内的 ::placeholder 加淡色斜体;新增
onBeforeRouteLeave 离开守卫(原来切菜单会静默丢未保存成绩)。
测试:服务端 13 包全绿(372 个测试函数);前端 vue-tsc 通过、205 例全过;Console 18 文件。
docs/ 与 devdocs/ 同步(FAQ 到 Q76,部署指南新增第 10 节「服务端日志与拦截复盘」)。
|
2026-09-11 01:59:58 +08:00 |
|
AskaEth
|
8e707e1e9b
|
feat: 团体奖牌 / 个人积分两个榜单的专属明细导出
用户答复「允许细化」——给另外两个榜单各做一份逐项来源导出(此前它们的「明细」共用
单位积分明细、个人榜连明细列都没有)。
- `GET /api/export/medals.xlsx` → `团体奖牌明细.xlsx`:总表 单位/大项/小项/组别/名次/奖牌/运动员-队
+ 每个有奖牌的单位一张分表(末尾小计 金X/银Y/铜Z);无奖牌的单位不出分表。
去重与牌色判定**完全交给 domain**(每组调 domain.BuildMedalStandings),导出侧只做
「同一报名项归一组」,不复制任何计奖规则——双计通道只记一枚奖牌因此自动成立。
- `GET /api/export/athletes.xlsx` → `个人积分总明细.xlsx`:总表 单位/运动员/大项/小项/组别/
名次/成绩/得分/来源 + 每个有得分的单位一张分表(按单位分,不按人分)。
得分取**记到个人的原始分**(与个人积分榜同一口径,不看 count_to_team、不乘折算系数)。
- 管理端三个榜单的卡片头各一个导出入口(导出总明细 / 导出奖牌明细 / 导出个人明细),
展开单位时明细卡头是「导出该单位明细」;导出页仍是 5 项,两个新导出只在积分榜页触发。
护栏(跨端点一致性,都有实测断言):
- 奖牌明细逐单位与「团体奖牌榜」对齐,双计项目只出一行;
- 个人明细合计与「个人积分榜」一致,且把折算系数改成 0.5 后个人明细一分不变、单位总分变 5。
测试:服务端 12 包全绿(358 个测试函数);前端 vue-tsc 通过、190 例全过;Console 18 文件。
docs/ 与 devdocs/ 同步(FAQ 到 Q72,含上一轮"个人积分榜不能导出"的修正)。
|
2026-09-11 01:17:14 +08:00 |
|
AskaEth
|
782a38f82f
|
feat: 大屏控制可看局域网设备并逐台切页 + 两种新导出(单页成绩 / 单位积分总明细)
## 大屏控制(用户:管理面板只能看到本机显示器,看不到局域网设备;也不能给某块屏单独切页)
- 服务端:`POST /api/display/command` 新增可选 `targets []string` —— 空 = 广播(公告语义不变),
非空 = 只发给点名的实例并返回命中数;命中 0 个返回 404 +「目标大屏已离线,请刷新在线列表」
(不落审计、公告也不落库)。`DisplaySender` 签名随之扩展,新增
`Hub.SendCommandToInstances`(与按房间的 SendCommand 并存)。
- 服务端:WS `hello` 新增可选 `screen`,`realtime.Instance` 新增 `Screen`(不带 omitempty,
局域网浏览器恒为空串),这样管理端能把「本机显示器」与「在线实例」精确对上。
- 管理端「大屏控制」:写死的「在线看板」占位换成真实列表(5 秒轮询、页面隐藏停表),
列出实例 id / 在线离线 / 落后 / 屏号或「局域网浏览器」/ 当前页 / 来源 / 心跳;
每行可单独切页;本机显示器卡片补「当前页」与逐屏切页;离线时控件置灰并说明原因。
页面下拉改为共用大屏那份页名清单(管理端两处标签随之变为「个人积分奖牌榜」「分项成绩」)。
## 导出(用户:保留多页工作簿的同时,再要一种全平铺单页;积分榜明细也要能导出)
- 新增 `GET /api/export/all-flat.xlsx` → `赛事成绩总表(单页).xlsx`:一个工作簿一张表,
列 = 大项/小项/组别/名次/号码/单位/运动员-队/成绩/积分/状态/备注;前三列取
`EventSheet` 的结构化字段(不解析标题),后 8 列就是 `eventColumns` 本身(append 拼出,
两处不可能漂移)。**既有「全部」(all.xlsx,多 sheet)一字未动。**
- 新增 `GET /api/export/teams.xlsx` → `单位积分总明细.xlsx`:一张带「单位」列的总表 +
每个有明细行的单位一张分表;**无明细行的单位不出分表**(空表没信息量,且会把假删除的
隐藏单位名字重新印进导出件);已隐藏但有流水的单位照常出表(它在榜单上)。
- 积分榜页:页头「导出总明细」、单位明细面板「导出该单位明细」;导出页新增「全部(单页)」。
测试:服务端 12 包全绿 / 347 个测试函数;前端 vue-tsc 通过、184 例全过;Console 18 文件。
docs/ 与 devdocs/ 同步(含新增 Q65–Q70 与两轮进度记录)。
|
2026-09-11 00:44:03 +08:00 |
|
AskaEth
|
5ae49b001c
|
feat: 参赛单位「彻底删除」= 假删除(界面隐藏,数据与榜单全保留)
与运动员的真物理删除是**两套相反口径**,有意如此(两个按钮同名,代码与
文档里都做了显著区分):
- 运动员 `?purge=1`:真删人员+报名+成绩+流水并重算积分(上一版 `9d4ee09`)
- 参赛单位 `?purge=1`:只置 `teams.deleted_at` + `active = 0`,让单位从列表里
消失;报名、成绩、积分流水、人工调整、纪录、名下运动员全部原样,
**不触发重算、积分榜不变**(单位下挂着几十号人,真删代价太大)
实现:
- 迁移 0018 `ALTER TABLE teams ADD COLUMN deleted_at TEXT`
- `listTeams` 默认排除已隐藏单位;`?includeDeleted=1` 仅 admin;
`GET /api/teams/{id}` 对已隐藏单位:admin 200 / 非 admin 404
- 审计 `team.purge`(detail 标明是隐藏);成功走 `writeVoid`(无统计对象)
- 报名导入按 `team_code` 命中已隐藏单位时复活(清 deleted_at、置 active=1)
- 故意不过滤的地方都加了注释:榜单补单位名、写路径与审计、导入按 code 查、
赛事包导出、大屏快照、按 team_id JOIN 取历史单位名——过滤会让历史成绩的
单位名变空白或让包的引用悬挂
管理端「参赛单位」行加「彻底删除」按钮 + 危险确认框(如实写明"从列表移除、
数据不受影响、积分榜不会变化"),失败原文展示、404 不降级。
顺带修一处界面文案与行为不符:单位页原写「停用后新报名不再可选该单位」,
但报名选择器其实仍会列出已停用单位供手动勾选(只有「全选」跳过它们),
已按真实行为改写(两处)。
测试:服务端 12 包全绿(新增 team_purge_test.go 与 0018 迁移用例);
前端 172 例全过(新增 team-purge-api.test.ts 6 例)。docs/ 与 devdocs/ 同步。
|
2026-09-10 23:48:28 +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
|
6bd7bd1627
|
feat: 「个人+团体」项目真正落地——通道级不计入单位 + 队内平均排名
口径(用户确认):
- 个人分按个人成绩排名给,**不计入单位总分**(下沉到通道级开关,不再动全局系数)
- 团体成绩 = 每队成员成绩的**平均值** → 排名(并列顺延)→ 团体模板给分 → 计入单位总分
- 平均值口径是**小项级单选项**:默认「只算有效成绩」;「全部报名人员计入」= 分母为报名条数、
没成绩按 0 加入分子;在**成绩录入页**就地切换并立即保存
服务端:
- 迁移 0017:score_ledger 加 count_to_team(重建,无子表);event_score_channels 加 count_to_team;
events 加 avg_mode(默认 valid)
- 规则包 Channel 加 countToTeam(缺省 true);channelTemplates 支持 individual_team →
默认 athlete(result, 不计入单位) + team(aggregate 队内平均)
- 计分引擎:SumByTeam 跳过 count_to_team=0 的流水;实现 basis=aggregate 的队内平均团体排名
(valid 分母=有有效成绩人数;all 分母=报名人数;某队 0 有效成绩不参与)
前端:
- 计分通道页:每行通道「计入单位总分」勾选 + basis 下拉「按队内平均」
- 成绩录入页:仅有队内平均通道的小项显示「平均值口径」单选(默认只算有效成绩),改动即存
- 录入页「实时积分预览」按新口径对齐(原先按主体求和且双通道重复计入,与发布后不一致)
- 大屏/管理端团体成绩只显示数值,不出现「平均」字样
自检:服务端 12 包全绿;前端 vue-tsc + build、node --test 134/134;console 16 文件通过。
|
2026-09-10 12:02:00 +08:00 |
|
AskaEth
|
592e47aef7
|
feat: 小项类型加「个人+团体」+ 报名导入拆团体/个人两部分
小项类型:
- 迁移 0016 给 events.event_type 的 CHECK 增加 individual_team。SQLite 改不了 CHECK,
而 events 被多张子表引用(含 ON DELETE CASCADE/SET NULL),事务内 DROP 重建会级联删数据,
故用 writable_schema 就地改写建表语句文本(只追加一个枚举值,'match' 在建表语句里仅出现一次,
无副作用),零数据搬动
- domain 加 EventTypeIndividualTeam;管理端下拉加「个人+团体」并提示会同时走两条通道;
guards/rules 的 eventType 白名单同步(否则会被降级成 individual)
- 注意:该类型目前只解决「能被选出来」,双计的算分语义(个人分不计入单位、团体按队内平均排名)
尚未实现,等口径确认后再动数据模型
报名导入:
- 卡片拆成「个人报名 / 团体报名」两个 tab,小项+组别提到 tab 之上共享
- 团体报名 = 从已配置单位清单里多选(默认全不勾,正好表达「部分单位不参加」),
可逐单位为登记队员(0 人即纯单位报名),一次提交为多个单位各建一条报名
- 已报名单位仍可覆盖名单,但会提示「提交将清空原名单」,且「全选」只勾启用中且未报名的单位
自检:服务端 12 包全绿;前端 vue-tsc + build、node --test 117/117;console 16 文件通过。
|
2026-09-10 11:19:31 +08:00 |
|
AskaEth
|
b5f40642ff
|
feat: 公告持久化(底栏常驻)+ 团体可单独报名 + 榜单系数开关
公告:
- 服务端把当前公告落库到 settings(display.announce_text / announce_seconds),
作为 display.announce 随 snapshot 与 /api/settings 下发(非 admin 快照也保留)
- announce 指令除转发外写库;新增 announce_clear 清空并广播;两者写审计
- realtime 协议登记 announce_clear(补上,使清除能即时推到大屏)
- 大屏把「全屏推送」与「底栏常驻」拆开:推送 seconds 秒后只退出全屏态,公告留在底栏,
直到被替换或清除;新增公告状态机(可测)与底栏取值优先级
推送中 > 服务端持久值 > 「暂无公告」;管理端加「清除公告」与当前公告状态
团体报名:
- EntryPicker 团体/对抗模式允许不勾队员直接报名该单位(此前必须勾人,导致
「只想给单位报个团体项目」做不到、单位下没有运动员时完全报不了)
- 核实录入页团体行:显示/填名次/保存/发布/算分闭环无缺陷(用真实服务端跑过),
抽出共享的 entryRowLabel 供两处复用
榜单口径:
- 基础数据 → 积分模板页新增「榜单口径(全局)」卡片:individualToTeamWeight
数字输入(默认 1、0–100),说明「个人项目得分按该系数折算计入单位总分」,
保存走 PUT /api/settings;因该值在服务端是全局键、模板表里没有此字段,
故做成独立卡片并标注全局,不塞进模板表单
自检:服务端 11 包全绿;前端 vue-tsc + build、node --test 99/99。
|
2026-09-10 11:06:58 +08:00 |
|
AskaEth
|
cded54e440
|
feat(display): kiosk 全屏不再显示数据版本号角标
那个 vN 是 change_log 的数据版本号(不是软件版本),用途是现场核对「这块屏有没有
跟上服务端」——与管理面板里的版本号一致就说明已同步。属调试信息,按「实例 · 屏」
同一规矩处理:只在窗口化调试时显示,kiosk 全屏不出现。
|
2026-09-10 10:35:03 +08:00 |
|
AskaEth
|
8c74a82da4
|
feat(display): 分项成绩与个人榜也显示单位 logo(个人榜在姓名前)
- 服务端补下发:dto.EventResultRow 与 athleteStandingDTO 各加 logoPath,
分别取「该行单位」与「该运动员所属单位」的 teams.logo_path;空串统一收敛为 null
- 大屏抽出 TeamLogo.vue(1em / object-fit:contain / 加载失败保留占位、@load 复原),
原先在积分榜与团体奖牌榜各写一遍的逻辑收敛成一份,四处复用
- 分项成绩页:单位列名字前显示 logo;个人积分榜:运动员姓名前显示其单位 logo
- guards/mock 同步解析与补字段;补 11 条测试(类型解析 + SFC 模板断言)
顺带修:管理端示例数据 athleteStandings 缺新必填字段导致的 vue-tsc 报错。
自检:服务端 11 包全绿;前端 vue-tsc + build、node --test 80/80。
|
2026-09-10 10:32:19 +08:00 |
|
AskaEth
|
6f47849e1e
|
fix(display): 隐藏调试标识 + 积分榜显示单位 logo
- TopBar 的「实例 N · 屏 N」只在该窗口确实处于调试态时渲染:非 windowed(kiosk)不显示,
窗口化下按 F11 全屏也不显示(同时监听 fullscreenchange,与 App.vue 口径一致)
- 积分榜与团体奖牌榜的队名左侧渲染单位 logo:数据本来就下发了(dto/standings.go 的
LogoPath),只是页面没画。图标 1em 随行字号浮动、object-fit:contain,行高不会被撑开;
无 logo 不占位,加载失败 visibility:hidden 保留占位避免队名跳动,@load 会复原
- resolveAssetUrl(路径归一化)从 admin 目录收敛到 web/src/shared/asset.ts,
管理端与大屏共用一份;原 image-path 测试随之迁到 shared/tests
未做(需服务端 DTO 支持,本次未授权):分项成绩页与个人榜的单位列同样没有 logoPath 字段,
所以那两个页面仍无法显示单位 logo。
自检:vue-tsc + pnpm build 通过;前端 node --test 69/69。
|
2026-09-10 10:20:03 +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
|
2d207fd654
|
feat: 赛事项目切换 + 记住上次打开的赛事(修「导入赛事包后没数据」)
根因:服务端启动只认 -project(缺省 default),Console 从不传;导入只是把包解到
event_projs/<名字>,没有任何切换入口 → 导入的赛事永远打不开,页面看到的是空的 default。
- 服务端 -project 缺省改为空串;解析顺序:显式 flag(并写回记忆)> <dataDir>/active_project.txt
> default;backup/passwd 口径一致;启动日志打印生效项目与来源
- GET /api/projects(admin)列出各赛事(是否当前/是否有 event.db/大小/更新时间)
- POST /api/projects/activate(admin):校验后原子写记忆文件 → 审计 → 返回
{ok,name,restarting:true} → 触发优雅关闭;Console 对非用户发起的退出会自动拉起,
重启后即打开新赛事(无需改 Console)。本来就是当前赛事则 restarting:false 不重启
- 管理端「导出」页新增赛事项目列表与「切换为当前赛事」;导入成功后提示「已导入为 X,
但还不是当前赛事」并给「立即切换并重启服务端」;切换后轮询 /api/health 等它回来再刷新
- 文档:使用手册(导入≠切换、生效四步)、常见问题(导入后看不到数据)
自检:服务端 12 包全绿;前端 vue-tsc + build、node --test 66/66;console 14 文件通过。
|
2026-09-10 09:47:06 +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
|
24fff8dd08
|
feat: 人员唯一标识改为证件号 + 导入冲突逐条决策
需求:人员以证件号为准(避免重名);批量导入时若证件号已存在于本地人员数据,
先导入其余人员,再把重复的人单独列出,由用户逐条/多选决定保留已有数据还是新数据。
服务端:
- 迁移 0013:athletes 增加 id_card + 部分唯一索引(空值可共存)
- athleteDTO/CRUD 读写 idCard,重复返回 409
- 导入表头新增可选列 id_card/gender;有姓名的行必须给证件号
- 匹配顺序:证件号 → 姓名+单位(且库中证件号为空则回填)→ 新建人员
- 证件号命中但姓名/性别/单位不一致 → 冲突,本轮不落库(报名也不建)
- 新增 POST /api/entries/import/resolve(file + decisions),keep-existing/keep-new
- 10 个新测试覆盖匹配/回填/冲突/决策/唯一索引
前端:
- 运动员表单与列表新增「证件号」,搜索支持证件号
- 报名导入:新模板 10 列;导入后冲突面板(每行复选框 + existing↔incoming 差异高亮
+ 全选/清空 + 保留已有数据/使用新数据 批量与逐条),未决策项始终可见
- 勾选报名(EntryPicker)缺证件号的运动员禁止勾选并提示
- 新增 import-conflicts.test.ts 等,前端测试 39/39
文档:使用手册(运动员证件号、导入表头与冲突流程)、常见问题(证件号重复怎么办)。
注:报名行的 team_code 与人员绑定单位不一致会计入冲突,由用户决策,不会静默改单位。
|
2026-09-10 02:09:15 +08:00 |
|
AskaEth
|
c9d5b8ea89
|
feat(admin): 报名导入新增「从已有名单勾选报名」
原来只能上传 CSV/XLSX。现在同一个 tab 上方多一个选择器:选小项 → 组别 → 单位,
从该单位已有运动员里勾人,一键加入报名。
- 个人项目:勾 N 人 → N 条报名(一人一行)
- 团体/对抗项目:勾 N 人 → 1 条报名,姓名拼进 member_names(再次提交即更新队员名单)
- 已报名的人/单位会标「已报名」并禁止重复勾选
- 复用服务端唯一报名入口 POST /entries/import:勾选结果转成同格式 CSV 提交,
字段校验、按 (组别,单位,运动员) upsert、行级报错回显与表格导入完全一致
- 纯函数 entrySelection.ts(表头唯一来源 + RFC 4180 转义)+ 4 个单测;
表头定义从 ImportSection/mockBaseData 收敛到一处
- 号码布/种子仍走表格导入,界面与文档都写明
自检:vue-tsc + pnpm build 通过,前端 node --test 30/30。
|
2026-09-10 02:03:16 +08:00 |
|
AskaEth
|
7531ea29e1
|
fix(admin): 组别填了年龄后「保存全部」无反应(number 字段被当字符串处理)
Vue 3 对 <input type="number"> 的 v-model 会自动 parseFloat(空值仍为 ''),
所以填了年龄后 row.ageMin/ageMax 是 number,而校验里写的是 row.ageMin.trim(),
点击处理器抛 TypeError 直接中断 —— 界面毫无反应;留空时是 '',.trim() 正常,所以能保存。
- CategoriesSection:新增 toAge() 统一取值(String() 包一层,非法值返回 NaN 交给校验标红),
validate 与 buildInputs 都改用它;CategoryRow 的数值字段类型如实标注为 string | number
- AdjustmentsPage:调整分值的 formDelta.value.trim() 同样会崩,改为 String(...).trim()
自检:vue-tsc + pnpm build 通过,前端 node --test 26/26。
|
2026-09-10 01:58:04 +08:00 |
|
AskaEth
|
c2696e768e
|
fix(admin): 写操作不再被实时连接状态拦截
原来 canWrite 依赖 realtimeStatus === 'online',WS 一断(服务端重启、网络瞬断、
以及之前那个 1008 停重连的 bug)整个管理端就变只读:基础数据的「保存全部」、
录入、发布、积分调整、大屏控制全部按钮变灰,且只是变淡、看不出原因。
写路径本来就是 HTTP,服务端会做完整校验,所以:
- stores/event.ts 的 canWrite 恒为 true(保留签名,角色判断由各页面叠加 auth.readOnly)
- useBaseData.canWrite 简化为 !auth.readOnly
- 顶部「连接中断」的提示改为「数据可能不是最新」,不再声称写操作停用
- 文档同步:FAQ 与使用手册区分「示例数据」(改动只在界面)与「连接中断」(照常提交)
新增 web/src/admin/tests/base-data.test.ts 锁住该行为(离线可写 / 访客不可写)。
|
2026-09-10 01:56:07 +08:00 |
|
AskaEth
|
a5941be9c4
|
fix(admin): 改密后管理面板永久「连接中断」+ 顶部版本号多一个 v
- /ws 身份在握手时确定,而 startRealtime() 全生命周期只建一次连接;改密会作废
服务端全部会话,服务端以 1008 关闭该连接,前端对 1008 是永久停止重连
(给真鉴权失败设计的)→ 重新登录后无人重启连接,界面一直显示「连接中断」,
只有重开窗口才会用新会话重新握手。
修:新增 restartRealtime(),登录成功与退出登录时各重启一次(同时修掉
「访客浏览后登录,旧连接仍是 guest 身份」的问题);1008 语义保持不变。
- AdminLayout 顶部版本号模板写死 v 前缀,而 serverHealth 已是完整版本串
(dev_0.1.0)→ 显示成 vdev_0.1.0。
新增 web/src/admin/tests/event-realtime.test.ts 锁该行为(临时置空 restartRealtime
会失败);为让 node --test 能加载真实 store,测试解析钩子补 @/ 别名。
|
2026-09-09 23:19:23 +08:00 |
|
AskaEth
|
27bddefaad
|
fix: 上线前快修(大屏自愈 / 奖牌口径 / 聚合作废 / 密码可发现性)
- P0 慢客户端关闭码 1008 → 4000:前端把所有 1008 当「登录失效」并永久停止重连,
大屏被踢后再也不自愈(服务端 markSlow 改用自定义码,前端逻辑不动)
- P1 REST 与 WS 的奖牌榜补 events.scored = 1 过滤,与导出成绩册口径一致
(否则「只排名不计分」项目只要挂着通道就会在大屏凭空出奖牌)
- P1 聚合流水作废去掉 source_id 子查询:改通道会重建通道 id,旧聚合流水
永远匹配不上 → 再插一份 → 分数翻倍
- P1 审核页/历史页的折算系数兜底由 ?? 1 改为 >0 ? : 1:/ws 快照对非 admin
会把 individualToTeamWeight 置 0,recorder 看到的个人折算分全是 0
- P2 删掉 signature.local.txt 后清理残留的 signature.build.txt,避免继续注入旧署名
- 初始管理员密码:banner 加纯 ASCII 锚 INITIAL ADMIN CREATED,文档改为搜它
(原来教的 findstr "password=" 已经失效,且中文 findstr 本身不可靠)
自检:服务端 12 包全绿;前端 vue-tsc + build 通过、node --test 24/24;console 语法通过。
|
2026-09-09 23:13:20 +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
|
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
|
76e725ca96
|
feat: 内部署名改为编译期注入(构建机本地文件,不进仓库);关于页展示
|
2026-09-09 20:07:59 +08:00 |
|
AskaEth
|
d288e2bb22
|
feat(admin): 基础数据/审核/破纪录/调整/导出/系统/日志/变更历史/关于 全部页面 + App 图标 + 大屏窗口化标题栏
|
2026-09-09 20:00:03 +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
|
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
|
bc8b1ee6c0
|
feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
|
2026-09-09 19:02:24 +08:00 |
|
AskaEth
|
b62bb022bd
|
fix(web): 构建后补回 assets/.gitkeep,保证新克隆可直接 go build
|
2026-09-09 18:39:00 +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 |
|