Files
Scoreboard/server/internal/domain/ledger.go
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

46 lines
1.4 KiB
Go

package domain
// TeamPoints 一个单位的分项汇总;系数只在汇总时折算,历史流水保持原始分。
type TeamPoints struct {
Total float64 // 单位总分
TeamOnly float64 // 仅记单位的流水
Individual float64 // 记到个人的流水(原始分,未乘系数)
}
// SumByTeam 汇总各单位得分:总分 = 仅记单位的流水 + 系数 × 记到个人的流水。
// weight <= 0 时按 1 处理,避免零值把个人分抹掉。
// count_to_team = 0 的流水(如「个人+团体」项目的个人通道)只记在个人头上,不计入单位总分。
func SumByTeam(entries []LedgerEntry, weight float64) map[int64]TeamPoints {
if weight <= 0 {
weight = 1
}
out := make(map[int64]TeamPoints)
for _, e := range entries {
if e.Voided || !e.CountToTeam {
continue
}
p := out[e.TeamID]
if e.AthleteID == 0 {
p.TeamOnly += e.Points
} else {
p.Individual += e.Points
}
p.Total = p.TeamOnly + weight*p.Individual
out[e.TeamID] = p
}
return out
}
// SumByAthlete 汇总每个运动员的个人总分(原始分,不折算系数)。
// 不计入单位总分的流水(count_to_team = 0)照样算在个人头上,个人榜不受影响。
func SumByAthlete(entries []LedgerEntry) map[int64]float64 {
out := make(map[int64]float64)
for _, e := range entries {
if e.Voided || e.AthleteID == 0 {
continue
}
out[e.AthleteID] += e.Points
}
return out
}