Files
nepiedg fd097729d7 feat: 戒烟成就、梦想图标预设、打卡统计与依赖注入调整
- 成就系统、连续打卡天数计算、管理后台成就 CRUD
- 梦想目标图标预设 DreamPreset 与用户端 dream-presets 接口
- 管理后台梦想图标 CRUD;戒烟打卡 summary 修正
- 忽略根目录编译产物 /api

Made-with: Cursor
2026-04-04 14:55:50 +08:00

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
}