Files
Scoreboard/server/internal/domain/helpers_test.go
T
AskaEth 7d5d94f815 feat: M0-M2 骨架落地(服务端/前端/宿主/数据库/积分引擎)
- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由
- store: system.db + event.db 两套迁移,业务表 + 种子数据
- domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试
- web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端)
- console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
2026-09-09 18:36:40 +08:00

201 lines
5.7 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, method AggregateMethod) ScoreChannel {
return ScoreChannel{ID: 3, EventID: 1, Scope: ScopeTeam, Basis: BasisAggregate, ProfileID: profileID, Aggregate: method}
}
// 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}
}