fix: 补 .gitattributes(CRLF for .bat) + README 端口 19540 + 提交 console 锁文件

This commit is contained in:
AskaEth committed 2026-09-09 20:57:58 +08:00
1 parent c121111f51
commit b80c64a115
39 files changed
+4217 -345

No files matched your search

+45 -55
View File
@@ -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) {