口径(用户确认): - 个人分按个人成绩排名给,**不计入单位总分**(下沉到通道级开关,不再动全局系数) - 团体成绩 = 每队成员成绩的**平均值** → 排名(并列顺延)→ 团体模板给分 → 计入单位总分 - 平均值口径是**小项级单选项**:默认「只算有效成绩」;「全部报名人员计入」= 分母为报名条数、 没成绩按 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 文件通过。
76 lines
2.4 KiB
Go
76 lines
2.4 KiB
Go
package rules
|
||
|
||
import "git.yeij.top/AskaEth/Scoreboard/server/internal/domain"
|
||
|
||
// 本文件是规则包类型与 domain 类型之间的唯一转换入口;
|
||
// 转换只搬字段、不填默认值(默认值统一由 Canonical 负责),避免两处口径打架。
|
||
|
||
// ToDomain 把模板转成 domain.ScoringProfile;id 由调用方(仓储层)决定。
|
||
func (p Profile) ToDomain(id int64) domain.ScoringProfile {
|
||
tie := p.TieMode
|
||
if tie == "" {
|
||
tie = domain.TieSkip
|
||
}
|
||
bonus := p.RecordBonusMode
|
||
if bonus == "" {
|
||
bonus = domain.RecordBonusMax
|
||
}
|
||
rules := make([]domain.ScoringRule, 0, len(p.Rules))
|
||
for _, r := range p.Rules {
|
||
rules = append(rules, domain.ScoringRule{Rank: r.Rank, Points: r.Points})
|
||
}
|
||
return domain.ScoringProfile{
|
||
ID: id,
|
||
Code: p.Code,
|
||
Name: p.Name,
|
||
AwardRanks: p.AwardRanks,
|
||
LastPlaceZero: p.LastPlaceZero,
|
||
TieMode: tie,
|
||
RecordBonusMode: bonus,
|
||
Rules: rules,
|
||
}
|
||
}
|
||
|
||
// ToDomain 把通道转成 domain.ScoreChannel;profileID 由调用方按模板编码查出。
|
||
// countToTeam 缺省(true)时不落指针,保持与「库里只有显式关闭的通道才写 0」一致。
|
||
func (c Channel) ToDomain(eventID, profileID int64, sortOrder int) domain.ScoreChannel {
|
||
out := domain.ScoreChannel{
|
||
EventID: eventID,
|
||
Scope: c.Scope,
|
||
Basis: c.Basis,
|
||
ProfileID: profileID,
|
||
SortOrder: sortOrder,
|
||
}
|
||
if !c.CountsToTeam() {
|
||
out.CountToTeam = BoolPtr(false)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ProfileIndex 按模板编码建索引,便于通道引用解析。
|
||
func (p *RulePack) ProfileIndex() map[string]Profile {
|
||
out := make(map[string]Profile, len(p.Profiles))
|
||
for _, prof := range p.Profiles {
|
||
out[prof.Code] = prof
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ToDomain 把破纪录配置转成 domain.RecordBonusConfig。
|
||
func (r RecordBonus) ToDomain() domain.RecordBonusConfig {
|
||
mode := r.Mode
|
||
if mode == "" {
|
||
mode = domain.RecordBonusMax
|
||
}
|
||
levels := make([]domain.RecordLevel, 0, len(r.Levels))
|
||
for _, lv := range r.Levels {
|
||
levels = append(levels, domain.RecordLevel{Code: lv.Code, Name: lv.Name, Points: lv.Points})
|
||
}
|
||
return domain.RecordBonusConfig{Enabled: r.Enabled, Mode: mode, Levels: levels}
|
||
}
|
||
|
||
// ToDomain 把人工调整开关转成 domain.ManualAdjustmentConfig。
|
||
func (m ManualAdjustment) ToDomain() domain.ManualAdjustmentConfig {
|
||
return domain.ManualAdjustmentConfig{Enabled: m.Enabled, ReasonRequired: m.ReasonRequired}
|
||
}
|