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

@@ -23,12 +23,19 @@ var (
// CommentService 评论服务
type CommentService struct {
db *gorm.DB
setting *SettingService
}
func NewCommentService(db *gorm.DB) *CommentService {
return &CommentService{db: db}
}
// WithSetting 注入站点设置(回复奖励规则;未注入时奖励关闭)
func (s *CommentService) WithSetting(setting *SettingService) *CommentService {
s.setting = setting
return s
}
// CommentNode 评论树节点:嵌入 model.Comment 使 JSON 字段拍平,replies 为空时省略
type CommentNode struct {
model.Comment
@@ -84,7 +91,7 @@ func (s *CommentService) ListFloorPaged(postID, boardID, viewerID uint, actor *A
if err := base.Session(&gorm.Session{}).
Where("parent_id IS NULL").
Order("created_at ASC").
Preload("User").
Preload("User").Preload("User.Badges.Badge").
Offset((page - 1) * size).
Limit(size).
Find(&roots).Error; err != nil {
@@ -100,7 +107,7 @@ func (s *CommentService) ListFloorPaged(postID, boardID, viewerID uint, actor *A
}
replyQ := applyCommentListVisibility(s.db.Unscoped().Where("root_id IN (?)", rootIDs), boardID, viewerID, actor)
var replies []model.Comment
if err := replyQ.Order("created_at ASC").Preload("User").Find(&replies).Error; err != nil {
if err := replyQ.Order("created_at ASC").Preload("User").Preload("User.Badges.Badge").Find(&replies).Error; err != nil {
return nil, 0, 0, err
}
@@ -355,9 +362,19 @@ func (s *CommentService) Create(userID, postID uint, content string, parentID *u
// 仅已发布评论立即计入评论数;待审评论通过审核时才 +1
if comment.Status == model.ContentStatusPublished {
s.db.Model(&post).UpdateColumn("comment_count", gorm.Expr("comment_count + 1"))
// 回复奖励(可配置;待审不发且过审不补发,防拒审刷分)
if s.setting != nil {
if rules, err := s.setting.PointsRules(); err == nil {
s.db.Transaction(func(tx *gorm.DB) error {
RewardIfConfigured(tx, userID, rules.ReplyReward, rules.ReplyDailyCap,
model.PointReasonReplyReward, "comment", comment.ID, "回复奖励:"+post.Title)
return nil
})
}
}
}
// 预加载用户
s.db.Preload("User").First(comment, comment.ID)
s.db.Preload("User").Preload("User.Badges.Badge").First(comment, comment.ID)
return comment, parent, nil
}
@@ -648,7 +665,7 @@ func (s *CommentService) Update(actor *Actor, commentID, userID uint, content st
return nil, err
}
if err := s.db.Preload("User").First(&comment, comment.ID).Error; err != nil {
if err := s.db.Preload("User").Preload("User.Badges.Badge").First(&comment, comment.ID).Error; err != nil {
return nil, err
}
node := toCommentNode(comment, nil, false, userID, nil)