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

+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
### 一句话