package model import ( "context" "errors" "strconv" "time" "gorm.io/gorm" "spark-server/internal/module/music/entity" ) type Repo struct { db *gorm.DB } func NewRepo(db *gorm.DB) *Repo { if db == nil { return nil } return &Repo{db: db} } func (r *Repo) Ready() bool { return r != nil && r.db != nil } type trackRow struct { ID int64 `gorm:"column:id;primaryKey"` Title string `gorm:"column:title"` CoverKey string `gorm:"column:cover_key"` AudioKey string `gorm:"column:audio_key"` BackgroundKey string `gorm:"column:background_key"` CoverURL string `gorm:"column:cover_url"` AudioURL string `gorm:"column:audio_url"` BackgroundURL string `gorm:"column:background_url"` RequireMember bool `gorm:"column:require_member"` SortOrder int `gorm:"column:sort_order"` Enabled bool `gorm:"column:enabled"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (trackRow) TableName() string { return "t_music_tracks" } type settingRow struct { SettingKey string `gorm:"column:setting_key;primaryKey"` SettingValue string `gorm:"column:setting_value"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (settingRow) TableName() string { return "t_music_settings" } type categoryRow struct { ID int64 `gorm:"column:id;primaryKey"` ParentID *int64 `gorm:"column:parent_id"` Name string `gorm:"column:name"` SortOrder int `gorm:"column:sort_order"` Enabled bool `gorm:"column:enabled"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (categoryRow) TableName() string { return "t_music_categories" } type categoryTrackRow struct { CategoryID int64 `gorm:"column:category_id;primaryKey"` TrackID int64 `gorm:"column:track_id;primaryKey"` CreatedAt time.Time `gorm:"column:created_at"` } func (categoryTrackRow) TableName() string { return "t_music_category_tracks" } func (r *Repo) ListAll(ctx context.Context) ([]entity.Track, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var rows []trackRow err := r.db.WithContext(ctx).Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } out := make([]entity.Track, 0, len(rows)) for i := range rows { out = append(out, toEntity(&rows[i])) } return r.attachCategoryIDs(ctx, out) } func (r *Repo) ListEnabled(ctx context.Context) ([]entity.Track, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var rows []trackRow err := r.db.WithContext(ctx).Where("enabled = ?", true).Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } out := make([]entity.Track, 0, len(rows)) for i := range rows { out = append(out, toEntity(&rows[i])) } return r.attachCategoryIDs(ctx, out) } func (r *Repo) ListEnabledByCategory(ctx context.Context, categoryID int64) ([]entity.Track, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } ids, err := r.categoryScopeIDs(ctx, categoryID) if err != nil { return nil, err } if len(ids) == 0 { return []entity.Track{}, nil } var trackIDs []int64 err = r.db.WithContext(ctx).Model(&categoryTrackRow{}). Where("category_id IN ?", ids).Distinct("track_id").Pluck("track_id", &trackIDs).Error if err != nil { return nil, err } if len(trackIDs) == 0 { return []entity.Track{}, nil } var rows []trackRow err = r.db.WithContext(ctx).Where("enabled = ? AND id IN ?", true, trackIDs). Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } out := make([]entity.Track, 0, len(rows)) for i := range rows { out = append(out, toEntity(&rows[i])) } return r.attachCategoryIDs(ctx, out) } func (r *Repo) categoryScopeIDs(ctx context.Context, categoryID int64) ([]int64, error) { var all []categoryRow if err := r.db.WithContext(ctx).Find(&all).Error; err != nil { return nil, err } byParent := map[int64][]int64{} for i := range all { c := &all[i] if c.ParentID != nil { byParent[*c.ParentID] = append(byParent[*c.ParentID], c.ID) } } out := []int64{categoryID} queue := []int64{categoryID} for len(queue) > 0 { cur := queue[0] queue = queue[1:] for _, child := range byParent[cur] { out = append(out, child) queue = append(queue, child) } } return out, nil } func (r *Repo) Get(ctx context.Context, id int64) (*entity.Track, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var row trackRow err := r.db.WithContext(ctx).First(&row, id).Error if err != nil { return nil, err } t := toEntity(&row) ids, err := r.TrackCategoryIDs(ctx, id) if err != nil { return nil, err } t.CategoryIDs = ids return &t, nil } func (r *Repo) Insert(ctx context.Context, t *entity.Track) error { if !r.Ready() { return errors.New("music repo not ready") } now := time.Now().UTC() row := trackRow{ Title: t.Title, CoverKey: t.CoverKey, AudioKey: t.AudioKey, BackgroundKey: t.BackgroundKey, CoverURL: t.CoverURL, AudioURL: t.AudioURL, BackgroundURL: t.BackgroundURL, RequireMember: t.RequireMember, SortOrder: t.SortOrder, Enabled: t.Enabled, CreatedAt: now, UpdatedAt: now, } if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { return err } t.ID = row.ID t.CreatedAt = row.CreatedAt t.UpdatedAt = row.UpdatedAt if t.CategoryIDs == nil { t.CategoryIDs = []int64{} } return nil } func (r *Repo) Update(ctx context.Context, t *entity.Track) error { if !r.Ready() { return errors.New("music repo not ready") } now := time.Now().UTC() t.UpdatedAt = now return r.db.WithContext(ctx).Model(&trackRow{}).Where("id = ?", t.ID).Updates(map[string]any{ "title": t.Title, "cover_key": t.CoverKey, "audio_key": t.AudioKey, "background_key": t.BackgroundKey, "cover_url": t.CoverURL, "audio_url": t.AudioURL, "background_url": t.BackgroundURL, "require_member": t.RequireMember, "sort_order": t.SortOrder, "enabled": t.Enabled, "updated_at": now, }).Error } func (r *Repo) Delete(ctx context.Context, id int64) error { if !r.Ready() { return errors.New("music repo not ready") } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("track_id = ?", id).Delete(&categoryTrackRow{}).Error; err != nil { return err } return tx.Delete(&trackRow{}, id).Error }) } func (r *Repo) GetSetting(ctx context.Context, key string) (string, error) { if !r.Ready() { return "", errors.New("music repo not ready") } var row settingRow err := r.db.WithContext(ctx).Where("setting_key = ?", key).First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil } if err != nil { return "", err } return row.SettingValue, nil } func (r *Repo) SetSetting(ctx context.Context, key, value string) error { if !r.Ready() { return errors.New("music repo not ready") } now := time.Now().UTC() row := settingRow{SettingKey: key, SettingValue: value, UpdatedAt: now} return r.db.WithContext(ctx).Save(&row).Error } func (r *Repo) MemberPreviewSeconds(ctx context.Context) (int, error) { raw, err := r.GetSetting(ctx, entity.SettingMemberPreviewSeconds) if err != nil { return entity.DefaultPreviewSeconds, err } if raw == "" { return entity.DefaultPreviewSeconds, nil } n, err := strconv.Atoi(raw) if err != nil || n < 0 { return entity.DefaultPreviewSeconds, nil } return n, nil } func (r *Repo) ListCategories(ctx context.Context) ([]entity.Category, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var rows []categoryRow err := r.db.WithContext(ctx).Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } out := make([]entity.Category, 0, len(rows)) for i := range rows { out = append(out, toCategoryEntity(&rows[i])) } return out, nil } func (r *Repo) ListEnabledCategories(ctx context.Context) ([]entity.Category, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var rows []categoryRow err := r.db.WithContext(ctx).Where("enabled = ?", true).Order("sort_order ASC, id ASC").Find(&rows).Error if err != nil { return nil, err } out := make([]entity.Category, 0, len(rows)) for i := range rows { out = append(out, toCategoryEntity(&rows[i])) } return out, nil } func (r *Repo) GetCategory(ctx context.Context, id int64) (*entity.Category, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var row categoryRow err := r.db.WithContext(ctx).First(&row, id).Error if err != nil { return nil, err } c := toCategoryEntity(&row) return &c, nil } func (r *Repo) InsertCategory(ctx context.Context, c *entity.Category) error { if !r.Ready() { return errors.New("music repo not ready") } now := time.Now().UTC() row := categoryRow{ ParentID: c.ParentID, Name: c.Name, SortOrder: c.SortOrder, Enabled: c.Enabled, CreatedAt: now, UpdatedAt: now, } if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { return err } c.ID = row.ID c.CreatedAt = row.CreatedAt c.UpdatedAt = row.UpdatedAt return nil } func (r *Repo) UpdateCategory(ctx context.Context, c *entity.Category) error { if !r.Ready() { return errors.New("music repo not ready") } now := time.Now().UTC() c.UpdatedAt = now return r.db.WithContext(ctx).Model(&categoryRow{}).Where("id = ?", c.ID).Updates(map[string]any{ "parent_id": c.ParentID, "name": c.Name, "sort_order": c.SortOrder, "enabled": c.Enabled, "updated_at": now, }).Error } func (r *Repo) DeleteCategory(ctx context.Context, id int64) error { if !r.Ready() { return errors.New("music repo not ready") } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var childIDs []int64 if err := tx.Model(&categoryRow{}).Where("parent_id = ?", id).Pluck("id", &childIDs).Error; err != nil { return err } all := append([]int64{id}, childIDs...) if err := tx.Where("category_id IN ?", all).Delete(&categoryTrackRow{}).Error; err != nil { return err } if len(childIDs) > 0 { if err := tx.Where("id IN ?", childIDs).Delete(&categoryRow{}).Error; err != nil { return err } } return tx.Delete(&categoryRow{}, id).Error }) } func (r *Repo) TrackCategoryIDs(ctx context.Context, trackID int64) ([]int64, error) { if !r.Ready() { return nil, errors.New("music repo not ready") } var ids []int64 err := r.db.WithContext(ctx).Model(&categoryTrackRow{}). Where("track_id = ?", trackID).Order("category_id ASC").Pluck("category_id", &ids).Error if err != nil { return nil, err } if ids == nil { ids = []int64{} } return ids, nil } func (r *Repo) SetTrackCategories(ctx context.Context, trackID int64, categoryIDs []int64) error { if !r.Ready() { return errors.New("music repo not ready") } return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("track_id = ?", trackID).Delete(&categoryTrackRow{}).Error; err != nil { return err } now := time.Now().UTC() seen := map[int64]struct{}{} for _, cid := range categoryIDs { if cid <= 0 { continue } if _, ok := seen[cid]; ok { continue } seen[cid] = struct{}{} row := categoryTrackRow{CategoryID: cid, TrackID: trackID, CreatedAt: now} if err := tx.Create(&row).Error; err != nil { return err } } return nil }) } func (r *Repo) attachCategoryIDs(ctx context.Context, tracks []entity.Track) ([]entity.Track, error) { if len(tracks) == 0 { return tracks, nil } ids := make([]int64, 0, len(tracks)) for i := range tracks { ids = append(ids, tracks[i].ID) } var links []categoryTrackRow err := r.db.WithContext(ctx).Where("track_id IN ?", ids).Order("category_id ASC").Find(&links).Error if err != nil { return nil, err } m := map[int64][]int64{} for i := range links { l := &links[i] m[l.TrackID] = append(m[l.TrackID], l.CategoryID) } for i := range tracks { cats := m[tracks[i].ID] if cats == nil { cats = []int64{} } tracks[i].CategoryIDs = cats } return tracks, nil } func toEntity(row *trackRow) entity.Track { return entity.Track{ ID: row.ID, Title: row.Title, CoverKey: row.CoverKey, AudioKey: row.AudioKey, BackgroundKey: row.BackgroundKey, CoverURL: row.CoverURL, AudioURL: row.AudioURL, BackgroundURL: row.BackgroundURL, RequireMember: row.RequireMember, SortOrder: row.SortOrder, Enabled: row.Enabled, CategoryIDs: []int64{}, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func toCategoryEntity(row *categoryRow) entity.Category { return entity.Category{ ID: row.ID, ParentID: row.ParentID, Name: row.Name, SortOrder: row.SortOrder, Enabled: row.Enabled, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } }