Files
Scoreboard/server/internal/domain/helpers_test.go
T
AskaEth 7f9701c465 fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力
- realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份
- httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表
- web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换
- console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复
- docs: 用户文档三份 + README
- 主代理: 破纪录加分接线(levels 未加载导致恒 0)
2026-09-09 21:06:40 +08:00

201 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package domain
import "testing"
// ---------- 通用构造 ----------
func ptr(f float64) *float64 { return &f }
// individualTop6 是规则包里的个人项目前六名:10/8/6/3/2/1。
func individualTop6() ScoringProfile {
return ScoringProfile{
ID: 1, Code: "individual_top6", Name: "个人项目前六名",
AwardRanks: 6, LastPlaceZero: false, TieMode: TieSkip,
Rules: []ScoringRule{{1, 10}, {2, 8}, {3, 6}, {4, 3}, {5, 2}, {6, 1}},
}
}
// teamTop6 是规则包里的团体项目前六名:15/10/8/4/2/1。
func teamTop6() ScoringProfile {
return ScoringProfile{
ID: 2, Code: "team_top6", Name: "团体项目前六名",
AwardRanks: 6, LastPlaceZero: false, TieMode: TieSkip,
Rules: []ScoringRule{{1, 15}, {2, 10}, {3, 8}, {4, 4}, {5, 2}, {6, 1}},
}
}
// nineProfile 是测试矩阵 case 1..6 使用的八名表:9/7/6/5/4/3/2/1。
func nineProfile(tie TieMode) ScoringProfile {
return ScoringProfile{
ID: 3, Code: "nine", Name: "八名表",
AwardRanks: 8, LastPlaceZero: false, TieMode: tie,
Rules: []ScoringRule{{1, 9}, {2, 7}, {3, 6}, {4, 5}, {5, 4}, {6, 3}, {7, 2}, {8, 1}},
}
}
func testCategory() EventCategory {
return EventCategory{ID: 1, EventID: 1, Name: "男子组", Multiplier: 1}
}
func individualEvent(multiplier float64) Event {
return Event{
ID: 1, Code: "E1", Name: "100米跑", EventType: EventTypeIndividual,
Compare: CompareAsc, Scored: true, Multiplier: multiplier, Status: EventStatusPublished,
}
}
func teamEvent(multiplier float64) Event {
e := individualEvent(multiplier)
e.EventType = EventTypeTeam
e.Code = "E2"
return e
}
func athleteResultChannel(profileID int64) ScoreChannel {
return ScoreChannel{ID: 1, EventID: 1, Scope: ScopeAthlete, Basis: BasisResult, ProfileID: profileID}
}
func athleteManualChannel(profileID int64) ScoreChannel {
return ScoreChannel{ID: 1, EventID: 1, Scope: ScopeAthlete, Basis: BasisManual, ProfileID: profileID}
}
func teamManualChannel(profileID int64) ScoreChannel {
return ScoreChannel{ID: 2, EventID: 1, Scope: ScopeTeam, Basis: BasisManual, ProfileID: profileID}
}
func teamAggregateChannel(profileID int64) ScoreChannel {
return ScoreChannel{ID: 3, EventID: 1, Scope: ScopeTeam, Basis: BasisAggregate, ProfileID: profileID}
}
// participant 是构造「报名项 + 成绩」的最小描述。
type participant struct {
entryID int64
athleteID int64
teamID int64
name string
value float64
noValue bool // true 表示这条成绩没有可比数值(如直接录名次的项目)
rank int
status ResultStatus
entryStatus EntryStatus
state ResultState
shares []TeamShare
}
func fixture(ps []participant) ([]Entry, []Result) {
entries := make([]Entry, 0, len(ps))
results := make([]Result, 0, len(ps))
for _, p := range ps {
entries = append(entries, Entry{
ID: p.entryID, EventID: 1, CategoryID: 1, TeamID: p.teamID,
AthleteID: p.athleteID, AthleteName: p.name,
Status: p.entryStatus, TeamShares: p.shares,
})
state := p.state
if state == "" {
state = ResultStatePublished
}
var value *float64
if !p.noValue {
value = ptr(p.value)
}
results = append(results, Result{
ID: p.entryID * 100, EventID: 1, EntryID: p.entryID,
Rank: p.rank, ResultValue: value, Status: p.status, State: state,
})
}
return entries, results
}
// lineup 生成 n 个参赛者:成绩 1..n,entryID 1..n。
func lineup(n int) []participant {
ps := make([]participant, 0, n)
for i := 1; i <= n; i++ {
ps = append(ps, participant{
entryID: int64(i), athleteID: int64(100 + i), teamID: int64(1000 + i),
name: "选手" + string(rune('A'+i-1)), value: float64(i),
})
}
return ps
}
// lineupValues 生成 n 个参赛者并指定成绩值,用于构造并列。
func lineupValues(values ...float64) []participant {
ps := make([]participant, 0, len(values))
for i, v := range values {
ps = append(ps, participant{
entryID: int64(i + 1), athleteID: int64(100 + i + 1), teamID: int64(1000 + i + 1),
name: "选手" + string(rune('A'+i)), value: v,
})
}
return ps
}
func mustScore(t *testing.T, in EventScoreInput) EventScore {
t.Helper()
es, err := ScoreEvent(in)
if err != nil {
t.Fatalf("ScoreEvent 返回错误: %v", err)
}
return es
}
func pointsByEntry(ledger []LedgerEntry) map[int64]float64 {
out := make(map[int64]float64)
for _, e := range ledger {
if e.Voided {
continue
}
out[e.EntryID] += e.Points
}
return out
}
func rankByEntry(ranked []RankedResult) map[int64]int {
out := make(map[int64]int, len(ranked))
for _, r := range ranked {
out[r.EntryID] = r.Rank
}
return out
}
func pointsByTeam(ledger []LedgerEntry) map[int64]float64 {
out := make(map[int64]float64)
for _, e := range ledger {
if e.Voided {
continue
}
out[e.TeamID] += e.Points
}
return out
}
// assertEntryPoints 断言 entryID 1..len(want) 的积分依次等于 want;缺键按 0 处理。
func assertEntryPoints(t *testing.T, got map[int64]float64, want []float64) {
t.Helper()
for i, w := range want {
id := int64(i + 1)
if !nearlyEqual(got[id], w) {
t.Errorf("entry %d 积分 = %v,期望 %v", id, got[id], w)
}
}
}
func testTeams(ids ...int64) []Team {
out := make([]Team, 0, len(ids))
for i, id := range ids {
out = append(out, Team{
ID: id, Code: string(rune('A' + i)), Name: "单位" + string(rune('A'+i)), Active: true,
})
}
return out
}
func ledgerFor(teamID int64, points float64) LedgerEntry {
return LedgerEntry{TeamID: teamID, SourceType: SourceResult, SourceID: teamID, Points: points}
}
func ledgerForAthlete(teamID, athleteID int64, points float64) LedgerEntry {
return LedgerEntry{TeamID: teamID, AthleteID: athleteID, SourceType: SourceResult, SourceID: athleteID, Points: points}
}