feat(joint-team): 联队 + 份额分成 —— 迁移 0024、引擎按份额拆、导出 / 大屏 / 录入三路打通;版本 dev_0.5.3

## 需求
现场 6 项球类比赛里,有两个单位因参赛人数不足**合并成一个单位参赛**。
做法不是新建「联队单位」(那样榜单会多出第 8 个单位,且分和奖牌分不到原单位头上),
而是**一条报名挂多个单位、按份额拆**。

## 拍板口径
- 份额**可配、默认均分**
- 积分**平均拆、接受 .5**(团体第 1 名 15 分 → 两个单位各 7.5 分)
- 奖牌**各记一枚**(两个单位各 1 枚完整金牌,不是各 0.5 枚)
- 显示上把联队名字写全(形如 `第一分公司+第二分公司联队`),成员单位的 logo 都放
- 导入表里单位编码写 `A+B`

## 一、核心层(数据模型 + 引擎 + 存储 + 报名写入 + 导入解析)
- 迁移 `0024_entry_team_shares.sql`:新建 `entry_team_shares(entry_id, team_id, points_share,
  medal_share, sort_order)`。**两个口径分开表达**是刻意的——积分要拆成小数(7.5)、奖牌要整枚
  各记,用一个数表达就会把「7.5 分」和「半枚金牌」绑在一起,奖牌榜立刻错。
  `entries.team_id` 保留为**主单位**;老数据 `INSERT OR IGNORE` 回填成「主单位独占 100%」。
- 引擎:`ResolveShares`(引擎与读侧共用)、`splitSharePoints`(按权重归一、**末位吞余数**,
  各份之和恒等于原来那一份)、`TeamShare.Medal`(缺省整枚)。积分按权重拆,奖牌每个份额各记整枚。
- 报名写入:`PUT /api/entries/{id}/team-shares`(recorder 及以上,保存后当场重算该小项组别并推榜)。
- 导入:`team_code` 支持 `A+B`(主单位 = 加号左边,均分,奖牌各整枚);非法形态
  (`A+` / `+B` / `A++B` / `A+A` / 成员 >5 / 单位不存在)**行级拦下、不落库**,给人话报错。

## 二、导出与导入模板
- 11 份导出件:联队写全成员单位(`第一分公司+第二分公司联队`),并注明**分拆给了哪几家、各多少**
  (份额占比 + 各单位实得积分,取自按份额拆完的流水)。
- 单位奖牌明细:奖牌格写「金牌(联队 …,各单位各记 1 枚)」;含联队时副标题说明
  「各单位奖牌数之和会大于实际颁发数」——这是口径 3 的必然结果,**不是 bug**。
- 按单位筛选导出把成员单位一起算上(否则 B 单位导自己的名单会看不到自己参加的那场联队比赛)。
- 分工作表模板:`A+B` 写法挂在「单位编码」表头的**单元格批注 + 输入提示**(中英各一份);
  **表头文字、列序、12 列、工作表命名一个字未动**(动了模板就导不回去)。

## 三、前端与大屏
- 录入页 / 复核页「单位」列:联队显示 `联队 第一分公司 67%(权重 2) 第二分公司 33%`,
  点该格打开「成员单位与份额」弹窗(加/删成员、改权重、主单位徽标、实时比例预览)。
  **普通报名(单单位)的文案、元素、类名一字未动**,只多了一个 `@click` 入口与悬停提示。
- 大屏:分项成绩 / 实时赛况单位列显示联队全名并**放上两个单位的 logo**(单位列宽按该行的
  logo 数留位);底栏同理。

## 四、验证(都留了证据)
- **老数据逐字节不变**(改算分引擎的硬门槛):引擎层 golden `a2aa1c97…`、接口层 golden
  `aa6cea37…`,两个哈希由主会话**在修前树上亲手重跑确认**;导出层另有 11 份导出件逐行对拍,
  差异 **0 处**。
- 前端 773 → **809 例**全绿(旧用例一行未改);服务端 14 包两轮全绿;Console 68 例全绿。
- 真机联调:录入页份额弹窗、大屏两个 logo、奖牌榜 A/B 各 1 金、写路径(移除成员 → 保存 → 200)。

## 五、版本
`dev_0.5.2` → **`dev_0.5.3`**(本批含数据迁移 0024 + 新功能,必须与上一包区分开);
安装器素材重生成(版本号烙在位图里,实测只有 `brand.bmp` 的 1x/2x 两张变化,其余 92 张逐字节未变)。
This commit is contained in:
AskaEth committed 2026-09-14 03:52:34 +08:00
1 parent b631418a85
commit 830a4dbdb7
61 files changed
+5649 -121

No files matched your search

