feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
@@ -32,7 +33,8 @@ func (s *PointsService) Balance(userID uint) (int, error) {
|
||||
return u.Points, nil
|
||||
}
|
||||
|
||||
// CreditTx 在事务内入账
|
||||
// CreditTx 在事务内入账;入账同时累加累计获得积分(经验值,等级依据)。
|
||||
// bounty_refund 是悬赏托管退回(自己托给自己的钱),不计入经验,与启动回填口径一致。
|
||||
func CreditTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||||
if delta <= 0 {
|
||||
return 0, ErrInvalidPoints
|
||||
@@ -43,7 +45,13 @@ func CreditTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID
|
||||
return 0, err
|
||||
}
|
||||
bal := u.Points + delta
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("points", bal).Error; err != nil {
|
||||
updates := map[string]interface{}{
|
||||
"points": bal,
|
||||
}
|
||||
if reason != model.PointReasonBountyRefund {
|
||||
updates["total_points"] = gorm.Expr("total_points + ?", delta)
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Create(&model.PointLedger{
|
||||
@@ -60,6 +68,31 @@ func CreditTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID
|
||||
return bal, nil
|
||||
}
|
||||
|
||||
// RewardIfConfigured 可配置奖励入账:规则分>0 且当日该 reason 正入账笔数未超 cap 时发放;
|
||||
// cap<=0 不限。计数按本地日统计 point_ledgers 同 reason 正入账笔数。
|
||||
// 并发下允许低概率超发 1-2 笔,论坛场景可接受(不做唯一索引级严格)。
|
||||
// 发放失败仅记日志不阻断主流程(奖励非关键路径)。
|
||||
func RewardIfConfigured(tx *gorm.DB, userID uint, points, cap int, reason, refType string, refID uint, note string) {
|
||||
if points <= 0 {
|
||||
return
|
||||
}
|
||||
if cap > 0 {
|
||||
var used int64
|
||||
if err := tx.Model(&model.PointLedger{}).
|
||||
Where("user_id = ? AND reason = ? AND delta > 0 AND created_at >= ?", userID, reason, localDay(time.Now())).
|
||||
Count(&used).Error; err != nil {
|
||||
log.Printf("[points] 奖励上限检查失败(忽略): user=%d reason=%s err=%v", userID, reason, err)
|
||||
return
|
||||
}
|
||||
if used >= int64(cap) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := CreditTx(tx, userID, points, reason, refType, refID, note); err != nil {
|
||||
log.Printf("[points] 奖励入账失败(忽略): user=%d reason=%s err=%v", userID, reason, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DebitTx 在事务内扣款
|
||||
func DebitTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||||
if delta <= 0 {
|
||||
@@ -291,6 +324,11 @@ func transferEnds(a, b model.PointLedger) (fromID, toID uint) {
|
||||
|
||||
// Ledger 分页查询本人积分流水
|
||||
func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64, error) {
|
||||
return ledgerPage(s.db, userID, page, size)
|
||||
}
|
||||
|
||||
// ledgerPage 积分流水分页查询(本人/管理端共用口径:id DESC,size 1..50 缺省 20)
|
||||
func ledgerPage(db *gorm.DB, userID uint, page, size int) ([]LedgerItem, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@@ -298,7 +336,7 @@ func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64
|
||||
size = 20
|
||||
}
|
||||
var total int64
|
||||
q := s.db.Model(&model.PointLedger{}).Where("user_id = ?", userID)
|
||||
q := db.Model(&model.PointLedger{}).Where("user_id = ?", userID)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -316,6 +354,33 @@ func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// AdminUserLedger 管理端查询任意用户积分流水
|
||||
func (s *PointsService) AdminUserLedger(userID uint, page, size int) ([]LedgerItem, int64, error) {
|
||||
return ledgerPage(s.db, userID, page, size)
|
||||
}
|
||||
|
||||
// AdminAdjustPoints 管理员手动调整积分:加分复用 CreditTx(同步累加累计获得→可升级),
|
||||
// 扣分复用 DebitTx(仅扣余额、不降级);reason 统一 admin_adjust,note 为调整理由。
|
||||
// 返回调整后余额、累计获得与等级。
|
||||
func (s *PointsService) AdminAdjustPoints(userID uint, delta int, note string) (bal, total, level int, err error) {
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if delta > 0 {
|
||||
bal, err = CreditTx(tx, userID, delta, model.PointReasonAdminAdjust, "admin", 0, note)
|
||||
} else {
|
||||
bal, err = DebitTx(tx, userID, -delta, model.PointReasonAdminAdjust, "admin", 0, note)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
var u model.User
|
||||
if err := s.db.Select("id", "total_points").First(&u, userID).Error; err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return bal, u.TotalPoints, model.LevelOf(u.TotalPoints), nil
|
||||
}
|
||||
|
||||
// DeltaDay 单日积分发放与消耗
|
||||
type DeltaDay struct {
|
||||
Date string `json:"date"`
|
||||
|
||||
Reference in New Issue
Block a user