feat: 用户徽章与等级体系,积分经济管理重构

- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件
- 新增等级模型:model/level 及前端 levels 元数据
- 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置
- 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
2026-09-25 16:38:29 +08:00
parent 7b327bc8cc
commit 4ae57d9392
57 changed files with 3939 additions and 147 deletions

106
backend/model/level_test.go Normal file
View File

@@ -0,0 +1,106 @@
package model
import "testing"
// withLevelDefs 临时切换等级快照并在测试结束后恢复现场(快照为进程级全局态)
func withLevelDefs(t *testing.T, defs []LevelDef) {
t.Helper()
prev := LevelDefs()
t.Cleanup(func() { ApplyLevelDefs(prev) })
ApplyLevelDefs(defs)
}
func TestLevelOfDefault(t *testing.T) {
withLevelDefs(t, DefaultLevelDefs())
cases := []struct {
total int
want int
}{
{0, 1},
{49, 1},
{50, 2},
{51, 2},
{199, 2},
{200, 3},
{500, 4},
{1200, 5},
{3000, 6},
{7999, 6},
{8000, 7},
{1 << 30, 7},
{-5, 1},
}
for _, c := range cases {
if got := LevelOf(c.total); got != c.want {
t.Errorf("LevelOf(%d) = %d, want %d", c.total, got, c.want)
}
}
}
func TestLevelOfCustomConfig(t *testing.T) {
withLevelDefs(t, []LevelDef{
{Threshold: 0, Color: "neutral"},
{Threshold: 10, Color: "gold"},
{Threshold: 100, Color: "info"},
})
cases := []struct {
total int
want int
}{
{0, 1},
{9, 1},
{10, 2},
{99, 2},
{100, 3},
{1 << 20, 3},
}
for _, c := range cases {
if got := LevelOf(c.total); got != c.want {
t.Errorf("LevelOf(%d) = %d, want %d", c.total, got, c.want)
}
}
}
func TestApplyLevelDefsFallback(t *testing.T) {
withLevelDefs(t, DefaultLevelDefs())
// 非法配置(首级阈值非 0、数量不足、非递增)一律回落默认表
invalid := [][]LevelDef{
{{Threshold: 10, Color: "gold"}, {Threshold: 50, Color: "info"}},
{{Threshold: 0, Color: "neutral"}},
{{Threshold: 0, Color: "neutral"}, {Threshold: 50, Color: "info"}, {Threshold: 50, Color: "ok"}},
{{Threshold: 0, Color: "neutral"}, {Threshold: -1, Color: "info"}},
}
for _, defs := range invalid {
ApplyLevelDefs(defs)
got := LevelDefs()
if len(got) != len(DefaultLevelDefs()) {
t.Fatalf("ApplyLevelDefs(invalid) 未回落默认表: got %d levels", len(got))
}
}
}
func TestValidLevelDefs(t *testing.T) {
valid := []LevelDef{{Threshold: 0, Color: "neutral"}, {Threshold: 1, Color: "gold"}}
if !ValidLevelDefs(valid) {
t.Errorf("ValidLevelDefs(合法两极) = false")
}
tooMany := make([]LevelDef, MaxLevelCount+1)
for i := range tooMany {
tooMany[i] = LevelDef{Threshold: i, Color: "gold"}
}
if ValidLevelDefs(tooMany) {
t.Errorf("ValidLevelDefs(超上限) = true")
}
}
func TestLevelDefsBoundaryCount(t *testing.T) {
withLevelDefs(t, DefaultLevelDefs())
defs := make([]LevelDef, MaxLevelCount)
for i := range defs {
defs[i] = LevelDef{Threshold: i * 10, Color: "gold"}
}
ApplyLevelDefs(defs)
if got := LevelOf(1 << 30); got != MaxLevelCount {
t.Errorf("LevelOf(极大值) = %d, want %d", got, MaxLevelCount)
}
}