feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
159
backend/service/badge_test.go
Normal file
159
backend/service/badge_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newBadgeTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.User{}, &model.Badge{}, &model.UserBadge{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestBadgeGrantIdempotent(t *testing.T) {
|
||||
db := newBadgeTestDB(t)
|
||||
s := NewBadgeService(db)
|
||||
|
||||
user := model.User{Username: "alice", Password: "x"}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatalf("seed user: %v", err)
|
||||
}
|
||||
badge, err := s.Create(" 骨灰玩家 ", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if badge.Name != "骨灰玩家" || badge.Color != DefaultBadgeColor {
|
||||
t.Fatalf("名称未规整/默认色不符: %+v", badge)
|
||||
}
|
||||
|
||||
if err := s.Grant(badge.ID, user.ID, 9); err != nil {
|
||||
t.Fatalf("Grant: %v", err)
|
||||
}
|
||||
// 重复授予应幂等成功且不产生新行
|
||||
if err := s.Grant(badge.ID, user.ID, 9); err != nil {
|
||||
t.Fatalf("Grant twice: %v", err)
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.UserBadge{}).Where("badge_id = ?", badge.ID).Count(&count)
|
||||
if count != 1 {
|
||||
t.Fatalf("授予记录=%d want 1", count)
|
||||
}
|
||||
|
||||
// 不存在的用户
|
||||
if err := s.Grant(badge.ID, 999, 9); !errors.Is(err, ErrBadgeUserGone) {
|
||||
t.Fatalf("不存在用户应 ErrBadgeUserGone, got %v", err)
|
||||
}
|
||||
// 不存在的徽章
|
||||
if err := s.Grant(999, user.ID, 9); !errors.Is(err, ErrBadgeNotFound) {
|
||||
t.Fatalf("不存在徽章应 ErrBadgeNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// ForUser 带 Badge 定义
|
||||
rows, err := s.ForUser(user.ID)
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("ForUser: %v rows=%d", err, len(rows))
|
||||
}
|
||||
if rows[0].Badge.Name != "骨灰玩家" {
|
||||
t.Fatalf("Preload Badge 失败: %+v", rows[0])
|
||||
}
|
||||
|
||||
// Revoke 后再撤销 → NotFound
|
||||
if err := s.Revoke(badge.ID, user.ID); err != nil {
|
||||
t.Fatalf("Revoke: %v", err)
|
||||
}
|
||||
if err := s.Revoke(badge.ID, user.ID); !errors.Is(err, ErrBadgeNotFound) {
|
||||
t.Fatal("重复撤销应 ErrBadgeNotFound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadgeDeleteCascadeAndListCount(t *testing.T) {
|
||||
db := newBadgeTestDB(t)
|
||||
s := NewBadgeService(db)
|
||||
|
||||
u1 := model.User{Username: "u1", Password: "x"}
|
||||
u2 := model.User{Username: "u2", Password: "x"}
|
||||
if err := db.Create(&u1).Error; err != nil {
|
||||
t.Fatalf("seed u1: %v", err)
|
||||
}
|
||||
if err := db.Create(&u2).Error; err != nil {
|
||||
t.Fatalf("seed u2: %v", err)
|
||||
}
|
||||
b1, _ := s.Create("元老", "medal", "gold")
|
||||
b2, _ := s.Create("萌新", "", "")
|
||||
|
||||
if err := s.Grant(b1.ID, u1.ID, 9); err != nil {
|
||||
t.Fatalf("Grant b1: %v", err)
|
||||
}
|
||||
if err := s.Grant(b1.ID, u2.ID, 9); err != nil {
|
||||
t.Fatalf("Grant b1 u2: %v", err)
|
||||
}
|
||||
if err := s.Grant(b2.ID, u1.ID, 9); err != nil {
|
||||
t.Fatalf("Grant b2: %v", err)
|
||||
}
|
||||
|
||||
// List 计数
|
||||
list, err := s.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("徽章数=%d want 2", len(list))
|
||||
}
|
||||
for _, b := range list {
|
||||
want := int64(2)
|
||||
if b.ID == b2.ID {
|
||||
want = 1
|
||||
}
|
||||
if b.Holders != want {
|
||||
t.Fatalf("徽章 %d 持有数=%d want %d", b.ID, b.Holders, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除级联:徽章与其授予记录一起消失
|
||||
if err := s.Delete(b1.ID); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.UserBadge{}).Where("badge_id = ?", b1.ID).Count(&count)
|
||||
if count != 0 {
|
||||
t.Fatalf("删除后授予记录=%d want 0", count)
|
||||
}
|
||||
if err := s.Delete(b1.ID); !errors.Is(err, ErrBadgeNotFound) {
|
||||
t.Fatal("重复删除应 ErrBadgeNotFound")
|
||||
}
|
||||
|
||||
// 名称唯一冲突
|
||||
if _, err := s.Create("萌新", "", ""); !errors.Is(err, ErrBadgeNameExists) {
|
||||
t.Fatalf("重名应 ErrBadgeNameExists, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadgeValidation(t *testing.T) {
|
||||
s := NewBadgeService(newBadgeTestDB(t))
|
||||
|
||||
// 空名称 / 超长名称 / 非法颜色
|
||||
if _, err := s.Create(" ", "", ""); !errors.Is(err, ErrBadgeInvalid) {
|
||||
t.Fatal("空名称应被拒绝")
|
||||
}
|
||||
long := make([]rune, MaxBadgeNameRunes+1)
|
||||
for i := range long {
|
||||
long[i] = '字'
|
||||
}
|
||||
if _, err := s.Create(string(long), "", ""); !errors.Is(err, ErrBadgeInvalid) {
|
||||
t.Fatal("超长名称应被拒绝")
|
||||
}
|
||||
if _, err := s.Create("x", "", "pink"); !errors.Is(err, ErrBadgeInvalid) {
|
||||
t.Fatal("色板外颜色应被拒绝")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user