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

View File

@@ -123,11 +123,22 @@ type User struct {
Banned bool `gorm:"default:false" json:"banned"`
CanManageMessages bool `gorm:"not null;default:false" json:"can_manage_messages"` // 站点消息管理(仅站长可授;站长/超管固有全站监管)
Points int `gorm:"not null;default:0" json:"points"` // 可用积分余额
TotalPoints int `gorm:"not null;default:0" json:"total_points"` // 累计获得积分(经验值,定级依据;消费不减)
Level int `gorm:"-" json:"level"` // 用户等级(AfterFind 由 TotalPoints 计算,纯展示不入库)
TokenVersion int `gorm:"default:0" json:"-"` // token 版本号,改密码/封禁时递增使旧 JWT 失效
LastSeenAt *time.Time `gorm:"index" json:"-"` // 最近活跃时间(在线统计,限频更新)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 管理员颁发的徽章;仅显式 Preload("User.Badges.Badge") 的查询点加载,未加载时省略
Badges []UserBadge `gorm:"foreignKey:UserID" json:"badges,omitempty"`
}
// AfterFind 由累计获得积分计算等级(纯算术,随行加载,所有序列化 User 的接口自动携带 level)
func (u *User) AfterFind(tx *gorm.DB) error {
u.Level = LevelOf(u.TotalPoints)
return nil
}
// RefreshToken 刷新令牌表(支持服务端撤销、一次性轮转与盗用检测)
@@ -276,6 +287,32 @@ type PointLedger struct {
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// Badge 管理员自定义徽章定义(名称/图标/颜色)
type Badge struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:32;not null;uniqueIndex" json:"name"` // ≤16 字(runes 校验见 service 层)
Icon string `gorm:"size:32;not null;default:''" json:"icon"` // lucide 图标名(前端白名单渲染,未知回落 Medal)
Color string `gorm:"size:16;not null;default:''" json:"color"` // CSS token 色板键(gold/accent/ok/danger/info/purple/teal)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserBadge 徽章授予记录(同一徽章每用户至多一枚)
type UserBadge struct {
ID uint `gorm:"primaryKey" json:"id"`
BadgeID uint `gorm:"not null;uniqueIndex:idx_badge_user" json:"badge_id"`
UserID uint `gorm:"not null;uniqueIndex:idx_badge_user;index" json:"user_id"`
Badge Badge `gorm:"foreignKey:BadgeID" json:"badge"` // 嵌套 Preload("User.Badges.Badge") 时填充
AwardedBy uint `json:"awarded_by"`
AwardedAt time.Time `json:"awarded_at"`
}
// 徽章色板键(对应前端 CSS token --<color> / --<color>-soft)
var BadgeColors = map[string]bool{
"gold": true, "accent": true, "ok": true, "danger": true,
"info": true, "purple": true, "teal": true,
}
// 积分流水原因
const (
PointReasonCheckin = "checkin"
@@ -285,6 +322,11 @@ const (
PointReasonBountyEscrow = "bounty_escrow"
PointReasonBountyRefund = "bounty_refund"
PointReasonBountyAward = "bounty_award"
PointReasonPostReward = "post_reward" // 发帖奖励(可配置,0=关闭)
PointReasonReplyReward = "reply_reward" // 回复奖励(可配置,0=关闭)
PointReasonStreakBonus = "streak_bonus" // 连续签到里程碑加成
PointReasonRecommend = "recommend_reward" // 帖子被推荐奖励(可配置,0=关闭,每帖仅一次)
PointReasonAdminAdjust = "admin_adjust" // 管理员手动调整(加分计累计可升级;扣分仅扣余额)
)
// PostPollVote 投票记录(多选时同一用户多行)
@@ -396,6 +438,7 @@ const (
NotificationTypeRejected = "rejected" // 内容审核未通过
NotificationTypeMention = "mention" // 群聊中 @ 了你(PostID=0,用 RoomID/MessageID)
NotificationTypePendingReview = "pending_review" // 有新内容待审核(发给可审者)
NotificationTypeBadge = "badge" // 获得管理员颁发的徽章
NotificationTypeDeleted = "deleted" // 帖子被管理员删除
)
@@ -414,8 +457,9 @@ type Notification struct {
CreatedAt time.Time `json:"created_at"`
Actor User `gorm:"foreignKey:ActorID" json:"actor,omitempty"`
Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"`
Room *ChatRoom `gorm:"foreignKey:RoomID;constraint:-" json:"room,omitempty"`
// PostID 允许为 0(mention/badge 等无关联帖子),禁建 FK 约束,否则 post_id=0 插入违反外键
Post Post `gorm:"foreignKey:PostID;constraint:-" json:"post,omitempty"`
Room *ChatRoom `gorm:"foreignKey:RoomID;constraint:-" json:"room,omitempty"`
}
// Announcement 站点公告(后台文章管理:支持草稿、标签与标签预设色)