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 }