feat(entry): 团体项目多队(迁移 0025)+ 修联队团体名次 / 大屏分项成绩 / 预览算分口径;版本 dev_0.5.5
## 一、新能力:团体项目多队
现场:羽毛球双打,一个单位可能派两个队,一个单位两个队各占一个名次。
- 迁移 `0025_entry_squad.sql`:`entries` 加 `squad TEXT NOT NULL DEFAULT ''`(自由文本队名,空串=不区分队伍);
唯一索引由 `(category_id, team_id) WHERE athlete_id IS NULL` 换成 `(category_id, team_id, squad) WHERE athlete_id IS NULL`
—— 原来那条正是「一个单位在一个组别只能报一条团体报名」的拦路虎。
- 导入:**第 13 列「队伍」**(`squad` / 队伍 / 队名 / 队伍名,可选,放最后);老 12 列表照常导入。
- 显示:`单位名 + 队名`(不加括号),榜单不写队名(分与牌本来就归单位)。
- 新增「改队名」:`PUT /api/entries/{id}/squad` + 「成员单位与份额」弹窗里的队伍输入框(只对纯团体报名显示)。
改队名**不需要重算**(`grep squad server/internal/domain/` = 0 处,改名前后流水/名次/榜单逐字节相同)。
- **老数据零变化**:接口层 JSON 剥掉新字段后逐字节相同;导出层 9 份导出件全单元格 750 行 dump 的 md5 相同。
## 二、修:团体名次控件没体现联队
联队的成员单位在「团体名次」里各占一行 → **合并成一行**(标签用大屏/导出一致的联队名),名次**只落主单位**;
成员单位不再进保存载荷,顺手堵掉「看 A、B 各一行,给 B 也录一个名次 → B 重复拿分」。
被并掉的成员单位原来若录过名次,区块里点名提示「已并入联队那一行,保存后清除」,不静默吞。
## 三、修:大屏「分项成绩」排名错乱
现场:第 3 栏列出的项目排名跳着显示(3、6、9、14、15、16),羽毛球单打第 1 名不显示。
**根因**:`group.rows.slice(0, 6)` 取的是**数据到达顺序**,整条渲染链**没有一处读 `rank`**。
修法:按名次升序(并列保持原先后、无名次排最后)再截断 —— 与同页「实时赛况」的既有口径一致。
复现证据:修前 `actual: [3,6,9,14,15,16]`(与现场数字一致)、`expected: [1,2,3,4,5,6]`。
顺带澄清:「只有第 3 栏出问题」不是栏的问题,是那一栏分到的数据次序不成立。
## 四、修:录入页预览算分与后端口径不一致(四处 + 一条延伸)
现场:手动录名次的项目(台球)「积分(自动)」列全是 0,而后端**有分**。
1. **预览不读 `channel_manual_ranks`、也不按通道 scope 分名次来源** → 「按单位录名次」的项目预览全 0,反过来名次填在成绩表时预览有分而服务端 0;
2. **末位归零判据不同**:预览原本「区间跨末位就整组归零」,与后端的 `startRank == lastPlace` 不符(并列第 1 时预览 0、服务端 10);
3. **`compare=none` 时预览静默降级成 manual**(后端此时直接报错)→ 改为同口径:显示**原因**而不是一个算不出来的数字;
4. **管理端保存计分通道缺守卫**:`compare=none + basis=result/aggregate` 原先只有规则包导入拦,手工能配出来、到发布才报错 → 保存前 400 + 人话。
延伸:预览的「已发布分」基线**只扣行分**,不挂报名项的分(团体名次那类)没扣 → 发布确认框「最大分差」虚高;新增 `unitPoints` 一并扣。
## 五、修:手录名次项目的「成绩」列空白
`displayResult()` 在 `resultValue`/`resultText` 都拿不到时**漏出空值**(且函数声明返回 `string`)→
新增占位 `—`、类型收紧,实时赛况与底栏对齐同一口径。
## 文档
`docs/报名名单导入规范.md`(12 → 13 列)、`docs/使用手册.md`(多队章节)、
`docs/升级说明.md`(新增 `dev_0.5.5` 一节)、`debug/使用说明/` 七份同步。
## 验证
- 全量自检两轮**全绿、`EXIT=0`**:服务端 14 包 / 前端 **883 例** 0 fail / Console 68 例 0 fail。
- 各块都先写会红的测试、留了原始输出(含现场报的那组数字与「预览全 0」)。
## 版本
`dev_0.5.4` → **`dev_0.5.5`**(含数据迁移 `0025`);安装器素材同步重生成(照例只动 `brand.bmp` 两张)。
## 下一批
`dev_0.5.6` = **团体报名队员身份**(把队员从文字名单变成人员关联,标识用**号码布**),工单见 `devdocs/20-团体报名队员身份.md`。
This commit is contained in:
1 parent
9663d5bfb0
commit
7a3e4ad365
67 files changed
+4658
-228
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
MANDATORY_COLUMNS,
|
||||
OPTIONAL_COLUMNS,
|
||||
REQUIRED_COLUMNS,
|
||||
SQUAD_COLUMN,
|
||||
buildSelectionRows,
|
||||
buildTeamSelectionRows,
|
||||
buildTemplateCsv,
|
||||
@@ -17,9 +18,11 @@ import {
|
||||
missingColumnMessage,
|
||||
registeredAthleteIds,
|
||||
registeredTeamIds,
|
||||
registeredTeamSquads,
|
||||
selectableTeamIds,
|
||||
selectionAllowsSubmit,
|
||||
setPickedTeams,
|
||||
setTeamPickSquad,
|
||||
teamSelectionAllowsSubmit,
|
||||
templateFileName,
|
||||
templateHeader,
|
||||
@@ -37,24 +40,26 @@ test('toCsv 按 RFC 4180 转义逗号 / 引号 / 换行', () => {
|
||||
assert.equal(toCsv([['a,"b"', 'c']]), '"a,""b""",c')
|
||||
})
|
||||
|
||||
test('表头含证件号 / 性别 / 年龄 / 批次可选列,必填列不含这四列', () => {
|
||||
test('表头含证件号 / 性别 / 年龄 / 批次 / 队伍可选列,必填列不含这五列', () => {
|
||||
assert.deepEqual(
|
||||
[...REQUIRED_COLUMNS],
|
||||
[
|
||||
'event_code', 'category', 'team_code', 'athlete_name', 'id_card', 'gender', 'age',
|
||||
'bib', 'seed', 'heat', 'status', 'member_names',
|
||||
'bib', 'seed', 'heat', 'status', 'member_names', 'squad',
|
||||
],
|
||||
)
|
||||
assert.equal(REQUIRED_COLUMNS[REQUIRED_COLUMNS.indexOf('gender') + 1], 'age', 'age 紧跟在 gender 之后')
|
||||
assert.equal(REQUIRED_COLUMNS[REQUIRED_COLUMNS.indexOf('seed') + 1], 'heat', 'heat 紧跟在 seed 之后')
|
||||
assert.deepEqual([...OPTIONAL_COLUMNS], ['id_card', 'gender', 'age', 'heat'])
|
||||
assert.equal(REQUIRED_COLUMNS.at(-1), SQUAD_COLUMN, '队伍列在最后:老表格一列都不会错位')
|
||||
assert.deepEqual([...OPTIONAL_COLUMNS], ['id_card', 'gender', 'age', 'heat', 'squad'])
|
||||
assert.equal(MANDATORY_COLUMNS.includes('id_card'), false)
|
||||
assert.equal(MANDATORY_COLUMNS.includes('age'), false)
|
||||
assert.equal(MANDATORY_COLUMNS.includes('heat'), false, '批次列可缺省,缺省即未分批')
|
||||
assert.equal(MANDATORY_COLUMNS.includes('squad'), false, '队伍列可缺省,缺省即不区分队伍')
|
||||
assert.equal(MANDATORY_COLUMNS.includes('athlete_name'), true)
|
||||
})
|
||||
|
||||
test('buildSelectionRows 个人项目:一人一行,共 12 列并带上证件号 / 号码布', () => {
|
||||
test('buildSelectionRows 个人项目:一人一行,共 13 列并带上证件号 / 号码布', () => {
|
||||
const rows = buildSelectionRows({
|
||||
eventCode: 'M100',
|
||||
categoryName: '男子组',
|
||||
@@ -68,13 +73,14 @@ test('buildSelectionRows 个人项目:一人一行,共 12 列并带上证件
|
||||
})
|
||||
assert.equal(rows.length, 4, '表头 + 3 名运动员')
|
||||
assert.deepEqual(rows[0], [...REQUIRED_COLUMNS], '第一行复用导入表头')
|
||||
assert.equal(rows[1]?.length, 12)
|
||||
// 勾选报名的人员已在库里,年龄列留空表示「不修改」;批次列留空 = 未分批(到录入页再编排)。
|
||||
assert.deepEqual(rows[1], ['M100', '男子组', 'F1', '张伟', '110101199001011234', '', '', '101', '', '', 'registered', ''])
|
||||
assert.equal(rows[1]?.length, 13)
|
||||
// 勾选报名的人员已在库里,年龄列留空表示「不修改」;批次列留空 = 未分批(到录入页再编排);
|
||||
// 队伍列留空 = 个人报名没有「第几支队」这回事。
|
||||
assert.deepEqual(rows[1], ['M100', '男子组', 'F1', '张伟', '110101199001011234', '', '', '101', '', '', 'registered', '', ''])
|
||||
// 没有证件号时号码布进 bib 列,服务端按号码布匹配人员并同时写入报名。
|
||||
assert.deepEqual(rows[2], ['M100', '男子组', 'F1', '李强', '', '', '', '102', '', '', 'registered', ''])
|
||||
assert.deepEqual(rows[2], ['M100', '男子组', 'F1', '李强', '', '', '', '102', '', '', 'registered', '', ''])
|
||||
// 只有证件号时 bib 列留空。
|
||||
assert.deepEqual(rows[3], ['M100', '男子组', 'F1', '王芳', '110101199001011235', '', '', '', '', '', 'registered', ''])
|
||||
assert.deepEqual(rows[3], ['M100', '男子组', 'F1', '王芳', '110101199001011235', '', '', '', '', '', 'registered', '', ''])
|
||||
})
|
||||
|
||||
test('buildSelectionRows 团体项目:整体一条,member_names 按列表顺序拼接', () => {
|
||||
@@ -86,8 +92,8 @@ test('buildSelectionRows 团体项目:整体一条,member_names 按列表顺
|
||||
athletes: [{ name: '王芳' }, { name: '刘洋' }, { name: '陈晨' }],
|
||||
})
|
||||
assert.equal(rows.length, 2, '表头 + 1 条团体报名')
|
||||
assert.equal(rows[1]?.length, 12)
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '王芳/刘洋/陈晨'])
|
||||
assert.equal(rows[1]?.length, 13)
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '王芳/刘洋/陈晨', ''])
|
||||
})
|
||||
|
||||
test('buildSelectionRows 团体项目可用 memberNames 覆盖名单与顺序', () => {
|
||||
@@ -100,7 +106,7 @@ test('buildSelectionRows 团体项目可用 memberNames 覆盖名单与顺序',
|
||||
athletes: [{ name: '王芳' }],
|
||||
memberNames: ['刘洋', '王芳'],
|
||||
})
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'scratched', '刘洋/王芳'])
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'scratched', '刘洋/王芳', ''])
|
||||
})
|
||||
|
||||
test('buildSelectionRows 个人项目:证件号与号码布都为空时两列留空,由导入通道报错', () => {
|
||||
@@ -111,7 +117,7 @@ test('buildSelectionRows 个人项目:证件号与号码布都为空时两列
|
||||
mode: 'individual',
|
||||
athletes: [{ name: '无标识' }],
|
||||
})
|
||||
assert.deepEqual(rows[1], ['M100', '男子组', 'F1', '无标识', '', '', '', '', '', '', 'registered', ''])
|
||||
assert.deepEqual(rows[1], ['M100', '男子组', 'F1', '无标识', '', '', '', '', '', '', 'registered', '', ''])
|
||||
})
|
||||
|
||||
// 团体项目「只报单位」:一个人都没勾也要能提交,member_names 留空(服务端 athlete_id 为 NULL 的报名合法)。
|
||||
@@ -124,7 +130,7 @@ test('buildSelectionRows 团体项目:0 名队员时 member_names 留空', ()
|
||||
athletes: [],
|
||||
})
|
||||
assert.equal(rows.length, 2, '表头 + 1 条只挂单位的报名')
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', ''])
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '', ''])
|
||||
})
|
||||
|
||||
test('团体 / 对抗项目 0 勾选可提交,个人项目必须勾人', () => {
|
||||
@@ -147,13 +153,14 @@ test('buildTeamSelectionRows 多个单位各一行,队员名单按 / 拼接,
|
||||
})
|
||||
assert.equal(rows.length, 4, '表头 + 3 个单位')
|
||||
assert.deepEqual(rows[0], [...REQUIRED_COLUMNS], '第一行复用导入表头')
|
||||
assert.equal(rows[1]?.length, 12)
|
||||
assert.equal(rows[1]?.length, 13)
|
||||
for (const row of rows.slice(1)) {
|
||||
assert.equal(row[3], '', '团体行的 athlete_name 必须为空,服务端才按单位 upsert')
|
||||
assert.equal(row[12], '', '不填队名时队伍列留空 = 不区分队伍')
|
||||
}
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'registered', '王芳/刘洋'])
|
||||
assert.deepEqual(rows[2], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', ''])
|
||||
assert.deepEqual(rows[3], ['BB', '混合组', 'F3', '', '', '', '', '', '', '', 'registered', '张伟'])
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'registered', '王芳/刘洋', ''])
|
||||
assert.deepEqual(rows[2], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '', ''])
|
||||
assert.deepEqual(rows[3], ['BB', '混合组', 'F3', '', '', '', '', '', '', '', 'registered', '张伟', ''])
|
||||
})
|
||||
|
||||
test('buildTeamSelectionRows:一个单位都没勾时只有表头,status 可覆盖', () => {
|
||||
@@ -165,7 +172,7 @@ test('buildTeamSelectionRows:一个单位都没勾时只有表头,status 可
|
||||
status: 'scratched',
|
||||
teams: [{ teamCode: 'F1', memberNames: [] }],
|
||||
})
|
||||
assert.deepEqual(scratched[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'scratched', ''])
|
||||
assert.deepEqual(scratched[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'scratched', '', ''])
|
||||
})
|
||||
|
||||
test('toggleTeamPick 勾选 / 取消单位,取消不清队员名单', () => {
|
||||
@@ -283,8 +290,8 @@ test('勾选状态转成 CSV 后表头与导入表一致,每个单位一行',
|
||||
const lines = toCsv(rows).split('\n')
|
||||
assert.equal(lines[0], REQUIRED_COLUMNS.join(','))
|
||||
assert.equal(lines.length, 3, '表头 + 两个单位')
|
||||
assert.equal(lines[1], 'BB,混合组,F1,,,,,,,,registered,"张,伟/李强"', '带逗号的姓名按 RFC 4180 转义')
|
||||
assert.equal(lines[2], 'BB,混合组,F2,,,,,,,,registered,', '只报单位时 member_names 留空')
|
||||
assert.equal(lines[1], 'BB,混合组,F1,,,,,,,,registered,"张,伟/李强",', '带逗号的姓名按 RFC 4180 转义')
|
||||
assert.equal(lines[2], 'BB,混合组,F2,,,,,,,,registered,,', '只报单位时 member_names 留空')
|
||||
})
|
||||
|
||||
test('registeredTeamIds 按组别取已报名单位;registeredAthleteIds 只认具体运动员', () => {
|
||||
@@ -303,6 +310,135 @@ test('registeredTeamIds 按组别取已报名单位;registeredAthleteIds 只
|
||||
assert.deepEqual([...registeredAthleteIds(entries, undefined, 1)], [])
|
||||
})
|
||||
|
||||
// ── 队伍(迁移 0025):同一个单位在一个组别可以派多个队 ────────────────────────
|
||||
|
||||
test('不填队名时报名行为与改造前完全一致:队伍列留空、其余列一个字都没变', () => {
|
||||
const rows = buildTeamSelectionRows({
|
||||
eventCode: 'BB',
|
||||
categoryName: '混合组',
|
||||
teams: [
|
||||
{ teamCode: 'F1', memberNames: ['王芳'], squad: '' },
|
||||
{ teamCode: 'F2', memberNames: [] },
|
||||
],
|
||||
})
|
||||
assert.deepEqual(rows[0], [...REQUIRED_COLUMNS], '表头就是导入表本身(队伍列在最后、可空)')
|
||||
assert.equal(rows[1]?.length, REQUIRED_COLUMNS.length)
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'registered', '王芳', ''])
|
||||
assert.deepEqual(rows[2], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '', ''])
|
||||
assert.equal(SQUAD_COLUMN, 'squad', '列名是服务端 importColumns 里的规范名')
|
||||
assert.equal(lookupImportColumn('队伍'), SQUAD_COLUMN, '中文表头「队伍」也认')
|
||||
assert.equal(lookupImportColumn('队名'), SQUAD_COLUMN)
|
||||
})
|
||||
|
||||
test('填了队名:队名进最后一列,其它列一个字不动', () => {
|
||||
const rows = buildTeamSelectionRows({
|
||||
eventCode: 'BB',
|
||||
categoryName: '混合组',
|
||||
teams: [
|
||||
{ teamCode: 'F1', memberNames: ['王芳', '刘洋'], squad: '一队' },
|
||||
// 同一个单位换个队名 = 这个单位的第二支队(服务端按 组别 + 单位 + 队伍 upsert)。
|
||||
{ teamCode: 'F1', memberNames: [], squad: '二队' },
|
||||
// 没填队名的单位照样一行,队名格留空。
|
||||
{ teamCode: 'F2', memberNames: ['张伟'] },
|
||||
],
|
||||
})
|
||||
assert.deepEqual(rows[0], [...REQUIRED_COLUMNS])
|
||||
assert.deepEqual(rows[1], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'registered', '王芳/刘洋', '一队'])
|
||||
assert.deepEqual(rows[2], ['BB', '混合组', 'F1', '', '', '', '', '', '', '', 'registered', '', '二队'])
|
||||
assert.deepEqual(rows[3], ['BB', '混合组', 'F2', '', '', '', '', '', '', '', 'registered', '张伟', ''])
|
||||
for (const line of toCsv(rows).split('\n')) {
|
||||
assert.equal(line.split(',').length, REQUIRED_COLUMNS.length)
|
||||
}
|
||||
})
|
||||
|
||||
test('队名两侧空白去掉再上表:服务端存的就是去空白后的文本', () => {
|
||||
const rows = buildTeamSelectionRows({
|
||||
eventCode: 'BB',
|
||||
categoryName: '混合组',
|
||||
teams: [{ teamCode: 'F1', memberNames: [], squad: ' 二队 ' }],
|
||||
})
|
||||
assert.equal(rows[1]?.[REQUIRED_COLUMNS.indexOf(SQUAD_COLUMN)], '二队')
|
||||
})
|
||||
|
||||
test('setTeamPickSquad 只改这一个单位的队名,逐个字输入都保留', () => {
|
||||
let state = toggleTeamPick(emptyTeamPick(), 1, true)
|
||||
state = toggleTeamPick(state, 2, true)
|
||||
state = setTeamPickSquad(state, 1, '一')
|
||||
state = setTeamPickSquad(state, 1, '一队')
|
||||
assert.equal(state.squads?.[1], '一队')
|
||||
assert.equal(state.squads?.[2], undefined, '另一个单位没填')
|
||||
assert.deepEqual(state.teamIds, [1, 2], '填队名不动勾选')
|
||||
// 清空输入框 = 改回不区分队伍,与从没填过一样。
|
||||
assert.equal(setTeamPickSquad(state, 1, '').squads?.[1], '')
|
||||
})
|
||||
|
||||
test('buildTeamSelections:填了队名的单位带上 squad,没填的与改造前逐字段相同', () => {
|
||||
const teams = [
|
||||
{ id: 1, code: 'F1' },
|
||||
{ id: 2, code: 'F2' },
|
||||
]
|
||||
const athletes = [{ id: 11, teamId: 1, name: '张伟', active: true }]
|
||||
let state = toggleTeamPick(emptyTeamPick(), 1, true)
|
||||
state = toggleTeamPick(state, 2, true)
|
||||
state = toggleMemberPick(state, 1, 11, true)
|
||||
assert.deepEqual(buildTeamSelections(state, teams, athletes), [
|
||||
{ teamCode: 'F1', memberNames: ['张伟'] },
|
||||
{ teamCode: 'F2', memberNames: [] },
|
||||
])
|
||||
|
||||
const withSquad = setTeamPickSquad(state, 1, ' 二队 ')
|
||||
assert.deepEqual(buildTeamSelections(withSquad, teams, athletes), [
|
||||
{ teamCode: 'F1', memberNames: ['张伟'], squad: '二队' },
|
||||
{ teamCode: 'F2', memberNames: [] },
|
||||
])
|
||||
})
|
||||
|
||||
test('勾选状态 → CSV 端到端:一个单位两个队就是两行,队名在最后一列', () => {
|
||||
const teams = [
|
||||
{ id: 1, code: 'F1' },
|
||||
{ id: 2, code: 'F2' },
|
||||
]
|
||||
let state = toggleTeamPick(emptyTeamPick(), 1, true)
|
||||
state = toggleTeamPick(state, 2, true)
|
||||
state = setTeamPickSquad(state, 1, '一队')
|
||||
state = setTeamPickSquad(state, 2, '青年队')
|
||||
|
||||
const rows = buildTeamSelectionRows({
|
||||
eventCode: 'BB',
|
||||
categoryName: '混合组',
|
||||
teams: buildTeamSelections(state, teams, []),
|
||||
})
|
||||
const lines = toCsv(rows).split('\n')
|
||||
assert.equal(lines[0], REQUIRED_COLUMNS.join(','))
|
||||
assert.equal(lines[1], 'BB,混合组,F1,,,,,,,,registered,,一队')
|
||||
assert.equal(lines[2], 'BB,混合组,F2,,,,,,,,registered,,青年队')
|
||||
})
|
||||
|
||||
test('registeredTeamSquads:按组别列出各单位已报名的队名,老报名(空串)不进表', () => {
|
||||
const entries = [
|
||||
{ categoryId: 7, teamId: 1, squad: '一队' },
|
||||
{ categoryId: 7, teamId: 1, squad: '二队' },
|
||||
{ categoryId: 7, teamId: 1, squad: '一队' },
|
||||
{ categoryId: 7, teamId: 2, squad: '' },
|
||||
{ categoryId: 7, teamId: 3 },
|
||||
{ categoryId: 8, teamId: 4, squad: '一队' },
|
||||
]
|
||||
const squads = registeredTeamSquads(entries, 7)
|
||||
assert.deepEqual(squads.get(1), ['一队', '二队'], '按报名表出现顺序、去重')
|
||||
assert.equal(squads.has(2), false, '不区分队伍的老报名不进表,徽标仍只说「已报名」')
|
||||
assert.equal(squads.has(3), false, '缺字段(老服务端)同样当空串')
|
||||
assert.equal(squads.has(4), false, '别的组别不算')
|
||||
assert.deepEqual([...registeredTeamSquads(entries, undefined)], [])
|
||||
})
|
||||
|
||||
test('entryRowLabel 团体行带上队名:同一单位的两个队在报错文案里也分得开', () => {
|
||||
assert.equal(entryRowLabel({ teamShort: '一司', squad: '二队' }), '一司二队')
|
||||
assert.equal(entryRowLabel({ teamShort: '一司', teamName: '第一分公司', squad: '' }), '一司', '没队名时不变')
|
||||
assert.equal(entryRowLabel({ teamName: '第一分公司', squad: '青年队' }), '第一分公司青年队')
|
||||
assert.equal(entryRowLabel({ athleteName: '张伟', teamShort: '一司', squad: '二队' }), '张伟', '个人行只写姓名')
|
||||
assert.equal(entryRowLabel({ memberNames: ['王芳'], teamShort: '一司', squad: '二队' }), '王芳')
|
||||
})
|
||||
|
||||
// 团体 / 接力行服务端不下发 athleteName(omitempty),回落到队员 / 单位名,
|
||||
// 否则防呆校验的文案会变成「:缺少成绩」。
|
||||
test('entryRowLabel 团体行回落到队员首人 / 单位', () => {
|
||||
@@ -321,8 +457,9 @@ test('两种语言的模板表头逐列对应:列数、顺序一致,只有
|
||||
assert.deepEqual(en, [...REQUIRED_COLUMNS], '英文模板就是英文规范名')
|
||||
assert.equal(zh.length, en.length, '两种模板列数必须一样')
|
||||
assert.deepEqual(zh, [
|
||||
'项目编码', '组别', '单位编码', '姓名', '证件号', '性别', '年龄', '号码布', '种子', '批次', '状态', '队员名单',
|
||||
'项目编码', '组别', '单位编码', '姓名', '证件号', '性别', '年龄', '号码布', '种子', '批次', '状态', '队员名单', '队伍',
|
||||
])
|
||||
assert.equal(zh.at(-1), '队伍', '队伍列也列在模板最后,与服务端 importColumns 同序')
|
||||
// 每一列的中文写法必须与服务端 handlers_config.go 的别名逐字一致,否则中文模板传回去会报缺列。
|
||||
assert.deepEqual(
|
||||
zh,
|
||||
@@ -340,7 +477,7 @@ test('模板 CSV:只有表头语言不同,示例行与列数完全相同', (
|
||||
assert.equal(en[0], templateHeader('en').join(','))
|
||||
assert.deepEqual(zh.slice(1), en.slice(1), '示例行共用,只有表头换语言')
|
||||
for (const line of zh) {
|
||||
assert.equal(line.split(',').length, REQUIRED_COLUMNS.length, '每行都是 12 列')
|
||||
assert.equal(line.split(',').length, REQUIRED_COLUMNS.length, '每行都是 13 列')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -357,6 +494,7 @@ test('lookupImportColumn 认英文名、中文别名,并容错全角 / BOM /
|
||||
assert.equal(lookupImportColumn('项目编码'), 'event_code')
|
||||
assert.equal(lookupImportColumn('小项编码'), 'event_code')
|
||||
assert.equal(lookupImportColumn('队员名单'), 'member_names')
|
||||
assert.equal(lookupImportColumn('队伍'), 'squad', '队伍列的中文表头')
|
||||
assert.equal(lookupImportColumn('EVENT_CODE'), 'event_code', '大小写不敏感')
|
||||
assert.equal(lookupImportColumn(' event_code '), 'event_code', '首尾空格')
|
||||
assert.equal(lookupImportColumn('\uFEFF项目编码'), 'event_code', 'BOM 粘在第一格列名上')
|
||||
@@ -368,13 +506,21 @@ test('lookupImportColumn 认英文名、中文别名,并容错全角 / BOM /
|
||||
|
||||
test('importColumnIndex 中英混用每列各自认;同一列写两遍直接拦下', () => {
|
||||
const index = importColumnIndex([
|
||||
'event_code', '组别', 'team_code', '姓名', '证件号', '性别', '年龄', '号码布', '种子', '批次', '状态', '队员名单',
|
||||
'event_code', '组别', 'team_code', '姓名', '证件号', '性别', '年龄', '号码布', '种子', '批次', '状态', '队员名单', '队伍',
|
||||
])
|
||||
assert.equal(index.get('event_code'), 0)
|
||||
assert.equal(index.get('category'), 1)
|
||||
assert.equal(index.get('member_names'), 11, '下标按混用后的实际列序取')
|
||||
assert.equal(index.get('squad'), 12, '队伍列在最后')
|
||||
assert.equal(index.size, REQUIRED_COLUMNS.length)
|
||||
|
||||
// 老表格(12 列、没有队伍列)照样认得:队伍是可选列,缺列 = 不区分队伍(老流程一个字不用改)。
|
||||
const legacy = importColumnIndex([
|
||||
'event_code', 'category', 'team_code', 'athlete_name', 'id_card', 'gender', 'age', 'bib', 'seed', 'heat', 'status', 'member_names',
|
||||
])
|
||||
assert.equal(legacy.size, REQUIRED_COLUMNS.length - 1)
|
||||
assert.equal(legacy.has('squad'), false)
|
||||
|
||||
for (const header of [
|
||||
['event_code', '项目编码', 'category', 'team_code', 'athlete_name', 'bib', 'seed', 'status', 'member_names'],
|
||||
['event_code', 'event_code', 'category', 'team_code', 'athlete_name', 'bib', 'seed', 'status', 'member_names'],
|
||||
|
||||
Reference in new issue
Block a user