Files
jiang13-forum/service/badge.go
freefire 6b0a1d4281 增加用户认证、等级、徽章与积分体系,并优化管理后台体验。
覆盖站长调账与积分解锁内容;后台按审核优先分组导航,仪表盘展示待办,用户管理改为成员目录式布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 02:42:18 +08:00

273 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"errors"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"gorm.io/gorm"
)
// BadgeService 徽章定义与发放
type BadgeService struct{}
func NewBadgeService() *BadgeService { return &BadgeService{} }
// ListDefs 列出徽章定义
func (s *BadgeService) ListDefs(includeDisabled bool) ([]model.BadgeDef, error) {
q := model.DB.Order("sort_order asc, id asc")
if !includeDisabled {
q = q.Where("enabled = ?", true)
}
var rows []model.BadgeDef
err := q.Find(&rows).Error
return rows, err
}
// UpsertDef 创建或更新徽章定义(按 code
func (s *BadgeService) UpsertDef(def *model.BadgeDef) error {
if def.Code == "" || def.Name == "" {
return errors.New("徽章代码与名称不能为空")
}
if def.Kind != model.BadgeKindAuto && def.Kind != model.BadgeKindLimited {
return errors.New("无效的徽章类型")
}
var existing model.BadgeDef
err := model.DB.Where("code = ?", def.Code).Limit(1).Find(&existing).Error
if err != nil {
return err
}
if existing.ID == 0 {
return model.DB.Create(def).Error
}
def.ID = existing.ID
return model.DB.Model(&existing).Updates(map[string]interface{}{
"name": def.Name,
"description": def.Description,
"icon": def.Icon,
"kind": def.Kind,
"metric": def.Metric,
"threshold": def.Threshold,
"sort_order": def.SortOrder,
"enabled": def.Enabled,
}).Error
}
// AwardLimited 站长颁发限定徽章
func (s *BadgeService) AwardLimited(userID, badgeID, adminID uint) error {
var def model.BadgeDef
if err := model.DB.First(&def, badgeID).Error; err != nil {
return errors.New("徽章不存在")
}
if def.Kind != model.BadgeKindLimited {
return errors.New("仅可颁发限定徽章")
}
if !def.Enabled {
return errors.New("徽章已停用")
}
var n int64
model.DB.Model(&model.UserBadge{}).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&n)
if n > 0 {
return errors.New("用户已拥有该徽章")
}
return model.DB.Create(&model.UserBadge{
UserID: userID,
BadgeID: badgeID,
AwardedAt: time.Now(),
AwardedBy: adminID,
}).Error
}
// Revoke 收回徽章
func (s *BadgeService) Revoke(userID, badgeID uint) error {
res := model.DB.Where("user_id = ? AND badge_id = ?", userID, badgeID).Delete(&model.UserBadge{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("用户未拥有该徽章")
}
return nil
}
// ListUserBadges 用户已获徽章(含定义)
func (s *BadgeService) ListUserBadges(userID uint) ([]model.UserBadge, error) {
var rows []model.UserBadge
err := model.DB.Preload("Badge").Where("user_id = ?", userID).
Order("awarded_at desc").Find(&rows).Error
return rows, err
}
// BadgeViews 转为展示视图(最多 limit 枚0=全部)
func BadgeViews(rows []model.UserBadge, limit int) []model.UserBadgeView {
out := make([]model.UserBadgeView, 0, len(rows))
for _, r := range rows {
if r.Badge.ID == 0 || !r.Badge.Enabled {
continue
}
out = append(out, model.UserBadgeView{
Code: r.Badge.Code,
Name: r.Badge.Name,
Description: r.Badge.Description,
Icon: r.Badge.Icon,
Kind: r.Badge.Kind,
})
if limit > 0 && len(out) >= limit {
break
}
}
return out
}
// EvaluateAuto 检查并授予符合条件的自动徽章
func (s *BadgeService) EvaluateAuto(userID uint) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
return err
}
var defs []model.BadgeDef
if err := model.DB.Where("kind = ? AND enabled = ?", model.BadgeKindAuto, true).Find(&defs).Error; err != nil {
return err
}
tenureDays := int(time.Since(user.CreatedAt).Hours() / 24)
var likes int64
_ = model.DB.Model(&model.Post{}).
Select("COALESCE(SUM(like_count), 0)").
Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished).
Scan(&likes).Error
income := user.CreatorIncomeTotal
owned := map[uint]bool{}
var existing []model.UserBadge
_ = model.DB.Where("user_id = ?", userID).Find(&existing).Error
for _, e := range existing {
owned[e.BadgeID] = true
}
for _, d := range defs {
if owned[d.ID] {
continue
}
ok := false
switch d.Metric {
case model.BadgeMetricTenureDays:
ok = tenureDays >= d.Threshold
case model.BadgeMetricLikesReceived:
ok = int(likes) >= d.Threshold
case model.BadgeMetricCreatorIncome:
ok = income >= d.Threshold
}
if !ok {
continue
}
_ = model.DB.Create(&model.UserBadge{
UserID: userID,
BadgeID: d.ID,
AwardedAt: time.Now(),
AwardedBy: 0,
}).Error
}
return nil
}
// AttachBadgeSummaries 批量为用户填充展示用徽章(最多 perUser 枚)
func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
if len(users) == 0 {
return
}
if perUser <= 0 {
perUser = 3
}
ids := make([]uint, 0, len(users))
seen := map[uint]bool{}
for _, u := range users {
if u == nil || u.ID == 0 {
continue
}
u.Level = model.LevelFromExp(u.Exp)
if !seen[u.ID] {
seen[u.ID] = true
ids = append(ids, u.ID)
}
}
if len(ids) == 0 {
return
}
var rows []model.UserBadge
_ = model.DB.Preload("Badge").Where("user_id IN ?", ids).
Order("awarded_at desc").Find(&rows).Error
grouped := map[uint][]model.UserBadgeView{}
for _, r := range rows {
if r.Badge.ID == 0 || !r.Badge.Enabled {
continue
}
list := grouped[r.UserID]
if len(list) >= perUser {
continue
}
list = append(list, model.UserBadgeView{
Code: r.Badge.Code,
Name: r.Badge.Name,
Description: r.Badge.Description,
Icon: r.Badge.Icon,
Kind: r.Badge.Kind,
})
grouped[r.UserID] = list
}
for _, u := range users {
if u == nil || u.ID == 0 {
continue
}
u.Badges = grouped[u.ID]
}
}
// AttachBadgeSummariesOnPosts 给帖子作者填充徽章摘要
func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []model.Post, perUser int) {
users := make([]*model.User, 0, len(posts))
for i := range posts {
if posts[i].User.ID > 0 {
users = append(users, &posts[i].User)
}
}
s.AttachBadgeSummaries(users, perUser)
}
// AttachBadgeSummariesOnComments 给评论作者填充徽章摘要
func (s *BadgeService) AttachBadgeSummariesOnComments(comments []model.Comment, perUser int) {
users := make([]*model.User, 0, len(comments))
for i := range comments {
if comments[i].User.ID > 0 {
users = append(users, &comments[i].User)
}
}
s.AttachBadgeSummaries(users, perUser)
}
// AddExp 增加经验不可为负消耗delta<=0 忽略)
func AddExp(userID uint, delta int) {
if userID == 0 || delta <= 0 {
return
}
_ = model.DB.Model(&model.User{}).Where("id = ?", userID).
UpdateColumn("exp", gorm.Expr("exp + ?", delta)).Error
}
// SetUserLevel 站长设等级(调整 Exp 到门槛)
func SetUserLevel(userID uint, level int) error {
if level < 1 || level > model.MaxLevel() {
return errors.New("等级须在 110")
}
exp := model.ExpForLevel(level)
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("exp", exp).Error
}
// SetVerified 设置认证
func SetVerified(userID uint, verified bool) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
return model.DB.Model(&user).Update("verified", verified).Error
}