spark-server/internal/module/music/model/repo.go
DESKTOP-L8OUL9F\gfl0034 d3a68c3cad feat(music): 分类多对多、后台完整编辑与 OpenAPI
新增总/子分类与曲目绑定;playlist 可按 categoryId 筛选并返回分类树;
popadmin 支持行内改曲名/排序/上下架/会员开关与封面背景音频上传;文档同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 10:38:06 +08:00

489 lines
13 KiB
Go

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 _, row := range rows {
out = append(out, toEntity(row))
}
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 _, row := range rows {
out = append(out, toEntity(row))
}
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 _, row := range rows {
out = append(out, toEntity(row))
}
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 _, c := range all {
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 _, row := range rows {
out = append(out, toCategoryEntity(row))
}
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 _, row := range rows {
out = append(out, toCategoryEntity(row))
}
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 _, t := range tracks {
ids = append(ids, t.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 _, l := range links {
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,
}
}