Files
Scoreboard/web/src/display/tests/guards-channels.test.ts
T
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

34 lines
1.7 KiB
TypeScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { parseEvent, parseScoreChannel } from '../guards'
// 大屏快照里的 avgMode / countToTeam 是后加的字段:旧服务端不下发时必须有默认值,
// 而 basis='aggregate'(新口径「按队内平均」)不能被当成未知取值回落到 result。
test('parseEvent 缺 avgMode 时按 valid 处理', () => {
assert.equal(parseEvent({ id: 1, code: 'TPULL', eventType: 'individual_team' }).avgMode, 'valid')
assert.equal(parseEvent({ id: 1, avgMode: 'all' }).avgMode, 'all')
assert.equal(parseEvent({ id: 1, avgMode: 'AVG' }).avgMode, 'valid')
})
test('parseScoreChannel 保留 aggregate 并按 true 补 countToTeam', () => {
const channel = parseScoreChannel({ id: 3, eventId: 1, scope: 'team', basis: 'aggregate', profileId: 2, sortOrder: 1 })
assert.equal(channel.basis, 'aggregate')
assert.equal(channel.countToTeam, true)
const personal = parseScoreChannel({ id: 4, eventId: 1, scope: 'athlete', basis: 'result', profileId: 1, countToTeam: false })
assert.equal(personal.countToTeam, false)
// 未知 basis 仍回落到 result,避免渲染出不存在的口径。
assert.equal(parseScoreChannel({ basis: 'sum' }).basis, 'result')
})
test('parseEvent 同时解析详情接口带出的通道', () => {
const event = parseEvent({
id: 1,
channels: [{ id: 1, scope: 'athlete', basis: 'result', profileId: 1, countToTeam: false }],
})
assert.equal(event.channels?.length, 1)
assert.equal(event.channels?.[0]?.countToTeam, false)
// 列表接口不下发 channels 时保持 undefined,不能凭空造出空数组。
assert.equal(parseEvent({ id: 1 }).channels, undefined)
})