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/ 五份(使用手册 / 报名名单导入规范 / 升级说明 / 部署指南 / 上线检查清单)同步。
This commit is contained in:
AskaEth committed 2026-09-14 01:10:16 +08:00
1 parent b74768b7d5
commit 1b8a37ff8c
32 files changed
+3302 -233

No files matched your search

@@ -146,6 +146,37 @@ export function templateFileName(language: TemplateLanguage): string {
return `entries-template-${language}.csv`
}
/**
* 分工作表模板(xlsx)的文件名:与单表 CSV 模板同一个命名口径,后缀区分成 xlsx。
*
* 两种模板**不是二选一**:单表 CSV 照样能用(老填表人不用改习惯),分工作表模板只是多了一种
* 「一个小项组别一张表」的组织方式,见 docs/报名名单导入规范.md。
*/
export function templateSheetFileName(language: TemplateLanguage): string {
return `entries-template-${language}.xlsx`
}
/**
* 小项名 / 组别名里出现 @ 的说明,没出现时返回 null。
*
* 与服务端 atSignNameMessage(entries_sheet_template.go)说的是同一件事、同一个理由:
* @ 是分工作表报名模板里「小项名称@组别名称」的分隔符,名字里再出现它,工作表名就拆不开。
* 只说「不能含 @」现场会以为是输入法问题,一定要把「为什么」写出来。
*/
export function atSignNameError(kind: string, name: string): string | null {
if (!name.includes('@') && !name.includes('@')) return null
return `${kind}「${name}」里不能有 @:@ 是分工作表模板中小项与组别的分隔符(表名要写成「小项名称@组别名称」)`
}
/**
* 示例数据模式下点「分工作表模板」的提示。
*
* 分工作表工作簿要按当前赛事的小项 × 组别逐张建表,浏览器里造不出 xlsx(也不该为此引一个库),
* 所以这里明确说「连上服务端才有」,而不是给一份假的下载文件。
*/
export const TEMPLATE_SHEET_MOCK_HINT =
'示例数据模式无法生成分工作表模板:它由服务端按当前赛事的小项与组别逐张建表。请连接服务端后重试,或继续用单表 CSV 模板'
/** 模板里的示例行:两种语言共用,只有表头不同。 */
const TEMPLATE_ROWS: readonly string[][] = [
['M100', '男子组', 'F1', '张伟', '110101199001011234', '男', '26', '101', '', '1', 'registered', ''],