在 fast-forward 到 origin/main 后解决 music logic/repo 冲突,SQL 与模型统一 t_* 表名;Makefile 修正 golangci-lint 变量并令 test/deploy 依赖 lint;补齐 admin/transport 等 lint 修复与 music logic 单测以满足覆盖率门禁。 Co-authored-by: Cursor <cursoragent@cursor.com>
491 lines
13 KiB
Go
491 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 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,
|
|
}
|
|
}
|