package domain import ( "strconv" "testing" ) // 本文件守住 devdocs/17 §4.5 的奖牌口径: // 入选 = 小项结算奖牌 && 通道是奖牌通道 && 名次前三; // 单位榜再加 scope == team || count_to_team_medals; // 个人榜只取 scope == athlete && AthleteID != 0。 // 奖牌开关只影响奖牌,不影响积分流水。 // medalInput 是奖牌开关的测试底座:3 名运动员各代表一个单位,个人通道按成绩排出 1/2/3 名。 func medalInput(ch ScoreChannel) EventScoreInput { entries, results := fixture(lineupValues(11, 12, 13)) return EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{ch}, Profiles: map[int64]ScoringProfile{1: individualTop6()}, Entries: entries, Results: results, } } // medalTotals 汇总单位榜 / 个人榜的金银铜。 func medalTotals(t *testing.T, in StandingsInput) (team, athlete [3]int) { t.Helper() for _, r := range BuildMedalStandings(in) { team[0] += r.Gold team[1] += r.Silver team[2] += r.Bronze } for _, r := range BuildAthleteStandings(in) { athlete[0] += r.Gold athlete[1] += r.Silver athlete[2] += r.Bronze } return team, athlete } func TestAwardMedalsSwitch(t *testing.T) { cases := []struct { name string award bool wantTeam [3]int wantAthlet [3]int }{ {"开启奖牌结算", true, [3]int{1, 1, 1}, [3]int{1, 1, 1}}, {"关闭奖牌结算", false, [3]int{0, 0, 0}, [3]int{0, 0, 0}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { in := medalInput(athleteResultChannel(1)) in.Event.AwardMedals = tc.award es := mustScore(t, in) if len(es.Ledger) != 3 { t.Fatalf("流水条数 = %d,期望 3(奖牌开关不影响积分)", len(es.Ledger)) } if got := pointsByEntry(es.Ledger); !nearlyEqual(got[1], 10) || !nearlyEqual(got[3], 6) { t.Fatalf("积分 = %v,期望 10/8/6(奖牌开关不影响积分)", got) } team, athlete := medalTotals(t, StandingsInput{ Teams: testTeams(1001, 1002, 1003), Ranked: es.Ranked, }) if team != tc.wantTeam || athlete != tc.wantAthlet { t.Fatalf("奖牌 = 单位 %v / 个人 %v,期望 %v / %v", team, athlete, tc.wantTeam, tc.wantAthlet) } }) } } func TestChannelCountToMedalsSwitch(t *testing.T) { cases := []struct { name string countTo *bool want [3]int }{ {"未配置按发牌", nil, [3]int{1, 1, 1}}, {"显式开启", boolPtr(true), [3]int{1, 1, 1}}, {"显式关闭", boolPtr(false), [3]int{0, 0, 0}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ch := athleteResultChannel(1) ch.CountToMedals = tc.countTo in := medalInput(ch) es := mustScore(t, in) if len(es.Ledger) != 3 { t.Fatalf("流水条数 = %d,期望 3(奖牌通道开关不影响积分)", len(es.Ledger)) } team, athlete := medalTotals(t, StandingsInput{ Teams: testTeams(1001, 1002, 1003), Ranked: es.Ranked, }) if team != tc.want || athlete != tc.want { t.Fatalf("奖牌 = 单位 %v / 个人 %v,期望 %v", team, athlete, tc.want) } }) } } // 个人奖牌不并入单位榜:个人榜照常记牌,单位榜一枚都没有。 func TestCountToTeamMedalsSwitch(t *testing.T) { ch := athleteResultChannel(1) ch.CountToTeamMedals = boolPtr(false) in := medalInput(ch) es := mustScore(t, in) team, athlete := medalTotals(t, StandingsInput{ Teams: testTeams(1001, 1002, 1003), Ranked: es.Ranked, }) if team != [3]int{0, 0, 0} { t.Fatalf("单位奖牌榜 = %v,期望 0(个人奖牌不并入单位榜)", team) } if athlete != [3]int{1, 1, 1} { t.Fatalf("个人奖牌榜 = %v,期望 1/1/1", athlete) } // 单位总分不受奖牌通道开关影响。 if got := SumByTeam(es.Ledger, 1); !nearlyEqual(got[1001].Total, 10) || !nearlyEqual(got[1003].Total, 6) { t.Fatalf("单位总分 = %v,期望 10/8/6(奖牌通道开关不影响积分)", got) } } // scope=team 的奖牌不受 count_to_team_medals 影响:它本来就记在单位头上。 func TestTeamScopeMedalIgnoresCountToTeamMedals(t *testing.T) { ch := teamManualChannel(2) ch.CountToTeamMedals = boolPtr(false) es := mustScore(t, EventScoreInput{ Event: teamEvent(1), Category: testCategory(), Channels: []ScoreChannel{ch}, Profiles: map[int64]ScoringProfile{2: teamTop6()}, TeamRanks: []TeamManualRank{{TeamID: 1001, Rank: 1}, {TeamID: 1002, Rank: 2}}, }) team, athlete := medalTotals(t, StandingsInput{Teams: testTeams(1001, 1002), Ranked: es.Ranked}) if team != [3]int{1, 1, 0} { t.Fatalf("单位奖牌榜 = %v,期望 1 金 1 银", team) } if athlete != [3]int{0, 0, 0} { t.Fatalf("个人奖牌榜 = %v,期望 0(团体名次不发个人牌)", athlete) } } // 旧全局开关与 per-通道开关是「与」的关系:总闸关掉时团体项目的奖牌一律不进单位榜。 func TestExcludeTeamEventMedalsStillApplies(t *testing.T) { es := mustScore(t, EventScoreInput{ Event: teamEvent(1), Category: testCategory(), Channels: []ScoreChannel{teamManualChannel(2)}, Profiles: map[int64]ScoringProfile{2: teamTop6()}, TeamRanks: []TeamManualRank{{TeamID: 1001, Rank: 1}}, }) teams := testTeams(1001) team, _ := medalTotals(t, StandingsInput{Teams: teams, Ranked: es.Ranked}) if team != [3]int{1, 0, 0} { t.Fatalf("默认口径单位榜 = %v,期望 1 金", team) } team, _ = medalTotals(t, StandingsInput{Teams: teams, Ranked: es.Ranked, ExcludeTeamEventMedals: true}) if team != [3]int{0, 0, 0} { t.Fatalf("全局排除团体项目奖牌后单位榜 = %v,期望 0", team) } } // P1(5) 回归:individual_team 项目的团体聚合名次现在能进 Ranked 并产出单位金牌。 func TestAggregateTeamRankProducesTeamMedal(t *testing.T) { entries, results := fixture([]participant{ {entryID: 1, athleteID: 1, teamID: 1001, value: 11}, {entryID: 2, athleteID: 2, teamID: 1001, value: 12}, {entryID: 3, athleteID: 3, teamID: 1002, value: 13}, }) ev := individualEvent(1) ev.EventType = EventTypeIndividualTeam ev.Compare = CompareAsc // 团体平均成绩越小越好 es := mustScore(t, EventScoreInput{ Event: ev, Category: testCategory(), Channels: []ScoreChannel{ athleteResultChannel(1), {ID: 2, EventID: 1, Scope: ScopeTeam, Basis: BasisAggregate, ProfileID: 2}, }, Profiles: map[int64]ScoringProfile{1: individualTop6(), 2: teamTop6()}, Entries: entries, Results: results, }) var teamRanked []RankedResult for _, rr := range es.Ranked { if rr.ChannelID != 2 { continue } teamRanked = append(teamRanked, rr) if rr.EntryID != 0 || rr.ResultID != 0 || rr.AthleteID != 0 { t.Fatalf("团体聚合名次 = %+v,期望不对应单条报名项 / 成绩", rr) } if rr.SubjectKey != "t:"+strconv.FormatInt(rr.TeamID, 10) { t.Fatalf("团体聚合名次 SubjectKey = %q,期望 t:%d", rr.SubjectKey, rr.TeamID) } } if len(teamRanked) != 2 { t.Fatalf("团体聚合名次 = %d 条,期望 2 条:%+v", len(teamRanked), teamRanked) } // 只拿团体通道的名次也能看出:1001 第 1 名(金),1002 第 2 名(银)。 aggOnly := BuildTeamStandings(StandingsInput{Teams: testTeams(1001, 1002), Ranked: teamRanked}) if aggOnly[0].TeamID != 1001 || aggOnly[0].Gold != 1 { t.Fatalf("团体名次单独统计 = %+v,期望单位 1001 记 1 金", aggOnly) } if aggOnly[1].TeamID != 1002 || aggOnly[1].Silver != 1 { t.Fatalf("团体名次单独统计 = %+v,期望单位 1002 记 1 银", aggOnly) } // 全量榜单:个人通道(1 金 1 银 1 铜)与团体通道(1 金 1 银)各记一枚,互不去重。 team, athlete := medalTotals(t, StandingsInput{Teams: testTeams(1001, 1002), Ranked: es.Ranked}) if team != [3]int{2, 2, 1} { t.Fatalf("单位奖牌合计 = %v,期望 2 金 2 银 1 铜", team) } if athlete != [3]int{1, 1, 1} { t.Fatalf("个人奖牌合计 = %v,期望 1/1/1(团体名次不进个人榜)", athlete) } }