- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力 - realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份 - httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表 - web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换 - console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复 - docs: 用户文档三份 + README - 主代理: 破纪录加分接线(levels 未加载导致恒 0)
277 lines
9.5 KiB
Go
277 lines
9.5 KiB
Go
package domain
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
)
|
||
|
||
func TestAppendLedger(t *testing.T) {
|
||
t.Run("自动续号", func(t *testing.T) {
|
||
out, err := AppendLedger([]LedgerEntry{{ID: 3, Points: 1}}, LedgerEntry{Points: 2}, LedgerEntry{Points: 3})
|
||
if err != nil {
|
||
t.Fatalf("追加失败: %v", err)
|
||
}
|
||
if len(out) != 3 || out[1].ID != 4 || out[2].ID != 5 {
|
||
t.Fatalf("id 续号错误: %+v", out)
|
||
}
|
||
})
|
||
t.Run("保留已有行", func(t *testing.T) {
|
||
base := []LedgerEntry{{ID: 1, Points: 5}}
|
||
out, _ := AppendLedger(base, LedgerEntry{Points: 2})
|
||
if base[0].Points != 5 || len(out) != 2 {
|
||
t.Fatalf("原流水被改动: base=%+v out=%+v", base, out)
|
||
}
|
||
})
|
||
t.Run("显式 id 冲突报错", func(t *testing.T) {
|
||
if _, err := AppendLedger([]LedgerEntry{{ID: 1}}, LedgerEntry{ID: 1}); !errors.Is(err, ErrDuplicateLedgerID) {
|
||
t.Fatalf("错误 = %v,期望 ErrDuplicateLedgerID", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestVoidLedger(t *testing.T) {
|
||
base := []LedgerEntry{{ID: 1, Points: 5}, {ID: 2, Points: 3}}
|
||
|
||
t.Run("作废指定流水", func(t *testing.T) {
|
||
out, err := VoidLedger(base, 1, "admin", "录入错误")
|
||
if err != nil {
|
||
t.Fatalf("作废失败: %v", err)
|
||
}
|
||
if !out[0].Voided || out[0].VoidedBy != "admin" || out[0].VoidReason != "录入错误" {
|
||
t.Fatalf("作废信息不全: %+v", out[0])
|
||
}
|
||
if out[1].Voided {
|
||
t.Fatalf("误作废了其他流水: %+v", out[1])
|
||
}
|
||
if base[0].Voided {
|
||
t.Fatal("作废不应原地修改入参")
|
||
}
|
||
})
|
||
t.Run("重复作废报错", func(t *testing.T) {
|
||
out, _ := VoidLedger(base, 1, "admin", "x")
|
||
if _, err := VoidLedger(out, 1, "admin", "y"); !errors.Is(err, ErrLedgerAlreadyVoided) {
|
||
t.Fatalf("错误 = %v,期望 ErrLedgerAlreadyVoided", err)
|
||
}
|
||
})
|
||
t.Run("流水不存在报错", func(t *testing.T) {
|
||
if _, err := VoidLedger(base, 404, "admin", "x"); !errors.Is(err, ErrLedgerNotFound) {
|
||
t.Fatalf("错误 = %v,期望 ErrLedgerNotFound", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestVoidBySource(t *testing.T) {
|
||
base := []LedgerEntry{
|
||
{ID: 1, SourceType: SourceResult, SourceID: 100, Points: 10},
|
||
{ID: 2, SourceType: SourceResult, SourceID: 100, Points: 8},
|
||
{ID: 3, SourceType: SourceResult, SourceID: 200, Points: 6},
|
||
}
|
||
out, count := VoidBySource(base, SourceResult, 100, "admin", "撤销")
|
||
if count != 2 {
|
||
t.Fatalf("作废条数 = %d,期望 2", count)
|
||
}
|
||
if !out[0].Voided || !out[1].Voided || out[2].Voided {
|
||
t.Fatalf("作废范围错误: %+v", out)
|
||
}
|
||
|
||
t.Run("重复作废是空操作", func(t *testing.T) {
|
||
again, count := VoidBySource(out, SourceResult, 100, "admin", "再撤销")
|
||
if count != 0 {
|
||
t.Fatalf("重复作废条数 = %d,期望 0", count)
|
||
}
|
||
if len(again) != len(out) {
|
||
t.Fatalf("流水条数变了: %d", len(again))
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestSumByTeam(t *testing.T) {
|
||
ledger := []LedgerEntry{
|
||
ledgerFor(1, 10),
|
||
ledgerForAthlete(1, 101, 10),
|
||
ledgerFor(2, 6),
|
||
}
|
||
got := SumByTeam(ledger, 1)
|
||
if !nearlyEqual(got[1].Total, 20) || !nearlyEqual(got[1].TeamOnly, 10) || !nearlyEqual(got[1].Individual, 10) {
|
||
t.Fatalf("单位1汇总 = %+v,期望 总分20 / 单位10 / 个人10", got[1])
|
||
}
|
||
if !nearlyEqual(got[2].Total, 6) {
|
||
t.Fatalf("单位2汇总 = %+v,期望 6", got[2])
|
||
}
|
||
|
||
half := SumByTeam(ledger, 0.5)
|
||
if !nearlyEqual(half[1].Total, 15) {
|
||
t.Fatalf("系数 0.5 时单位1总分 = %v,期望 15", half[1].Total)
|
||
}
|
||
}
|
||
|
||
func TestSumByAthlete(t *testing.T) {
|
||
ledger := []LedgerEntry{
|
||
ledgerForAthlete(1, 101, 10),
|
||
ledgerForAthlete(1, 101, 2),
|
||
ledgerFor(1, 100), // 仅记单位,不进个人榜
|
||
}
|
||
got := SumByAthlete(ledger)
|
||
if len(got) != 1 || !nearlyEqual(got[101], 12) {
|
||
t.Fatalf("个人汇总 = %+v,期望只有 101:12", got)
|
||
}
|
||
}
|
||
|
||
func TestRecomputeEventLedger(t *testing.T) {
|
||
old := []LedgerEntry{
|
||
{ID: 1, EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10},
|
||
{ID: 2, EventID: 2, SourceType: SourceResult, SourceID: 200, TeamID: 1, Points: 8},
|
||
}
|
||
fresh := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 6}}
|
||
|
||
t.Run("有变化时旧流水作废并追加新流水", func(t *testing.T) {
|
||
out, err := RecomputeEventLedger(old, 1, fresh, "judge", "更正")
|
||
if err != nil {
|
||
t.Fatalf("重算失败: %v", err)
|
||
}
|
||
if len(out) != 3 {
|
||
t.Fatalf("流水条数 = %d,期望 3(旧2 + 新1)", len(out))
|
||
}
|
||
if !out[0].Voided || out[1].Voided {
|
||
t.Fatalf("作废范围错误: %+v", out)
|
||
}
|
||
if !nearlyEqual(SumByTeam(out, 1)[1].Total, 14) {
|
||
t.Fatalf("重算后总分 = %v,期望 6 + 8 = 14", SumByTeam(out, 1)[1].Total)
|
||
}
|
||
})
|
||
|
||
t.Run("无变化时原样返回", func(t *testing.T) {
|
||
same := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10}}
|
||
out, err := RecomputeEventLedger(old, 1, same, "judge", "重复发布")
|
||
if err != nil {
|
||
t.Fatalf("重算失败: %v", err)
|
||
}
|
||
if len(out) != len(old) {
|
||
t.Fatalf("流水条数 = %d,期望 %d(幂等)", len(out), len(old))
|
||
}
|
||
})
|
||
|
||
t.Run("只影响目标小项", func(t *testing.T) {
|
||
out, _ := RecomputeEventLedger(old, 1, fresh, "judge", "更正")
|
||
if out[1].Voided {
|
||
t.Fatalf("其他小项的流水被误作废: %+v", out[1])
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestApplyLedgerOps(t *testing.T) {
|
||
a := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10}}
|
||
b := []LedgerEntry{{EventID: 2, SourceType: SourceResult, SourceID: 200, TeamID: 2, Points: 8}}
|
||
|
||
t.Run("追加与作废可乱序收敛", func(t *testing.T) {
|
||
ops := []LedgerOp{
|
||
{Kind: LedgerOpAppend, Entries: b},
|
||
{Kind: LedgerOpAppend, Entries: a},
|
||
{Kind: LedgerOpVoidSource, SourceType: SourceResult, SourceID: 100, By: "admin", Reason: "撤销"},
|
||
}
|
||
out, err := ApplyLedgerOps(nil, ops)
|
||
if err != nil {
|
||
t.Fatalf("应用失败: %v", err)
|
||
}
|
||
got := SumByTeam(out, 1)
|
||
if !nearlyEqual(got[1].Total, 0) || !nearlyEqual(got[2].Total, 8) {
|
||
t.Fatalf("最终状态 = %+v,期望 1:0 / 2:8", got)
|
||
}
|
||
})
|
||
|
||
t.Run("未知操作报错", func(t *testing.T) {
|
||
if _, err := ApplyLedgerOps(nil, []LedgerOp{{Kind: "magic"}}); !errors.Is(err, ErrUnknownLedgerOp) {
|
||
t.Fatalf("错误 = %v,期望 ErrUnknownLedgerOp", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestAdjustmentsToLedger(t *testing.T) {
|
||
adj := PointAdjustment{ID: 1, TeamID: 5, Delta: -3, Category: AdjustmentPenalty, Reason: "犯规"}
|
||
|
||
t.Run("未启用时报错", func(t *testing.T) {
|
||
if _, err := AdjustmentsToLedger(ManualAdjustmentConfig{}, []PointAdjustment{adj}); !errors.Is(err, ErrManualAdjustmentDisabled) {
|
||
t.Fatalf("错误 = %v,期望 ErrManualAdjustmentDisabled", err)
|
||
}
|
||
})
|
||
t.Run("启用后转成流水", func(t *testing.T) {
|
||
out, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true, ReasonRequired: true}, []PointAdjustment{adj})
|
||
if err != nil {
|
||
t.Fatalf("转换失败: %v", err)
|
||
}
|
||
if len(out) != 1 || out[0].SourceType != SourceAdjustment || !nearlyEqual(out[0].Points, -3) {
|
||
t.Fatalf("流水 = %+v,期望一条 -3 的 adjustment", out)
|
||
}
|
||
})
|
||
t.Run("理由必填", func(t *testing.T) {
|
||
bad := adj
|
||
bad.Reason = " "
|
||
if _, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true, ReasonRequired: true}, []PointAdjustment{bad}); !errors.Is(err, ErrAdjustmentReasonRequired) {
|
||
t.Fatalf("错误 = %v,期望 ErrAdjustmentReasonRequired", err)
|
||
}
|
||
})
|
||
t.Run("已作废的调整被跳过", func(t *testing.T) {
|
||
voided := adj
|
||
voided.Voided = true
|
||
out, err := AdjustmentsToLedger(ManualAdjustmentConfig{Enabled: true}, []PointAdjustment{voided})
|
||
if err != nil || len(out) != 0 {
|
||
t.Fatalf("流水 = %+v err=%v,期望空", out, err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestActiveLedger(t *testing.T) {
|
||
ledger := []LedgerEntry{{ID: 1, Points: 1}, {ID: 2, Points: 2, Voided: true}}
|
||
got := ActiveLedger(ledger)
|
||
if len(got) != 1 || got[0].ID != 1 {
|
||
t.Fatalf("未作废流水 = %+v,期望只有 id=1", got)
|
||
}
|
||
}
|
||
|
||
// TestRecomputeEventLedgerKeepsAdjustments 验证重算只作废 result/record_bonus,
|
||
// 人工调整原样保留——否则每重算一次就把人工扣/加分抹掉。
|
||
func TestRecomputeEventLedgerKeepsAdjustments(t *testing.T) {
|
||
existing := []LedgerEntry{
|
||
{ID: 1, EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 10},
|
||
{ID: 2, EventID: 1, SourceType: SourceRecordBonus, SourceID: 100, TeamID: 1, Points: 2},
|
||
{ID: 3, EventID: 1, SourceType: SourceAdjustment, SourceID: 7, TeamID: 1, Points: -3},
|
||
}
|
||
fresh := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 6}}
|
||
|
||
out, err := RecomputeEventLedger(existing, 1, fresh, "judge", "成绩更正")
|
||
if err != nil {
|
||
t.Fatalf("重算失败: %v", err)
|
||
}
|
||
for _, e := range out {
|
||
if e.SourceType == SourceAdjustment {
|
||
if e.Voided {
|
||
t.Fatalf("人工调整被误作废: %+v", e)
|
||
}
|
||
}
|
||
}
|
||
if got := SumByTeam(out, 1)[1].Total; !nearlyEqual(got, 3) {
|
||
t.Fatalf("重算后总分 = %v,期望 6(新成绩分)- 3(调整保留)= 3", got)
|
||
}
|
||
}
|
||
|
||
// TestRecomputeEventLedgerIdempotentWithAdjustments 验证带人工调整时重复重算也幂等。
|
||
func TestRecomputeEventLedgerIdempotentWithAdjustments(t *testing.T) {
|
||
existing := []LedgerEntry{
|
||
{ID: 1, EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 6},
|
||
{ID: 2, EventID: 1, SourceType: SourceAdjustment, SourceID: 7, TeamID: 1, Points: -3},
|
||
}
|
||
fresh := []LedgerEntry{{EventID: 1, SourceType: SourceResult, SourceID: 100, TeamID: 1, Points: 6}}
|
||
out, err := RecomputeEventLedger(existing, 1, fresh, "judge", "重复发布")
|
||
if err != nil {
|
||
t.Fatalf("重算失败: %v", err)
|
||
}
|
||
if len(out) != len(existing) {
|
||
t.Fatalf("流水条数 = %d,期望 %d(幂等)", len(out), len(existing))
|
||
}
|
||
for _, e := range out {
|
||
if e.Voided {
|
||
t.Fatalf("幂等重算不应作废任何流水: %+v", e)
|
||
}
|
||
}
|
||
}
|