fd097729d7
- 成就系统、连续打卡天数计算、管理后台成就 CRUD - 梦想目标图标预设 DreamPreset 与用户端 dream-presets 接口 - 管理后台梦想图标 CRUD;戒烟打卡 summary 修正 - 忽略根目录编译产物 /api Made-with: Cursor
188 lines
4.3 KiB
Go
188 lines
4.3 KiB
Go
package achievement
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var ErrThemeNotFound = errors.New("achievement theme not found")
|
|
|
|
type Service struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewService(db *gorm.DB) *Service {
|
|
return &Service{db: db}
|
|
}
|
|
|
|
type UserAchievement struct {
|
|
ThemeID uint `json:"theme_id"`
|
|
ThemeName string `json:"theme_name"`
|
|
ThemeKey string `json:"theme_key"`
|
|
ThemeIcon string `json:"theme_icon"`
|
|
Days int `json:"days"`
|
|
Current *Level `json:"current"`
|
|
Next *Level `json:"next,omitempty"`
|
|
Progress float64 `json:"progress"`
|
|
}
|
|
|
|
func (s *Service) ListActiveThemes(ctx context.Context) ([]Theme, error) {
|
|
var themes []Theme
|
|
err := s.db.WithContext(ctx).
|
|
Where("is_active = ?", true).
|
|
Preload("Levels", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("required_days ASC, sort_order ASC")
|
|
}).
|
|
Order("sort_order ASC, id ASC").
|
|
Find(&themes).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list active themes: %w", err)
|
|
}
|
|
return themes, nil
|
|
}
|
|
|
|
func (s *Service) GetUserAchievement(ctx context.Context, themeID uint, days int) (*UserAchievement, error) {
|
|
var theme Theme
|
|
err := s.db.WithContext(ctx).
|
|
Preload("Levels", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("required_days ASC, sort_order ASC")
|
|
}).
|
|
First(&theme, themeID).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrThemeNotFound
|
|
}
|
|
return nil, fmt.Errorf("get theme: %w", err)
|
|
}
|
|
|
|
if days < 0 {
|
|
days = 0
|
|
}
|
|
|
|
result := &UserAchievement{
|
|
ThemeID: theme.ID,
|
|
ThemeName: theme.Name,
|
|
ThemeKey: theme.Key,
|
|
ThemeIcon: theme.Icon,
|
|
Days: days,
|
|
}
|
|
|
|
for i, level := range theme.Levels {
|
|
if days >= level.RequiredDays {
|
|
lvl := level
|
|
result.Current = &lvl
|
|
if i+1 < len(theme.Levels) {
|
|
next := theme.Levels[i+1]
|
|
result.Next = &next
|
|
}
|
|
}
|
|
}
|
|
|
|
if result.Current != nil && result.Next != nil {
|
|
rangeTotal := result.Next.RequiredDays - result.Current.RequiredDays
|
|
rangeDone := days - result.Current.RequiredDays
|
|
if rangeTotal > 0 {
|
|
result.Progress = float64(rangeDone) / float64(rangeTotal)
|
|
}
|
|
} else if result.Current != nil && result.Next == nil {
|
|
result.Progress = 1.0
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) SeedDefaults(ctx context.Context) error {
|
|
var count int64
|
|
if err := s.db.WithContext(ctx).Model(&Theme{}).Count(&count).Error; err != nil {
|
|
return fmt.Errorf("count themes: %w", err)
|
|
}
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
themes := []struct {
|
|
Name string
|
|
Key string
|
|
Icon string
|
|
Levels []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}
|
|
}{
|
|
{
|
|
Name: "修仙", Key: "xiuxian", Icon: "⚔️",
|
|
Levels: []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}{
|
|
{"炼体", 0}, {"练气", 3}, {"筑基", 7}, {"金丹", 30}, {"元婴", 90}, {"化神", 365},
|
|
},
|
|
},
|
|
{
|
|
Name: "军队", Key: "army", Icon: "🎖️",
|
|
Levels: []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}{
|
|
{"新兵", 0}, {"排长", 3}, {"连长", 7}, {"营长", 30}, {"师长", 90}, {"大将", 365},
|
|
},
|
|
},
|
|
{
|
|
Name: "魔法", Key: "magic", Icon: "🔮",
|
|
Levels: []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}{
|
|
{"学徒", 0}, {"术士", 3}, {"大术士", 7}, {"元素师", 30}, {"大魔导师", 90}, {"圣导师", 365},
|
|
},
|
|
},
|
|
{
|
|
Name: "武侠", Key: "wuxia", Icon: "🗡️",
|
|
Levels: []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}{
|
|
{"徒弟", 0}, {"掌门弟子", 3}, {"门派高手", 7}, {"大侠", 30}, {"剑仙", 90}, {"武圣", 365},
|
|
},
|
|
},
|
|
{
|
|
Name: "电竞", Key: "esports", Icon: "🏆",
|
|
Levels: []struct {
|
|
Name string
|
|
RequiredDays int
|
|
}{
|
|
{"青铜", 0}, {"白银", 3}, {"黄金", 7}, {"铂金", 30}, {"钻石", 90}, {"王者", 365},
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, t := range themes {
|
|
theme := Theme{
|
|
Name: t.Name,
|
|
Key: t.Key,
|
|
Icon: t.Icon,
|
|
SortOrder: i,
|
|
IsActive: true,
|
|
}
|
|
if err := s.db.WithContext(ctx).Create(&theme).Error; err != nil {
|
|
return fmt.Errorf("seed theme %s: %w", t.Key, err)
|
|
}
|
|
for j, l := range t.Levels {
|
|
level := Level{
|
|
ThemeID: theme.ID,
|
|
Name: l.Name,
|
|
RequiredDays: l.RequiredDays,
|
|
SortOrder: j,
|
|
}
|
|
if err := s.db.WithContext(ctx).Create(&level).Error; err != nil {
|
|
return fmt.Errorf("seed level %s/%s: %w", t.Key, l.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|