fix: 补 .gitattributes(CRLF for .bat) + README 端口 19540 + 提交 console 锁文件
This commit is contained in:
1 parent
c121111f51
commit
b80c64a115
39 files changed
+4217
-345
No files matched your search
@@ -12,6 +12,7 @@ const rankEpsilon = 1e-9
|
||||
// ScoreOptions 控制引擎的可选行为;零值即本次运动会的口径。
|
||||
type ScoreOptions struct {
|
||||
// UnionAttribution 决定联队成绩的归属;空值等同 UnionWhole(整体记给主单位)。
|
||||
// 未实现:生产链路没有赋值入口,联队份额也无处加载(见 UnionAttribution 注释)。
|
||||
UnionAttribution UnionAttribution
|
||||
// IncludeDrafts 为 true 时草稿成绩也参与计分;默认只算已发布(草稿不上屏也不计分)。
|
||||
IncludeDrafts bool
|
||||
@@ -57,26 +58,44 @@ type RankedResult struct {
|
||||
}
|
||||
|
||||
// ScoreEvent 按项目的计分通道逐条计算名次与积分,输出流水与名次明细。
|
||||
// 项目 scored=false 或没有通道时不产生任何流水(只排名不计分)。
|
||||
// 计分与排名解耦:项目 scored=false 或没有通道时照样产出名次(Points=0),只是不写流水。
|
||||
// 没有通道时按小项自身的录入方式(result_mode)推导排名口径,见 rankOnlyChannel。
|
||||
func ScoreEvent(in EventScoreInput) (EventScore, error) {
|
||||
var out EventScore
|
||||
if !in.Event.Scored {
|
||||
return out, nil
|
||||
rankOnly := len(in.Channels) == 0
|
||||
channels := sortedChannels(in.Channels)
|
||||
if rankOnly {
|
||||
channels = []ScoreChannel{rankOnlyChannel(in)}
|
||||
}
|
||||
|
||||
for _, ch := range sortedChannels(in.Channels) {
|
||||
profile, ok := in.Profiles[ch.ProfileID]
|
||||
if !ok {
|
||||
return EventScore{}, fmt.Errorf("%w: 通道 %d 引用模板 %d", ErrProfileNotFound, ch.ID, ch.ProfileID)
|
||||
for _, ch := range channels {
|
||||
// 不计分时不需要积分表:既不查模板,也不可能算出非零分。
|
||||
profile := ScoringProfile{}
|
||||
if in.Event.Scored && !rankOnly {
|
||||
p, ok := in.Profiles[ch.ProfileID]
|
||||
if !ok {
|
||||
return EventScore{}, fmt.Errorf("%w: 通道 %d 引用模板 %d", ErrProfileNotFound, ch.ID, ch.ProfileID)
|
||||
}
|
||||
profile = p
|
||||
}
|
||||
ranked, err := scoreChannel(in, ch, profile)
|
||||
if err != nil {
|
||||
return EventScore{}, err
|
||||
}
|
||||
if !in.Event.Scored {
|
||||
for i := range ranked {
|
||||
ranked[i].Points = 0
|
||||
}
|
||||
}
|
||||
out.Ranked = append(out.Ranked, ranked...)
|
||||
out.Ledger = append(out.Ledger, ledgerFromRanked(in, ch, ranked)...)
|
||||
if in.Event.Scored {
|
||||
out.Ledger = append(out.Ledger, ledgerFromRanked(in, ch, ranked)...)
|
||||
}
|
||||
}
|
||||
|
||||
if !in.Event.Scored {
|
||||
return out, nil
|
||||
}
|
||||
bonus, err := recordBonusLedger(in)
|
||||
if err != nil {
|
||||
return EventScore{}, err
|
||||
@@ -85,6 +104,20 @@ func ScoreEvent(in EventScoreInput) (EventScore, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// rankOnlyChannel 在没有计分通道时,按小项自身的录入方式推导排名口径:
|
||||
// manual 直接录名次,其余按成绩自动排名;名次记给个人,团体/对抗项目记给单位。
|
||||
func rankOnlyChannel(in EventScoreInput) ScoreChannel {
|
||||
basis := BasisResult
|
||||
if in.Event.ResultMode == ResultModeManual {
|
||||
basis = BasisManual
|
||||
}
|
||||
scope := ScopeAthlete
|
||||
if in.Event.EventType == EventTypeTeam || in.Event.EventType == EventTypeMatch {
|
||||
scope = ScopeTeam
|
||||
}
|
||||
return ScoreChannel{EventID: in.Event.ID, Scope: scope, Basis: basis}
|
||||
}
|
||||
|
||||
// sortedChannels 固定通道处理顺序,保证同一份输入每次产出同样的流水顺序。
|
||||
func sortedChannels(channels []ScoreChannel) []ScoreChannel {
|
||||
out := append([]ScoreChannel(nil), channels...)
|
||||
@@ -325,22 +358,17 @@ func rankedResult(in EventScoreInput, ch ScoreChannel, c candidate, rank int, po
|
||||
return rr
|
||||
}
|
||||
|
||||
// scoreByAggregate 先把同一主体的成绩汇总成一个值,再按值排名(预留能力)。
|
||||
// scoreByAggregate 先把同一主体的成绩汇总成一个值(求和),再按值排名(预留能力)。
|
||||
func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfile) ([]RankedResult, error) {
|
||||
cands, err := collectCandidates(in, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := ch.Aggregate
|
||||
if method == "" {
|
||||
method = AggregateSum
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
subject int64
|
||||
entry Entry
|
||||
values []float64
|
||||
total float64
|
||||
}
|
||||
order := make([]int64, 0)
|
||||
buckets := make(map[int64]*bucket)
|
||||
@@ -358,13 +386,13 @@ func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfil
|
||||
buckets[subject] = b
|
||||
order = append(order, subject)
|
||||
}
|
||||
b.values = append(b.values, c.value)
|
||||
b.total += c.value
|
||||
}
|
||||
|
||||
agg := make([]candidate, 0, len(order))
|
||||
for _, id := range order {
|
||||
b := buckets[id]
|
||||
c := candidate{entry: b.entry, subject: b.subject, value: aggregateValue(method, b.values)}
|
||||
c := candidate{entry: b.entry, subject: b.subject, value: b.total}
|
||||
c.entry.ID = 0 // 聚合结果不对应单条报名项
|
||||
agg = append(agg, c)
|
||||
}
|
||||
@@ -408,44 +436,6 @@ func scoreByAggregate(in EventScoreInput, ch ScoreChannel, profile ScoringProfil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func aggregateValue(method AggregateMethod, values []float64) float64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
switch method {
|
||||
case AggregateAvg:
|
||||
var sum float64
|
||||
for _, v := range values {
|
||||
sum += v
|
||||
}
|
||||
return sum / float64(len(values))
|
||||
case AggregateMax:
|
||||
best := values[0]
|
||||
for _, v := range values[1:] {
|
||||
if v > best {
|
||||
best = v
|
||||
}
|
||||
}
|
||||
return best
|
||||
case AggregateMin:
|
||||
best := values[0]
|
||||
for _, v := range values[1:] {
|
||||
if v < best {
|
||||
best = v
|
||||
}
|
||||
}
|
||||
return best
|
||||
case AggregateCount:
|
||||
return float64(len(values))
|
||||
default:
|
||||
var sum float64
|
||||
for _, v := range values {
|
||||
sum += v
|
||||
}
|
||||
return sum
|
||||
}
|
||||
}
|
||||
|
||||
func sortCandidatesByValue(cands []candidate, compare CompareMode) {
|
||||
sort.SliceStable(cands, func(i, j int) bool {
|
||||
if nearlyEqual(cands[i].value, cands[j].value) {
|
||||
|
||||
Reference in new issue
Block a user