feat: 戒烟成就、梦想图标预设、打卡统计与依赖注入调整
- 成就系统、连续打卡天数计算、管理后台成就 CRUD - 梦想目标图标预设 DreamPreset 与用户端 dream-presets 接口 - 管理后台梦想图标 CRUD;戒烟打卡 summary 修正 - 忽略根目录编译产物 /api Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package achievement
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Theme struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Name string `gorm:"size:50;not null;comment:主题名称" json:"name"`
|
||||
Key string `gorm:"size:50;uniqueIndex;not null;comment:主题标识" json:"key"`
|
||||
Icon string `gorm:"size:255;comment:主题图标" json:"icon"`
|
||||
SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"`
|
||||
IsActive bool `gorm:"default:true;comment:是否启用" json:"is_active"`
|
||||
|
||||
Levels []Level `gorm:"foreignKey:ThemeID" json:"levels,omitempty"`
|
||||
}
|
||||
|
||||
func (Theme) TableName() string {
|
||||
return "fa_achievement_theme"
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
ThemeID uint `gorm:"index;not null;comment:主题ID" json:"theme_id"`
|
||||
Name string `gorm:"size:50;not null;comment:等级名称" json:"name"`
|
||||
Icon string `gorm:"size:255;comment:等级图标" json:"icon"`
|
||||
RequiredDays int `gorm:"not null;default:0;comment:所需打卡天数" json:"required_days"`
|
||||
SortOrder int `gorm:"default:0;comment:排序" json:"sort_order"`
|
||||
}
|
||||
|
||||
func (Level) TableName() string {
|
||||
return "fa_achievement_level"
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user