feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
@@ -2,13 +2,15 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DailyCheckinPoints 每日签到固定积分
|
||||
// DailyCheckinPoints 每日签到默认基础分(可被设置 points_checkin_base 覆盖)
|
||||
const DailyCheckinPoints = 5
|
||||
|
||||
// maxStreakScan 连续天数最多向前回溯的天数
|
||||
@@ -22,18 +24,38 @@ type CheckinStatus struct {
|
||||
CheckedToday bool `json:"checked_today"`
|
||||
Streak int `json:"streak"` // 连续签到天数
|
||||
TotalPoints int `json:"total_points"` // 累计积分(可用余额)
|
||||
TodayPoints int `json:"today_points"` // 今日签到可得积分
|
||||
TodayPoints int `json:"today_points"` // 今日签到可得基础积分(连续加成另行入账,流水可见)
|
||||
}
|
||||
|
||||
// CheckinService 每日签到
|
||||
type CheckinService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
setting *SettingService
|
||||
}
|
||||
|
||||
func NewCheckinService(db *gorm.DB) *CheckinService {
|
||||
return &CheckinService{db: db}
|
||||
}
|
||||
|
||||
// WithSetting 注入站点设置(积分增长规则;未注入时用默认值)
|
||||
func (s *CheckinService) WithSetting(setting *SettingService) *CheckinService {
|
||||
s.setting = setting
|
||||
return s
|
||||
}
|
||||
|
||||
// rules 读取积分规则;未注入或读取失败回落默认值(签到可用性优先于规则精度)
|
||||
func (s *CheckinService) rules() PointsRules {
|
||||
if s.setting == nil {
|
||||
return PointsRules{CheckinBase: DefaultPointsCheckinBase, StreakEveryDays: DefaultPointsStreakEveryDays}
|
||||
}
|
||||
rules, err := s.setting.PointsRules()
|
||||
if err != nil {
|
||||
log.Printf("[checkin] 读取积分规则失败,使用默认值: %v", err)
|
||||
return PointsRules{CheckinBase: DefaultPointsCheckinBase, StreakEveryDays: DefaultPointsStreakEveryDays}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// localDay 取本地时区零点日期
|
||||
func localDay(t time.Time) time.Time {
|
||||
y, m, d := t.Date()
|
||||
@@ -53,9 +75,25 @@ func streakFrom(dates map[string]bool, anchor time.Time) int {
|
||||
return streak
|
||||
}
|
||||
|
||||
// streakDatesInTx 从事务连接查询 anchor 起回溯窗口内的签到日期集合
|
||||
func streakDatesInTx(tx *gorm.DB, userID uint, anchor time.Time) (map[string]bool, error) {
|
||||
var dates []time.Time
|
||||
if err := tx.Model(&model.Checkin{}).
|
||||
Where("user_id = ? AND checkin_date >= ?", userID, anchor.AddDate(0, 0, -maxStreakScan)).
|
||||
Pluck("checkin_date", &dates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dateSet := make(map[string]bool, len(dates))
|
||||
for _, d := range dates {
|
||||
dateSet[localDay(d).Format("2006-01-02")] = true
|
||||
}
|
||||
return dateSet, nil
|
||||
}
|
||||
|
||||
// Status 查询用户签到状态
|
||||
func (s *CheckinService) Status(userID uint) (*CheckinStatus, error) {
|
||||
today := localDay(time.Now())
|
||||
base := s.rules().CheckinBase
|
||||
|
||||
var todayRow model.Checkin
|
||||
err := s.db.Where("user_id = ? AND checkin_date::date = ?::date", userID, today).
|
||||
@@ -73,16 +111,10 @@ func (s *CheckinService) Status(userID uint) (*CheckinStatus, error) {
|
||||
if !checkedToday {
|
||||
anchor = today.AddDate(0, 0, -1)
|
||||
}
|
||||
var dates []time.Time
|
||||
if err := s.db.Model(&model.Checkin{}).
|
||||
Where("user_id = ? AND checkin_date >= ?", userID, anchor.AddDate(0, 0, -maxStreakScan)).
|
||||
Pluck("checkin_date", &dates).Error; err != nil {
|
||||
dateSet, err := streakDatesInTx(s.db, userID, anchor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dateSet := make(map[string]bool, len(dates))
|
||||
for _, d := range dates {
|
||||
dateSet[localDay(d).Format("2006-01-02")] = true
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := s.db.Model(&model.User{}).
|
||||
@@ -95,13 +127,15 @@ func (s *CheckinService) Status(userID uint) (*CheckinStatus, error) {
|
||||
CheckedToday: checkedToday,
|
||||
Streak: streakFrom(dateSet, anchor),
|
||||
TotalPoints: int(total),
|
||||
TodayPoints: DailyCheckinPoints,
|
||||
TodayPoints: base,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckIn 执行今日签到(每日一条,唯一索引兜底并发)
|
||||
// CheckIn 执行今日签到(每日一条,唯一索引兜底并发)。
|
||||
// 基础分与连续签到里程碑加成在同一事务内入账(加成规则可配置:每满 N 天额外 +X)。
|
||||
func (s *CheckinService) CheckIn(userID uint) (*CheckinStatus, error) {
|
||||
today := localDay(time.Now())
|
||||
rules := s.rules()
|
||||
|
||||
var status *CheckinStatus
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -117,15 +151,28 @@ func (s *CheckinService) CheckIn(userID uint) (*CheckinStatus, error) {
|
||||
row := model.Checkin{
|
||||
UserID: userID,
|
||||
CheckinDate: today,
|
||||
Points: DailyCheckinPoints,
|
||||
Points: rules.CheckinBase,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 同步入账到用户积分余额
|
||||
if _, err := CreditTx(tx, userID, DailyCheckinPoints, model.PointReasonCheckin, "checkin", row.ID, "每日签到"); err != nil {
|
||||
if _, err := CreditTx(tx, userID, rules.CheckinBase, model.PointReasonCheckin, "checkin", row.ID, "每日签到"); err != nil {
|
||||
return err
|
||||
}
|
||||
// 连续签到里程碑加成(每满 N 天额外 +X;N=0 或加成=0 时关闭)
|
||||
if rules.StreakEveryDays > 0 && rules.StreakBonus > 0 {
|
||||
dateSet, err := streakDatesInTx(tx, userID, today)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if streak := streakFrom(dateSet, today); streak > 0 && streak%rules.StreakEveryDays == 0 {
|
||||
if _, err := CreditTx(tx, userID, rules.StreakBonus, model.PointReasonStreakBonus, "checkin", row.ID,
|
||||
"连续签到 "+strconv.Itoa(streak)+" 天奖励"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user