feat: M0-M2 骨架落地(服务端/前端/宿主/数据库/积分引擎)
- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由 - store: system.db + event.db 两套迁移,业务表 + 种子数据 - domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试 - web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端) - console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
This commit is contained in:
1 parent
a61d793d79
commit
7d5d94f815
76 files changed
+10131
No files matched your search
@@ -0,0 +1,195 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func bonusConfig(mode RecordBonusMode) RecordBonusConfig {
|
||||
return RecordBonusConfig{
|
||||
Enabled: true, Mode: mode,
|
||||
Levels: []RecordLevel{
|
||||
{Code: "meet", Name: "赛会纪录", Points: 2},
|
||||
{Code: "city", Name: "市级纪录", Points: 5},
|
||||
{Code: "province", Name: "省级纪录", Points: 8},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusDisabled(t *testing.T) {
|
||||
cfg := bonusConfig(RecordBonusMax)
|
||||
cfg.Enabled = false
|
||||
got := RecordBonusPoints(cfg, []RecordBreak{{EntryID: 1, Level: "meet", State: RecordBreakApproved}})
|
||||
if got != nil {
|
||||
t.Fatalf("未启用时 = %+v,期望 nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusStateFilter(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
state RecordBreakState
|
||||
want float64
|
||||
}{
|
||||
{"已通过计入", RecordBreakApproved, 2},
|
||||
{"待审核不计入", RecordBreakPending, 0},
|
||||
{"已驳回不计入", RecordBreakRejected, 0},
|
||||
{"状态空值不计入", "", 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := RecordBonusPoints(bonusConfig(RecordBonusMax), []RecordBreak{{EntryID: 1, Level: "meet", State: tc.state}})
|
||||
if tc.want == 0 {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("结果 = %+v,期望空", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(got) != 1 || !nearlyEqual(got[0].Points, tc.want) {
|
||||
t.Fatalf("结果 = %+v,期望 %v 分", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusUnknownLevel(t *testing.T) {
|
||||
got := RecordBonusPoints(bonusConfig(RecordBonusMax), []RecordBreak{{EntryID: 1, Level: "galaxy", State: RecordBreakApproved}})
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("未配置档位 = %+v,期望空(不能凭空加分)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusModes(t *testing.T) {
|
||||
breaks := []RecordBreak{
|
||||
{ID: 1, EntryID: 1, Level: "meet", State: RecordBreakApproved},
|
||||
{ID: 2, EntryID: 1, Level: "city", State: RecordBreakApproved},
|
||||
{ID: 3, EntryID: 1, Level: "province", State: RecordBreakApproved},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
mode RecordBonusMode
|
||||
want float64
|
||||
}{
|
||||
{"max 取最高档", RecordBonusMax, 8},
|
||||
{"sum 累加", RecordBonusSum, 15},
|
||||
{"模式空值按 max", "", 8},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := RecordBonusPoints(bonusConfig(tc.mode), breaks)
|
||||
if len(got) != 1 || !nearlyEqual(got[0].Points, tc.want) {
|
||||
t.Fatalf("结果 = %+v,期望 %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusGroupsByEntry(t *testing.T) {
|
||||
breaks := []RecordBreak{
|
||||
{ID: 1, EntryID: 2, Level: "meet", State: RecordBreakApproved},
|
||||
{ID: 2, EntryID: 1, Level: "city", State: RecordBreakApproved},
|
||||
{ID: 3, EntryID: 1, Level: "meet", State: RecordBreakApproved},
|
||||
}
|
||||
got := RecordBonusPoints(bonusConfig(RecordBonusMax), breaks)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("条数 = %d,期望 2(按报名项分组)", len(got))
|
||||
}
|
||||
if got[0].EntryID != 1 || got[1].EntryID != 2 {
|
||||
t.Fatalf("顺序 = %+v,期望按 EntryID 升序", got)
|
||||
}
|
||||
if !nearlyEqual(got[0].Points, 5) || !nearlyEqual(got[1].Points, 2) {
|
||||
t.Fatalf("分值 = %v/%v,期望 5/2", got[0].Points, got[1].Points)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusDedupLevel(t *testing.T) {
|
||||
breaks := []RecordBreak{
|
||||
{ID: 1, EntryID: 1, Level: "meet", State: RecordBreakApproved},
|
||||
{ID: 2, EntryID: 1, Level: "meet", State: RecordBreakApproved},
|
||||
}
|
||||
got := RecordBonusPoints(bonusConfig(RecordBonusSum), breaks)
|
||||
if len(got) != 1 || !nearlyEqual(got[0].Points, 2) {
|
||||
t.Fatalf("重复档位 = %+v,期望只计一次 2 分", got)
|
||||
}
|
||||
if len(got[0].Levels) != 1 {
|
||||
t.Fatalf("档位列表 = %v,期望去重", got[0].Levels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusNoBreaks(t *testing.T) {
|
||||
if got := RecordBonusPoints(bonusConfig(RecordBonusMax), nil); got != nil {
|
||||
t.Fatalf("无破纪录登记 = %+v,期望 nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordBonusLedger(t *testing.T) {
|
||||
entries, results := fixture(lineup(2))
|
||||
base := EventScoreInput{
|
||||
Event: individualEvent(1), Category: testCategory(),
|
||||
Channels: []ScoreChannel{athleteResultChannel(1)},
|
||||
Profiles: map[int64]ScoringProfile{1: individualTop6()},
|
||||
Entries: entries, Results: results,
|
||||
RecordBreaks: []RecordBreak{{
|
||||
ID: 1, EventID: 1, EntryID: 1, AthleteID: 101, TeamID: 1001,
|
||||
Level: "meet", State: RecordBreakApproved,
|
||||
}},
|
||||
}
|
||||
|
||||
t.Run("未启用不产生加分流水", func(t *testing.T) {
|
||||
cfg := bonusConfig(RecordBonusMax)
|
||||
cfg.Enabled = false
|
||||
base.RecordBonus = &cfg
|
||||
es := mustScore(t, base)
|
||||
for _, e := range es.Ledger {
|
||||
if e.SourceType == SourceRecordBonus {
|
||||
t.Fatalf("不应有加分流水: %+v", e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("启用后归属到人", func(t *testing.T) {
|
||||
cfg := bonusConfig(RecordBonusMax)
|
||||
base.RecordBonus = &cfg
|
||||
es := mustScore(t, base)
|
||||
var found bool
|
||||
for _, e := range es.Ledger {
|
||||
if e.SourceType != SourceRecordBonus {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if e.TeamID != 1001 || e.AthleteID != 101 || !nearlyEqual(e.Points, 2) {
|
||||
t.Fatalf("加分流水 = %+v,期望 1001/101 记 2 分", e)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("未产生加分流水")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("联队按份额拆分加分", func(t *testing.T) {
|
||||
ps := []participant{{
|
||||
entryID: 1, athleteID: 101, teamID: 1001, value: 1,
|
||||
shares: []TeamShare{{TeamID: 1001, Weight: 3}, {TeamID: 1002, Weight: 2}},
|
||||
}}
|
||||
e2, r2 := fixture(ps)
|
||||
cfg := bonusConfig(RecordBonusMax)
|
||||
es := mustScore(t, EventScoreInput{
|
||||
Event: individualEvent(1), Category: testCategory(),
|
||||
Channels: []ScoreChannel{athleteResultChannel(1)},
|
||||
Profiles: map[int64]ScoringProfile{1: individualTop6()},
|
||||
Entries: e2, Results: r2,
|
||||
RecordBonus: &cfg,
|
||||
RecordBreaks: []RecordBreak{{
|
||||
ID: 1, EventID: 1, EntryID: 1, AthleteID: 101, TeamID: 1001,
|
||||
Level: "meet", State: RecordBreakApproved,
|
||||
}},
|
||||
Options: ScoreOptions{UnionAttribution: UnionSplit},
|
||||
})
|
||||
var bonus float64
|
||||
for _, e := range es.Ledger {
|
||||
if e.SourceType == SourceRecordBonus && e.TeamID == 1002 {
|
||||
bonus += e.Points
|
||||
}
|
||||
}
|
||||
if !nearlyEqual(bonus, 0.8) {
|
||||
t.Fatalf("单位1002 的加分 = %v,期望 2 × 0.4 = 0.8", bonus)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user