package domain import "testing" // ---------- 通用构造 ---------- func ptr(f float64) *float64 { return &f } func boolPtr(b bool) *bool { return &b } // 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, RecordEligible: true, Scored: true, AwardMedals: 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 } 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, }) 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, CountToTeam: true} } func ledgerForAthlete(teamID, athleteID int64, points float64) LedgerEntry { return LedgerEntry{TeamID: teamID, AthleteID: athleteID, SourceType: SourceResult, SourceID: athleteID, Points: points, CountToTeam: true} } // ledgerForPersonal 是「不计入单位总分」的流水:只记在个人头上(如「个人+团体」项目的个人通道)。 func ledgerForPersonal(teamID, athleteID int64, points float64) LedgerEntry { e := ledgerForAthlete(teamID, athleteID, points) e.CountToTeam = false return e }