+61
View File
@@ -102,6 +102,8 @@
表格列依次为:勾选框、录入(状态徽标)、号码、批次、单位、运动员 / 队、成绩输入、名次、状态、积分(自动)、备注、操作。**「运动员 / 队」这一列在姓名后面直接跟着该人的号码布**(灰色小字,例如 `张伟 101`):号码列仍然保留,这一处只是省得在长表格里左右横跳找号。
**「单位」这一列兼作「成员单位与份额」的入口**:普通报名显示单位简称(或全称)——**这一格与以前长得一模一样**,只是鼠标移上去变成手型、旁边有一行悬停提示,**点一下**会打开「成员单位与份额」弹窗(什么单位参赛、各占多少,见 4.11)。一条报名挂了多个单位(**联队**,两个单位因人数不足合并参赛)时,这一格会多一个「联队」小标,并把成员单位与份额逐条列出来:`联队 第一分公司 50% 第二分公司 50%`;不是均分时还会带上权重,例如 `联队 第一分公司 67%(权重 2) 第二分公司 33%`。
| 列 | 操作 |
| --- | --- |
| 成绩输入 | 直接输入成绩;工具栏会提示当前单位(秒 / 米 / 次 / 分),成绩格比过去更宽,四位数也看得全;打字时格子旁边会浮出一个小提示(见下) |
@@ -272,6 +274,45 @@
> 需要先「存草稿」再动这些入口:修改 / 作废 / 退回会重算并整表刷新,未保存的编辑会被刷新覆盖,页面会拦下并提示。
### 4.11 联队(两个单位合并参赛)
有些项目**两个单位因参赛人数不足,合并成一个单位参赛**。这种情况**不要新建一个「联队单位」**——那样榜单会多出一个单位,联队拿到的分与奖牌也分不到原单位头上。正确做法是**一条报名挂多个单位、按份额分成**:榜单仍然是原来的单位数,分数与奖牌按份额记给各单位,联队的名字在大屏与导出件上写全。
**三个口径(赛会已定,录之前先知道)**
| 口径 | 怎么写 |
| --- | --- |
| 份额 | **可配、默认均分**:两个单位各占一半(50% / 50%);要改成别的比例(例如 2:1)就填权重 |
| 积分 | **平均拆,接受 .5**:团体第 1 名 15 分 → 两个单位**各 7.5 分**(榜单上就写 7.5) |
| 奖牌 | **每个成员单位各记一枚整枚**:联队的金牌,两个单位**各记 1 枚**(不是各 0.5 枚) |
> ⚠️ 由此产生一个**必须知道**的结果:**各单位的奖牌数之和会大于实际颁发的奖牌数**。A、B 联队拿 1 枚金牌,奖牌榜上 A 记 1 金、B 记 1 金,而场上只颁发了 1 枚。**这是定稿口径,不是数据错了**;拿奖牌数去和实际颁发的奖牌核对时请记住这一点。导出件的「总表 / 单位奖牌明细」副标题里也写着这句话。
**怎么配(两个入口)**
1. **导入报名表时**:把「单位编码」那一格写成 `A+B`(例如 `F1+F2`)——**加号左边那个是主单位**(名次与榜单的归属主体),全角 `+` 与两侧空格都认。默认均分;一个联队最多 5 个单位。写法与报错见[报名名单导入规范 · 第三节](./报名名单导入规范.md);
2. **导入之后在界面上改**:到「成绩录入」页**点那一行的「单位」格**(复核页只读,改份额在录入页),打开「成员单位与份额」弹窗。要改成非均分(例如 2:1)**只能在弹窗里改,导入表里写不了**。
**「成员单位与份额」弹窗怎么用**
1. 弹窗里每个成员单位一行:**编码 / 单位全称 / 「主单位」徽标 / 权重输入框 / 移除**;
2. 权重是**相对权重**:都留空(或填 0)就是**均分**(各 50%),填 `2` 与 `1` 就是 **67% 与 33%**;
3. 底部「**添加成员单位…**」下拉里选一个单位再点「添加」——下拉里列的是还没在成员里的单位(**已停用**的单位也可以加,会标注「已停用」;被「彻底删除」的单位不出现);
4. 弹窗下方实时显示 `当前比例:第一分公司 50% · 第二分公司 50%`,两个以上成员时旁边还会给出联队名预览(`第一分公司+第二分公司联队`);
5. **主单位不能去掉**(它的「移除」是灰的):报名挂在它名下,要换主单位只能重新建报名;
6. 点 **「保存成员单位」** 提交——服务端**当场重算该小项该组别**的名次与积分并刷新大屏榜单,提示「成员单位已保存」。**没改动时保存按钮是灰的**,点「关闭」不会发出任何请求。
**要注意的**
- **权限**:读得到成绩的人都能看见这一格;**能改的是录入员及以上**(与录成绩同档)。访客只读时输入框置灰,并写明「当前为访客只读视图」或「服务端未连接」;示例数据模式下不能保存;
- **普通报名完全不受影响**:只有一个单位的报名,这一格的文案、元素与以前一样,**不改动就不会发出任何请求**;
- **分数怎么分、牌怎么记**:保存后这条成绩的分按份额拆给各成员单位(榜单上各处都已经是拆后的数),奖牌**每个成员单位各记一枚**(见上面的口径表与那句提醒);
- **按单位导出名单时,成员单位也看得到这条报名**:用 B 单位筛「报名名单」,能看到主单位是 A、成员里有 B 的联队报名——否则 B 导出自己的名单会发现「没派人上场」;
- ⚠️ **个人积分总明细里,联队里的同一个人会出现两行**(引擎按份额给两个成员单位各留一行流水)。导出侧只在「来源」列写清这两行为什么出现(`成绩(联队分拆:…)`):**看两行加起来才是这个人的总数**,他的总分没有被算两遍;
- ⚠️ **团体名次(见 4.9)要录在「主单位」那一行**:联队的名次录在主单位上,系统才会按份额拆给各成员单位;**不要再给成员单位单独录一份名次**,那会让成员单位再拿一份分(重复计分)。
- ⚠️ **「按队内平均」这类聚合通道的项目,联队不拆份额**:这类通道的名次不对应任何一条报名,分仍记在主单位头上(本轮如此)。要保证联队一起算,请用不聚合的口径(或按 4.9 在主单位上录名次)。
- 一件事只做一次:**同一个小项同一个组别里,两个单位联合参赛就写一条 `A+B` 的报名**。`A+B` 与 `B+A` 是**两条不同的报名**(主单位不同),不要两种写法都填。
---
## 5. 成绩审核
@@ -280,6 +321,9 @@
1. 左侧「待审草稿」按「小项 + 组别」分组,显示每组草稿条数,可搜索小项 / 组别 / 单位;
2. 点选一组后,右侧显示该组成绩明细(单位、运动员 / 队、成绩、名次、状态、积分);
> 「单位」这一列与录入页同一口径:普通报名显示单位简称;**联队**(一条报名挂多个单位)会多一个「联队」小标,并把成员单位与份额逐条列出(`第一分公司 50%` / `第二分公司 50%`),悬停能看到联队全名——复核时一眼就看出这一条其实是几个单位合并报的。本页只读,改份额请到「成绩录入」页点那一行的「单位」格(见 4.11)。
3. 用「整组对比 / 逐条对比」切换,查看发布后各单位积分的变化;
4. 「发布影响」列出受影响单位与分值变化;
5. 操作:
@@ -387,6 +431,7 @@
### 8.2 单位奖牌榜 / 个人积分榜
- 单位奖牌榜显示金、银、铜与合计;
- ⚠️ **有联队(两个单位合并参赛)时,各单位的奖牌数加起来会大于实际颁发的奖牌数**——联队的每一枚奖牌**每个成员单位各记一枚整枚**(A、B 联队拿 1 枚金牌,榜上 A 记 1 金、B 记 1 金,场上只颁发了 1 枚)。**这是定稿口径,不是数据错了**;拿榜单去和实际颁发的奖牌核对时请记住这一点。联队本身怎么配见 4.11;
- **单位奖牌榜有自己的名次与顺序:金牌多的在前,其次比银牌、铜牌,最后按单位编码排**——它不看总分,所以「金牌多但总分低」的单位在这里排在前面,在单位总分榜上却靠后,这是口径不同,不是数据错了;
- 个人积分榜显示运动员、单位、个人总分与奖牌数;
- 单位奖牌榜那一列的「明细」展开的是**同一个单位积分明细面板**(见 8.1),不是奖牌专属明细;但这份榜单本身有专属的逐枚奖牌导出(见 8.3);
@@ -399,6 +444,11 @@
- **导出奖牌明细**(单位奖牌榜卡片头):导出 `单位奖牌明细.xlsx`——第一张总表逐枚奖牌列出「单位 / 大项 / 小项 / 组别 / 名次 / 奖牌 / 运动员、队」,`奖牌` 列是 `金牌` / `银牌` / `铜牌`;其后每个**有奖牌**的单位一张分表(列同上、去掉「单位」,末尾一行小计 `金 X / 银 Y / 铜 Z`),没有奖牌的单位不出分表。口径与「单位奖牌榜」一致:同一个报名项在「个人+团体」这类双计通道下只记一枚奖牌,不会重复计数;
- **导出个人明细**(个人积分榜卡片头):导出 `个人积分总明细.xlsx`——第一张总表逐条列出「单位 / 运动员 / 大项 / 小项 / 组别 / 名次 / 成绩 / 得分 / 来源」;其后每个**有得分**的单位一张分表(按单位分,不按人分),没有得分流水的人不出现在表里。`得分` 是记到个人的原始分,**与「个人积分榜」逐个数字一致,且不乘榜单口径里的个人项目得分折算系数**(那个系数只作用于单位总分);
- **导出该单位明细**(右侧单位明细面板头部):先展开某个单位再点,导出 `单位积分明细.xlsx`(列:序号、大项、小项、组别、运动员、名次、成绩、来源、积分、备注,末行是「合计」)。**备注列会写下修订痕迹**:被改过的成绩写「已修订 ×N;最后一次 <时间> <原因>」,没改过的行留空——与屏幕上那个角标是同一句话,事后拿表格对账不会对不上。单位奖牌榜展开的也是这个单位积分明细面板,所以想按单位逐条看时同样用它;
- **联队(一条报名挂多个单位)在这几份导出件里怎么体现**(见 4.11):
- **单位积分明细 / 单位积分总明细的「备注」列**:这一条是从联队分来的,会写清分给了哪些单位、各拿多少分(多少 %),例如 `联队分拆:第一分公司 5 分(50%)、第二分公司 5 分(50%)`;「积分」列写的是**本单位实得的那一份**(上例是 5 分,不是联队拿到的 10 分);
- **个人积分总明细的「来源」列**:这一条是联队分来的写成 `成绩(联队分拆:第一分公司 5 分(50%)、第二分公司 5 分(50%))`。⚠️ 这张表里**联队成员会出现两行**(引擎按份额给两个成员单位各留一行流水)——**看两行加起来才是这个人的总数**,他不是被算了两遍;
- **单位奖牌明细的「奖牌」列**:联队那一枚写成 `金牌(联队 第一分公司+第二分公司,各单位各记 1 枚)`(非联队行仍然只有「金牌」两个字)。为了让这句话写得下,这一列**加宽并允许换行**,格子里的值本身没变;
- **总表 / 单位奖牌明细的副标题**:赛事里有联队时,会追加一句 `· 注:本赛事含联队,奖牌按「每个成员单位各记 1 枚」统计,各单位奖牌数之和会大于实际颁发数`——**各单位奖牌数加起来比实际颁发的多,是定稿口径,不是数据错了**(A、B 联队拿 1 枚金牌,两边榜上各 1 枚);
- 这些导出都是只读下载(访客也能用),不需要写权限;导出过程中按钮会变成「导出中…」并禁用,避免重复下载。
### 8.4 推送明细到大屏
@@ -660,6 +710,8 @@
报名有两种方式,都在本页完成:**从已有名单勾选**适合零散补报,**表格导入**适合批量录入。
> 两个单位因参赛人数不足**合并成一个队参赛**时,不要建一个「联队单位」——**在导入表的「单位编码」列写 `F1+F2`**,或导入后到「成绩录入」页点那一行的「单位」格配成员单位与份额(见 4.11)。榜单仍是原来的单位数,分数与奖牌按份额记给各单位。
> 本页的「从已有名单勾选」在**成绩录入页的工具栏里也有一个入口**(按钮叫 **「补报名」**),打开的是同一套勾选界面,只是预选了你正在录的那个小项与组别——在录入界面把某人移出报名之后,就地补回来即可(见 4.8)。
**从已有名单勾选**
@@ -687,7 +739,9 @@
- 「**中文表头 / 英文表头**」=**单表模板**(`entries-template-zh.csv` / `entries-template-en.csv`):一份表装全部报名,每行都要填「项目编码」与「组别」两列;
- 「**分工作表 · 中文表头 / 分工作表 · 英文表头**」=**分工作表模板**(`.xlsx`):按当前赛事已配好的**小项 × 组别各建一张工作表**,表名是「**小项名称@组别名称**」(例如 `100 米跑@男子组 30岁以下`),范围由表名承担,数据区只填人和单位。小项组别多时用它更不容易抄串;
- 两种模板的表头与列顺序完全一样,只有「范围从哪来」不同(详细规范见 [报名名单导入规范 · 第八节](./报名名单导入规范.md));
- **两个单位联合参赛(联队)**:把「单位编码」那一格写成 `F1+F2`(加号左边是主单位,默认均分,见 4.11)。**分工作表模板**里,「单位编码」表头那一格挂了一条**单元格批注**(鼠标停上去就弹出来)、这一列还有**输入提示**(点中该列的格子就弹出来),中英各一份,写的正是这套写法——表头文字、列顺序与工作表名一个字都没动,**老表格照常导入**;
2. 表头共 12 列:`event_code`、`category`、`team_code`、`athlete_name`、`id_card`、`gender`、`age`、`bib`、`seed`、`heat`、`status`、`member_names`(大小写不敏感)。其中 `id_card`(证件号)、`gender`(性别)、`age`(年龄)、`heat`(批次)为可选列,缺失不影响导入,其余列必须包含;
- `team_code`(单位编码)有两种填法:**一个单位**填它的编码(`F1`);**两个单位联合参赛**填 `F1+F2`(加号左边是主单位,默认均分;非均分要到录入页改,见 4.11);
- 用**分工作表模板**时,`event_code`、`category` 两列也可以整列不要或整列留空(范围由工作表名决定);一旦填了就必须与工作表名一致,不一致的那一行会报错并跳过;
- 工作表名必须是「小项名称@组别名称」;工作簿里只要有**一张**表名带 `@`,整份文件就按分工作表模板处理,别的表名不合规会**整份拒绝并点名是哪张表**(不让数据无声消失);
- 小项名与组别名里**不允许出现 `@`**(它是表名里的分隔符):新建 / 编辑小项与组别时界面会当场拦下并说明原因;**已经带 `@` 的历史名字不影响报名与成绩**,但下载分工作表模板时会被点名要求先改名;
@@ -757,6 +811,11 @@
| 个人积分总明细 | **在「积分榜 → 个人积分榜」页操作**:逐条列出单位 / 运动员 / 大项 / 小项 / 组别 / 名次 / 成绩 / 得分 / 来源,每个有得分的单位一张分表 | `个人积分总明细.xlsx` |
- 「分项成绩表」需先选择小项,「单位积分明细」需先选择单位;「报名名单」的四个筛选(小项 / 组别 / 单位 / 批次)**都可以留空**,留空就是不筛这一维度(全导);
- **联队(一条报名挂多个单位)在这几份导出件里的写法**(见 4.11):
- **「分项成绩表」「全部」「全部(单页)」的「单位」列**:写成联队全名 `第一分公司+第二分公司联队`(成员单位名写全,与导入表的 `F1+F2` 同构);**「备注」列**写清这条分给了谁、各多少,例如 `联队分拆:第一分公司 5 分(50%)、第二分公司 5 分(50%)`——「积分」列写的是**联队拿到的总分**(上例 10 分),每家的那一份看备注;
- **「报名名单」**:「单位编码」列回写成 `F1+F2`(**导出的名单可以直接拿去重导**,不用手工改回单个单位)、「单位名」列写联队全名、「备注」列写 `联队分拆:第一分公司 50%、第二分公司 50%`(这张表没有积分列,所以只写份额比例);
- **按单位筛选导出时,成员单位也看得到这条报名**:「报名名单」按 F2 筛选,能导出「主单位是 F1、成员里含 F2」的联队报名(否则 F2 导出自己的名单会发现「没派人上场」);
- 「单位积分明细 / 单位积分总明细」「个人积分总明细」「单位奖牌明细」里联队的写法见 8.3;**没有联队的赛事,这些列与升级前逐字一致**。
- **三种「还没出结果」的行也照样占一行**(「分项成绩表」「全部」「全部(单页)」都是,逐格口径完全一致):
- **录了但还没发布**:成绩列写 **`分数未录入`**、名次与积分列写 `—`、状态列写「草稿」;
- **完全没有录成绩**(连草稿都没有):名次 / 成绩 / 积分三列都写 `—`(不是空、也不是 `0`)、状态列写「未录入」;
@@ -992,6 +1051,8 @@
> 单位 logo 取自「基础数据 → 参赛单位」里上传的图片(见 10.1),按行内文字大小显示,不会撑开行高;没传 logo 的单位不占位,图片临时加载不出来时保留占位、刷新后恢复,队名不会跳动。
> **联队(两个单位合并参赛)在大屏上怎么写**(见 4.11):「分项成绩」与「实时赛况」的**单位列写联队全名**——`第一分公司+第二分公司联队`,并且**把两个单位的 logo 都放上**(单位列宽会为该行实际要画的 logo 数留位置,第二个 logo 不会把名字挤成省略号);底栏「最新完成项目」那一行同样写联队全名。**单位奖牌榜、总积分榜本来就是逐单位一行,服务端已按份额拆好**(例:A、B 各 1 枚金、各 7.5 分),屏上不需要另做处理。**没有联队的赛事,大屏画面与升级前逐像素一致**。
> 大屏的标题字体与正文字体取自「系统设置 → 赛事信息 → 字体」(见 14.3):可选四款内置字体,也可以上传自己的字体;上传字体加载失败时自动回落到默认字体,画面不会出现方块或空白。
> 除了 Console 开出来的本机大屏窗口,**局域网设备(另一台电脑、电视浏览器)打开 `http://<主机IP>:19540/display` 也能当大屏用**。这个地址不用手敲:管理端「大屏控制 → 局域网大屏」卡片里有**可复制的完整链接与二维码**(见第 9 节)。这类设备会出现在管理端「大屏控制 → 在线大屏」列表里(标记为「局域网浏览器」),也能被单独切页,用法见第 9 节;它们不受 Console 的 kiosk、防熄屏控制(另见常见问题 Q29)。
+91 -1
View File
@@ -6,7 +6,97 @@
---
## dev_0.5.2(当前版本)
## dev_0.5.3(当前版本)
### 一句话
**有一条数据迁移(`0024`),但升级后不用重配任何东西、老数据一个数字都不变**:一条报名现在可以挂**多个单位、按份额分成**——两个单位因参赛人数不足合并成一个联队参赛时,分数按份额拆给各单位(默认均分,允许出现 7.5 这种小数),奖牌**每个成员单位各记一枚整枚**。升级前就有的赛事,榜单、名次、积分、奖牌、导出件与升级前一模一样——**联队能力只有在真的把某条报名配成联队之后才会体现**。不用重新导入规则包。
### ① 数据迁移:一条(`0024`)
- **它做什么**:新建一张 `entry_team_shares` 表,记「这条报名归哪些单位、各占多少」(积分份额 + 奖牌是否记给本单位),并把**已有的每一条报名回填成「主单位独占 100%」**。`entries.team_id` 保留为**主单位**(联队时就是加号左边那个),既有的查询、唯一键、榜单、导入都还认它。
- **只加一张表 + 回填**:不改任何既有字段、不删任何数据;回填用的是「已经有份额就不动」的写法,重复执行(或迁移被重放)**不会覆盖已经配好的联队份额**。
- **怎么看它跑过**:启动一次 Console,在运行日志或 `logs\operations\` 当天文件里能看到 `已应用迁移 … name=0024_entry_team_shares.sql`。
### ② 老数据零变化(有对拍证据)
- **无联队的老数据在引擎层、接口层、导出层三层都做过签名 / 逐行对拍,差异 0 处**,签名取自**联队实现之前**的代码,实现之后必须一字不差:
- 引擎层:`server/internal/domain/legacy_forms_golden_test.go`(`TestLegacyFormsScoringUnchanged`,签名 `a2aa1c97…`);
- 接口层:`server/internal/httpapi/legacy_forms_golden_test.go`(`TestLegacyFormsAPISnapshotUnchanged`,签名 `aa6cea37…`);
- 导出层:`server/internal/export/joint_team_export_test.go` 的 `TestSingleUnitExportsHaveNoJointTeamText`(只有一条独占份额时,各份导出件一个字都不能多)。
- 所以:**升级后单位总分榜、单位奖牌榜、个人积分榜的每一个数字、每一条名次、每一枚奖牌、每一份导出件都与升级前一模一样**。本事只在真的把某条报名配成联队之后才生效。
### ③ 不用重新导入规则包
- 本版**没有改规则包格式、没有改计分口径、没有改任何计分通道配置**:随包规则包与现场那份都不用动,也不需要重算。
- (对照:dev_0.5.1 那次是建议重新导入的,本版不是。)
### ④ 新增能力:联队 + 份额分成(口径已定稿)
| 口径 | 定稿写法 |
| --- | --- |
| 份额 | **可配、默认均分**(权重都留空 = 各 50%;填 2 与 1 = 67% 与 33%) |
| 积分 | **平均拆,接受 .5**:团体第 1 名 15 分 → 两个单位各 **7.5 分**(榜单上就写 7.5) |
| 奖牌 | **每个成员单位各记一枚整枚**:联队的金牌,两个单位**各记 1 枚**(不是各 0.5 枚) |
| 显示 | 联队的名字**都写出来**:`第一分公司+第二分公司联队` |
| 导入 | 报名表的**单位编码**列写 `A+B`(加号左边那个是**主单位**;全角 `+` 与两侧空格等价) |
怎么用:
- **配联队的两个入口**:
1. **导入报名表**时把单位编码写成 `A+B`(默认均分;最多 5 个单位;`A+`、`+B`、`A++B`、`A+A` 这些写法会被**逐行拦下**并说清是第几行、错在哪,**其余行照常导入**);报名模板(分工作表 xlsx)的「单位编码」表头挂了一条**单元格批注**、该列还有**输入提示**,中英各一份,内容就是这套写法——**表头文字、列序、12 列、工作表命名一个字都没动**,老表格照常导入;
2. 导入后到**「成绩录入」页点那一行的「单位」格**,打开「**成员单位与份额**」弹窗改(加成员单位、填权重、去掉成员)。⚠️ **非均分(例如 2:1)不能在导入表里写**,只能在弹窗里改。
- **谁能改**:份额编辑与录成绩同档,**录入员及以上**;访客只读时输入框置灰并写明原因。
- **改完当场生效**:保存立即重算该小项该组别并推送大屏。
- **录入与复核页看得到**:联队的单位格显示成 `联队 第一分公司 50% 第二分公司 50%`(非均分时带 `(权重 2)`)。**普通报名(只有一个单位)的格子文案、元素一个字没动**,只是多了一个可点的入口和一行悬停提示。
- **大屏**:分项成绩表与实时赛况的「单位」列写 `第一分公司+第二分公司联队`,并**把两个单位的 logo 都放上**(单位列宽会为该行的 logo 数留位);底栏「最新完成项目」同样写联队全名;单位奖牌榜与总积分榜本来就是逐单位一行,服务端已按份额拆好(例:A、B 各 1 枚金、各 7.5 分)。
### ⑤ 导出件里现在写成什么样
| 位置 | 现在写成 |
| --- | --- |
| 分项成绩表 / 全部成绩(单页)「单位」列 | `第一分公司+第二分公司联队` |
| 同上「备注」列 | `联队分拆:第一分公司 5 分(50%)、第二分公司 5 分(50%)` |
| 报名名单「单位编码 / 单位名 / 备注」 | `T1+T2` / `第一分公司+第二分公司联队` / `联队分拆:第一分公司 50%、第二分公司 50%` |
| 单位积分明细 / 总明细「备注」列 | `联队分拆:… 各多少分(各多少 %)` |
| 个人积分总明细「来源」列 | `成绩(联队分拆:…)` |
| 单位奖牌明细「奖牌」列 | `金牌(联队 第一分公司+第二分公司,各单位各记 1 枚)` |
| 总表 / 单位奖牌明细的副标题 | 追加 `· 注:本赛事含联队,奖牌按「每个成员单位各记 1 枚」统计,各单位奖牌数之和会大于实际颁发数` |
- **按单位筛选导出**:成员单位的名单里也能看到**主单位是另一个单位的联队报名**(否则 B 导出自己的名单会发现「没派人上场」);
- 导出件的**值只在真的有联队时才变**:没有联队的赛事,导出件与升级前逐字一致。
### ⑥ 两条要主动告知的口径后果
1. **各单位的奖牌数之和会大于实际颁发的奖牌数**——因为联队的每一枚奖牌**每个成员单位各记 1 枚**。例:A、B 联队拿 1 枚金牌,奖牌榜上 A 记 1 金、B 记 1 金,而场上只颁发了 1 枚。**这是定稿口径,不是数据错了**;含联队的「总表 / 单位奖牌明细」副标题会写这句话,奖牌明细里联队那一格也写着「各单位各记 1 枚」。**拿奖牌数去和实际颁发的奖牌核对时请记住这一点。**
2. **个人积分总明细里,联队里的同一个人会出现两行**——引擎按份额给两个成员单位各留了一行流水,导出侧只在「来源」列写清这两行为什么出现(`成绩(联队分拆:…)`)。**看两行加起来才是这个人的总数**;这不是重复计分(他的总分没有被算两遍),是按份额拆开的结果。
### ⑦ 顺带:单位奖牌明细的「奖牌」列加宽并允许换行
- 导出件「单位奖牌明细」的「奖牌」列原来是**宽 8、居中**,写不下 `金牌(联队 第一分公司+第二分公司,各单位各记 1 枚)` 那句(会被右边的「运动员/队」裁掉),现在**加宽到 26 并允许换行**。
- **列里的值没变**:没有联队时这一格仍然只是「金牌」「银牌」「铜牌」三个字。
### ⑧ 升级后 5 分钟核对
1. 启动一次 Console,看运行日志 / `logs\operations\` 当天文件:应能看到 `0024` 这条迁移记录,没有别的报错;
2. 刷新管理端「积分榜」与大屏:**单位总分榜、单位奖牌榜、个人积分榜的每一个数字与升级前一致**(本版不改任何计分口径;只有你之后自己配了联队的地方才会变);
3. 打开「成绩录入」页:普通报名的**「单位」那一格与升级前长得一样**(只是鼠标移上去变成手型、有一行悬停提示,点一下会打开「成员单位与份额」弹窗,什么都不改直接关掉等于没动过);
4. 打开「基础数据 → 报名导入」,下载一份「**分工作表 · 中文表头**」,把鼠标停在表头「**单位编码**」那一格上:应弹出「单位编码怎么写」的批注(内容含 `A+B`);点中该列任一格应弹出同样的输入提示;
5. 真要用联队的:按 ④ 把那条报名配好(导入写 `A+B`,或点单位格改份额),确认榜单上两个单位**各拿到一半分、各记一枚牌**、大屏上两个单位的 logo 都在;
6. 用《上线检查清单》过一遍。
### ⑨ 详细说明在哪
| 想了解 | 看 |
| --- | --- |
| 联队怎么录、份额怎么改、分成后长什么样 | 《使用手册》4.11 |
| 导入表里联队怎么写、写错了报什么错 | 《报名名单导入规范》第三节的「单位编码」、第六节 |
| 大屏与导出件上联队写成什么 | 《使用手册》17.1、8.3、11.1 |
| 迁移与份额表结构 | `server/migrations/event/0024_entry_team_shares.sql` |
---
## dev_0.5.2
### 一句话
+61 -3
View File
@@ -54,7 +54,7 @@
|---|---|---|---|
| `event_code`(项目编码) | **必须** | 小项编码,抄系统里配好的 | `M100` |
| `category`(组别) | **必须** | 组别**名称**(中文,要跟系统里一字不差) | `男子30岁以下` |
| `team_code`(单位编码) | **必须** | 参赛单位编码,抄系统里配好的 | `F1` |
| `team_code`(单位编码) | **必须** | 参赛单位编码,抄系统里配好的。**两个单位联合参赛(联队)就写 `A+B`**,见第三节 | `F1`;联队写 `F1+F2` |
| `athlete_name`(姓名) | **必须** | 运动员姓名。**团体项目这一列留空** | `张伟` |
| `id_card`(证件号) | 可选 | 证件号(身份证号)。**系统的第一认人依据**;一个证件号只能属于一个人 | `110101199001011234` |
| `gender`(性别) | 可选 | 性别,只写「男」或「女」 | `男` |
@@ -88,6 +88,39 @@
在系统「基础数据 → 参赛单位」里给每个大单位配的**编码**,比如 `F1`、`F2`。系统里没有的会提示「单位 XXX 不存在」。
被「彻底删除」的单位,导入时若再次用到它的编码,它会**重新出现(复活)**,这一行照常导入——单位编码是唯一的,系统按编码复用原来那条记录,它以前的报名、成绩与积分都还在。
**两个单位联合参赛(联队)怎么写**
同一小项组别里,两个单位合起来出一支队参赛时,**这一格写两个编码、中间用 `+` 连起来**:
```
F1+F2
```
- **加号左边那个是「主单位」**:名次与榜单的归属主体就是它。`F1+F2` 与 `F2+F1` 是**两条不同的报名**(主单位不同),同一组别里只写一条即可;
- **全角 `+` 与两侧空格都认**:`F1+F2`、`F1 + F2` 与 `F1+F2` 是同一个意思(中文输入法下打出来的常是全角);
- **默认均分**:两个单位各占一半(50% / 50%)。团体第 1 名 15 分 → 两个单位**各 7.5 分**;奖牌**两边各记一枚整枚**(联队的金牌,F1、F2 各记 1 枚);
- ⚠️ **想改成非均分(例如 2:1)不能在表里写**:导入表只表达「是哪几个单位」。份额要等导入完到「成绩录入」页**点那一行的「单位」格**,在「成员单位与份额」弹窗里改(见[使用手册 · 4.11](./使用手册.md#411-联队两个单位合并参赛));
- **一个联队最多 5 个单位**;成员单位一个都不能写错——写错的那一行会被拦下,其余行照常导入;
- **加号两边都必须有编码**,而且同一个单位不能写两遍。
**怎么写会被拦下(逐行报错,其余行照常导入)**
| 写成 | 报错 |
|---|---|
| `F1+` / `+F2` / `F1++F2` | `第 N 行:单位编码 F1+ 里有一段是空的:联队要写成两个单位编码用 + 连起来(例如 A+B)` |
| `F1+F1` | `第 N 行:单位编码 F1+F1 里同一个单位出现了两次:联队的每个单位只能写一次` |
| `F1+F2+F3+F4+F5+F6` | `第 N 行:单位编码 F1+F2+F3+F4+F5+F6 里写了 6 个单位:一个联队最多 5 个单位` |
| `F1+ZZ`(`ZZ` 不存在) | `第 N 行:单位 ZZ 不存在(来自联队编码 F1+ZZ)` |
| `ZZ`(单个单位,写法没变) | `第 N 行:单位 ZZ 不存在` |
> 报错都写在**行号**上,并把你那一格**原样打出来**——照那句话改那一行即可,不用重传整张表。
**导入之后长什么样**
- 联队仍是一条报名,但**分与奖牌都按份额记给各成员单位**:单位总分榜、单位奖牌榜上 A、B 各有一份;
- 大屏和导出件里写的是**联队的名字**:`第一分公司+第二分公司联队`,报名名单导出的「单位编码」列也回写成 `F1+F2`(**导出的名单可以直接拿去重导**,不用手工改回单个单位);
- 用成员单位筛「报名名单」导出时,**也能看到这条联队报名**(否则 B 导出自己的名单会以为没派人上场)。
### `athlete_name` 运动员姓名
- **个人项目**:填姓名,必填。
- **团体项目**:**这一列留空**,表示"这是某个单位的一条团体报名",队员写在 `member_names` 里。
@@ -163,7 +196,7 @@
---
## 四、三个完整例子
## 四、四个完整例子
### 例 1:个人项目(七单位 × 若干人)
@@ -195,6 +228,24 @@
这类项目(比如团体引体向上)**按个人报名填**:每人一行、填姓名和单位。系统会自己算两份成绩——个人的名次分记在个人身上,团体分按各队成员的**平均成绩**排名后算给单位。**不需要**再单独写团体行。
### 例 4:两个单位联合参赛(联队)
本届运动会 6 项球类比赛里,**F2 与 F5 因参赛人数不足,合并出一个队参赛**。`BALL` 是篮球、`男子组`:
| event_code | category | team_code | athlete_name | id_card | gender | age | bib | seed | heat | status | member_names |
|---|---|---|---|---|---|---|---|---|---|---|---|
| BALL | 男子组 | F1 | | | | | | | | registered | 张三/李四 |
| BALL | 男子组 | F2+F5 | | | | | | | | registered | 王五/赵六/钱七 |
| BALL | 男子组 | F3 | | | | | | | | registered | 孙八 |
要点:
- **写一条 `F2+F5` 的报名就够了**,不要 F2、F5 各写一条(那会变成两队人上场);也不要新建一个叫「F2F5」的单位;
- `F2` 在**加号左边**,所以 **F2 是主单位**:这一条的名次与榜单以它为主体。写成 `F5+F2` 是另一条报名(主单位变成 F5),同一组别里只写一种;
- 默认**均分**:这条成绩拿到的分,F2 与 F5 **各一半**(团体第 1 名 15 分 → 各 7.5 分);奖牌**两边各记一枚整枚**(一块金牌,F2 榜上 1 枚、F5 榜上 1 枚);
- 要改成 **2:1** 这类非均分:**不能在这张表里写**,导入后到「成绩录入」页点那一行的「单位」格改(见[使用手册 · 4.11](./使用手册.md#411-联队两个单位合并参赛));
- 大屏与导出件里写的是 `第二分公司+第五分公司联队`,报名名单导出的「单位编码」列回写成 `F2+F5`(可以直接拿去重导)。
---
## 五、导入前请先检查(自检清单)
@@ -210,6 +261,7 @@
- [ ] 每个证件号在系统里只属于一个人,别和别人的档案重号
- [ ] `status` 只用那 4 个英文词(大小写都行)或第三节列的中文别名,不写别的词;拿不准就留空
- [ ] `heat` 要么留空,要么是 1–999 的整数(不参与算分,只是录入分组)
- [ ] **联队**(两个单位联合参赛)写的是 `F1+F2` 这种形状:加号两边都有编码、**同一个单位没写两遍**、成员不超过 5 个;**加号左边那个是主单位**(见第三节)
- [ ] 全表没有因为"怕麻烦"把某些列删掉——**表头 8 个必须列一个都不能少**(哪怕整列是空的)
- [ ] 用的是**分工作表模板**时:每张工作表的名字都是「小项名称@组别名称」、没有多余的工作表;「项目编码 / 组别」两列要么整列留空、要么与表名一字不差(见第八节)
@@ -228,6 +280,10 @@
| 无法解析 CSV:表格里多半有没配对的双引号 | 格子里**手工**写了双引号,或只有一个引号没配对 | 让 Excel 重新另存一次(引号由 Excel 按规则加,别手工写);确实需要引号的,写成 `""` 转义 |
| 第 N 行:小项 XXX 不存在 | `event_code` 写错,或该小项还没在系统里配 | 到「基础数据 → 大项 / 小项」核对编码;没配的先配 |
| 第 N 行:单位 XX 不存在 | `team_code` 写错,或该单位还没配 | 到「基础数据 → 参赛单位」核对 |
| 第 N 行:单位 ZZ 不存在(来自联队编码 F1+ZZ) | 这一格写的是**联队** `F1+ZZ`,其中 `ZZ` 没配 | 核对加号右边那个编码(括号里把**整格**原样打出来了,一眼看出是哪一段没对上);不改联队写法的话,就照单单位的办法核对编码 |
| 第 N 行:单位编码 F1+ 里有一段是空的:联队要写成两个单位编码用 + 连起来(例如 A+B) | **加号有一边是空的**(`F1+`、`+F2`、`F1++F2` 都是这一条)。多半是填一半或多了个加号 | 补上缺的那个编码,或删掉多余的加号;联队至少两个单位 |
| 第 N 行:单位编码 F1+F1 里同一个单位出现了两次:联队的每个单位只能写一次 | 同一个单位在联队里写了两遍(`F1+F1`) | 一个单位只写一次;两个单位联合参赛就是 `F1+F2` |
| 第 N 行:单位编码 … 里写了 6 个单位:一个联队最多 5 个单位 | 加号太多、成员超过 5 个 | **一个联队最多 5 个单位**;现场最多见过三个单位合并,超过 5 个多半是这一格填错了东西(例如整格都是加号) |
| 第 N 行:组别 XXX 不存在 | 组别名和系统里不一致,或该小项还没有这个组别 | 到「基础数据 → 组别」核对名称(注意别多字少字) |
| 第 N 行:小项 XXX 没有组别 | 该小项一个组别都没配 | 先在「组别」里配好,再导入 |
| 第 N 行:填写了 athlete_name 就必须填 id_card 或 bib | 个人行缺两项标识 | 补证件号或号码布;确实都没有的,请先在「运动员」里登记 |
@@ -292,6 +348,7 @@
- **「项目编码」「组别」两列可以整列留空**——表名已经说明了是哪个小项、哪个组别;
一旦填了,就**必须与表名一致**,不一致的那一行会报错并跳过(系统不猜、也不会挑一个用);
- 其余列与单表模板完全一样(第三节逐列说明照样适用);
- **下载下来的模板自带「联队怎么写」的说明**:分工作表模板里,「单位编码」**表头那一格有单元格批注**(鼠标停上去弹出,右边带一个小红三角),**该列每一格还有输入提示**(点中格子就弹出来);中文表头、英文表头各一份,内容就是 `A+B` 这套写法(加号左边是主单位、积分平均分、奖牌两边各记一枚)。**说明只挂在批注与输入提示上,不占任何一格数据、也不改表头与列序**——填好的表原样传回来照样导入;
- **只填了表头、没填人的那张表**会被跳过并给一条提示(= 这个组别没人报名),其余表照常导入;
- 不要在工作簿里加「说明」之类的工作表:**只要工作簿里有一张表名带 `@`**,整份文件就按分工作表模板处理,
其它表名(例如 `Sheet1`、`备注`)必须也是「小项名称@组别名称」的形状,否则**整份拒绝**并点名是哪张表——
@@ -337,6 +394,7 @@ Excel 对工作表名有硬限制:**最长 31 个字符**,同一份工作簿
> 7. 表里**不要合并单元格**、不要加标题行、不要插图;
> 8. 填完先对着第五节的清单自查一遍,再发给收表人;
> 9. **批次(`heat`)**可以不填;要分批比赛就填 1–999 的整数,它只影响录入时怎么分组找人,**不影响成绩与名次**;
> 10. **存成什么格式**:Excel →「另存为」→「**CSV UTF-8 (逗号分隔) (*.csv)**」最稳;中文 Excel 默认的「CSV (逗号分隔)」也能用。**别用「Unicode 文本」(UTF-16)**,**别手工改名或把两个文件拼起来**,也别在格子里手写双引号(让 Excel 自己加)。
> 10. **两个单位联合参赛(联队)**:`team_code` 那一格写 `F1+F2`(加号左边是主单位,默认均分,最多 5 个单位),**不要**新建一个「联队单位」、也不要两个单位各写一行;非均分(例如 2:1)要在导入后到「成绩录入」页点那一行的「单位」格改;
> 11. **存成什么格式**:Excel →「另存为」→「**CSV UTF-8 (逗号分隔) (*.csv)**」最稳;中文 Excel 默认的「CSV (逗号分隔)」也能用。**别用「Unicode 文本」(UTF-16)**,**别手工改名或把两个文件拼起来**,也别在格子里手写双引号(让 Excel 自己加)。
>
> 一句话:**下载模板 → 只往下面粘数据 → 另存为 CSV UTF-8 → 上传**。中间任何一个"顺手改一下"都可能让整份表被拦下。