Files
Scoreboard/server/internal/domain/standings_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

248 lines
7.2 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 mkRanked(teamID int64, scope Scope, rank int) RankedResult {
return RankedResult{
TeamID: teamID,
Shares: []TeamShare{{TeamID: teamID, Weight: 1}},
Scope: scope,
Rank: rank,
}
}
func mkAthleteRanked(athleteID, teamID int64, rank int) RankedResult {
r := mkRanked(teamID, ScopeAthlete, rank)
r.AthleteID = athleteID
return r
}
func TestTeamStandingsWeight(t *testing.T) {
ledger := []LedgerEntry{
ledgerFor(1, 10), // 仅记单位
ledgerForAthlete(1, 9, 10), // 记到个人
}
cases := []struct {
name string
weight float64
want float64
}{
{"系数 1", 1, 20},
{"系数 0.5", 0.5, 15},
{"系数 2", 2, 30},
{"系数 0 按 1 处理", 0, 20},
{"系数负数按 1 处理", -1, 20},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rows := BuildTeamStandings(StandingsInput{
Teams: testTeams(1), Ledger: ledger, IndividualToTeamWeight: tc.weight,
})
if !nearlyEqual(rows[0].Points, tc.want) {
t.Fatalf("总分 = %v,期望 %v", rows[0].Points, tc.want)
}
if !nearlyEqual(rows[0].TeamOnlyPoints, 10) || !nearlyEqual(rows[0].IndividualPoints, 10) {
t.Fatalf("分项 = %v/%v,期望 10/10", rows[0].TeamOnlyPoints, rows[0].IndividualPoints)
}
})
}
}
func TestTeamStandingsSortOrder(t *testing.T) {
cases := []struct {
name string
ranked []RankedResult
want int64
}{
{"总分优先", nil, 1},
{"金牌优先", []RankedResult{mkRanked(2, ScopeAthlete, 1)}, 2},
{
"银牌优先",
[]RankedResult{
mkRanked(1, ScopeAthlete, 1),
mkRanked(2, ScopeAthlete, 1),
mkRanked(2, ScopeAthlete, 2),
},
2,
},
{
"铜牌优先",
[]RankedResult{
mkRanked(1, ScopeAthlete, 1),
mkRanked(2, ScopeAthlete, 1),
mkRanked(1, ScopeAthlete, 2),
mkRanked(2, ScopeAthlete, 2),
mkRanked(2, ScopeAthlete, 3),
},
2,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ledger := []LedgerEntry{ledgerFor(1, 10), ledgerFor(2, 10)}
if tc.name == "总分优先" {
ledger[0].Points = 20
}
rows := BuildTeamStandings(StandingsInput{Teams: testTeams(1, 2), Ledger: ledger, Ranked: tc.ranked})
if rows[0].TeamID != tc.want {
t.Fatalf("榜首 = 单位 %d,期望 %d", rows[0].TeamID, tc.want)
}
if rows[0].Rank != 1 || rows[1].Rank != 2 {
t.Fatalf("名次 = %d/%d,期望 1/2", rows[0].Rank, rows[1].Rank)
}
})
}
}
func TestTeamStandingsIncludesAllTeams(t *testing.T) {
t.Run("没有得分的单位也占一行", func(t *testing.T) {
rows := BuildTeamStandings(StandingsInput{Teams: testTeams(1, 2), Ledger: []LedgerEntry{ledgerFor(1, 10)}})
if len(rows) != 2 {
t.Fatalf("行数 = %d,期望 2", len(rows))
}
if rows[1].TeamID != 2 || rows[1].Points != 0 {
t.Fatalf("第二行 = %+v,期望单位2零分", rows[1])
}
})
t.Run("流水里出现的未知单位也上榜", func(t *testing.T) {
rows := BuildTeamStandings(StandingsInput{Teams: testTeams(1), Ledger: []LedgerEntry{ledgerFor(9, 5)}})
if len(rows) != 2 || rows[0].TeamID != 9 {
t.Fatalf("行 = %+v,期望未知单位9以5分居首", rows)
}
})
}
func TestMedalStandings(t *testing.T) {
ranked := []RankedResult{
mkRanked(1, ScopeTeam, 1), // 团体项目金牌
mkRanked(2, ScopeAthlete, 1), // 个人项目金牌
mkRanked(2, ScopeAthlete, 3),
}
in := StandingsInput{Teams: testTeams(1, 2), Ranked: ranked}
rows := BuildMedalStandings(in)
if len(rows) != 2 {
t.Fatalf("行数 = %d,期望 2", len(rows))
}
var gold, silver, bronze, total int
for _, r := range rows {
gold += r.Gold
silver += r.Silver
bronze += r.Bronze
total += r.Total
}
if gold != 2 || silver != 0 || bronze != 1 || total != 3 {
t.Fatalf("奖牌合计 = %d/%d/%d(共%d),期望 2/0/1(共3)", gold, silver, bronze, total)
}
in.ExcludeTeamEventMedals = true
rows = BuildMedalStandings(in)
for _, r := range rows {
if r.TeamID == 1 && r.Gold != 0 {
t.Fatalf("排除团体奖牌后单位1金牌 = %d,期望 0", r.Gold)
}
}
}
func TestAthleteStandings(t *testing.T) {
ledger := []LedgerEntry{
ledgerForAthlete(1001, 1, 10),
ledgerForAthlete(1002, 2, 10),
}
athletes := []Athlete{
{ID: 1, Name: "张三", TeamID: 1001},
{ID: 2, Name: "李四", TeamID: 1002},
}
teams := []Team{
{ID: 1001, Code: "A", Name: "一单位"},
{ID: 1002, Code: "B", Name: "二单位"},
}
t.Run("金牌优先", func(t *testing.T) {
rows := BuildAthleteStandings(StandingsInput{
Teams: teams, Athletes: athletes, Ledger: ledger,
Ranked: []RankedResult{mkAthleteRanked(2, 1002, 1)},
})
if rows[0].AthleteID != 2 {
t.Fatalf("榜首 = %+v,期望运动员2(金牌优先)", rows)
}
})
t.Run("同分按姓名", func(t *testing.T) {
rows := BuildAthleteStandings(StandingsInput{Teams: teams, Athletes: athletes, Ledger: ledger})
if rows[0].AthleteID != 1 {
t.Fatalf("榜首 = %+v,期望运动员1(张三 < 李四)", rows)
}
})
t.Run("姓名与单位信息齐全", func(t *testing.T) {
rows := BuildAthleteStandings(StandingsInput{Teams: teams, Athletes: athletes, Ledger: ledger})
if rows[0].Name != "张三" || rows[0].TeamCode != "A" || rows[0].TeamName != "一单位" {
t.Fatalf("行 = %+v,期望姓名/单位信息齐全", rows[0])
}
})
t.Run("团体项目奖牌不计入个人榜", func(t *testing.T) {
rows := BuildAthleteStandings(StandingsInput{
Teams: teams, Athletes: athletes, Ledger: ledger,
Ranked: []RankedResult{{
TeamID: 1001, AthleteID: 1, Scope: ScopeTeam, Rank: 1,
Shares: []TeamShare{{TeamID: 1001, Weight: 1}},
}},
})
for _, r := range rows {
if r.AthleteID == 1 && r.Gold != 0 {
t.Fatalf("个人榜不应统计团体项目奖牌: %+v", r)
}
}
})
}
func TestUnionMedals(t *testing.T) {
rr := RankedResult{
TeamID: 1, Rank: 1, Scope: ScopeAthlete,
Shares: []TeamShare{{TeamID: 1, Weight: 0.6}, {TeamID: 2, Weight: 0.4}},
}
t.Run("拆分时两单位各记一金", func(t *testing.T) {
rows := BuildTeamStandings(StandingsInput{Teams: testTeams(1, 2), Ranked: []RankedResult{rr}})
for _, r := range rows {
if r.Gold != 1 {
t.Fatalf("单位 %d 金牌 = %d,期望 1", r.TeamID, r.Gold)
}
}
})
t.Run("整体归属只记主单位", func(t *testing.T) {
rr := rr
rr.Shares = []TeamShare{{TeamID: 1, Weight: 1}}
rows := BuildTeamStandings(StandingsInput{Teams: testTeams(1, 2), Ranked: []RankedResult{rr}})
if rows[0].TeamID != 1 || rows[0].Gold != 1 {
t.Fatalf("整体归属榜首 = %+v,期望单位1记1金", rows[0])
}
if rows[1].Gold != 0 {
t.Fatalf("单位2金牌 = %d,期望 0", rows[1].Gold)
}
})
}
func TestStandingsEmptyInput(t *testing.T) {
t.Run("无单位无流水", func(t *testing.T) {
if rows := BuildTeamStandings(StandingsInput{}); len(rows) != 0 {
t.Fatalf("行数 = %d,期望 0", len(rows))
}
})
t.Run("个人榜无数据", func(t *testing.T) {
if rows := BuildAthleteStandings(StandingsInput{}); len(rows) != 0 {
t.Fatalf("行数 = %d,期望 0", len(rows))
}
})
}
func TestSumByTeamIgnoresVoided(t *testing.T) {
ledger := []LedgerEntry{ledgerFor(1, 10), ledgerFor(1, 5)}
ledger[1].Voided = true
got := SumByTeam(ledger, 1)
if !nearlyEqual(got[1].Total, 10) {
t.Fatalf("总分 = %v,期望只算未作废的 10", got[1].Total)
}
}