feat(music): 曲目增加背景图字段与管理端上传
新增 migration 0021(background_key/url)、playlist/CRUD 返回 backgroundUrl、
POST .../music/{id}/background,并热更 popadmin 音乐列表背景上传;公读策略放行 perm/music/*。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0a54e9b2ef
commit
ade98eef4d
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,3 +11,7 @@ coverage.out
|
||||
.env
|
||||
certs/*.key
|
||||
server
|
||||
|
||||
_release/
|
||||
*.pem
|
||||
|
||||
|
||||
@ -57,9 +57,11 @@ jwt:
|
||||
denylist_prefix: "denylist:"
|
||||
refresh_prefix: "refresh:"
|
||||
|
||||
# Must match Aliyun wyrs.admin.jwt-secret / WYRS_ADMIN_JWT_SECRET (popadmin login).
|
||||
# Must match Java WYRS_ADMIN_JWT_SECRET (popadmin login).
|
||||
# Prod validate REJECTS the jar default "wyrs-admin-jwt-secret-change-in-production-2025".
|
||||
# Prefer setting the same value via SPARK_ADMIN_JWT_SECRET and /etc/wyrs-admin/app.env.
|
||||
admin:
|
||||
jwt_secret: "wyrs-admin-jwt-secret-change-in-production"
|
||||
jwt_secret: "replace-with-shared-WYRS_ADMIN_JWT_SECRET"
|
||||
|
||||
taskpool:
|
||||
size: 256
|
||||
|
||||
90
deploy/popadmin/patch_music_bg.py
Normal file
90
deploy/popadmin/patch_music_bg.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Patch popadmin.js music() UI to support background image upload/display."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
p = Path(__file__).with_name("popadmin.js")
|
||||
c = p.read_text(encoding="utf-8")
|
||||
|
||||
replacements = [
|
||||
(
|
||||
"· 封面/mp3 存 sparkr",
|
||||
"· 封面/背景图/mp3 存 sparkr",
|
||||
),
|
||||
(
|
||||
"<thead><tr><th>封面</th><th>ID</th><th>曲名</th><th>会员</th><th>上架</th><th>排序</th><th>音频</th><th>操作</th></tr></thead>",
|
||||
"<thead><tr><th>封面</th><th>背景</th><th>ID</th><th>曲名</th><th>会员</th><th>上架</th><th>排序</th><th>音频</th><th>操作</th></tr></thead>",
|
||||
),
|
||||
(
|
||||
"${t.coverUrl ? `<img src=\"${esc(t.coverUrl)}\" alt=\"\" style=\"width:48px;height:48px;object-fit:cover;border-radius:6px\">` : '—'}</td>\n"
|
||||
" <td>${esc(t.id)}</td>",
|
||||
"${t.coverUrl ? `<img src=\"${esc(t.coverUrl)}\" alt=\"\" style=\"width:48px;height:48px;object-fit:cover;border-radius:6px\">` : '—'}</td>\n"
|
||||
" <td>${t.backgroundUrl ? `<img src=\"${esc(t.backgroundUrl)}\" alt=\"\" style=\"width:48px;height:48px;object-fit:cover;border-radius:6px\">` : '—'}</td>\n"
|
||||
" <td>${esc(t.id)}</td>",
|
||||
),
|
||||
(
|
||||
'<label class="secondary btn-sm" style="display:inline-block;cursor:pointer;padding:4px 8px">封面<input type="file" accept="image/*" data-mcover="${esc(t.id)}" hidden></label>\n'
|
||||
' <label class="secondary btn-sm" style="display:inline-block;cursor:pointer;padding:4px 8px">mp3<input type="file" accept="audio/mpeg,.mp3" data-maudio="${esc(t.id)}" hidden></label>',
|
||||
'<label class="secondary btn-sm" style="display:inline-block;cursor:pointer;padding:4px 8px">封面<input type="file" accept="image/*" data-mcover="${esc(t.id)}" hidden></label>\n'
|
||||
' <label class="secondary btn-sm" style="display:inline-block;cursor:pointer;padding:4px 8px">背景<input type="file" accept="image/*" data-mbg="${esc(t.id)}" hidden></label>\n'
|
||||
' <label class="secondary btn-sm" style="display:inline-block;cursor:pointer;padding:4px 8px">mp3<input type="file" accept="audio/mpeg,.mp3" data-maudio="${esc(t.id)}" hidden></label>',
|
||||
),
|
||||
(
|
||||
": '<tr><td colspan=\"8\">暂无曲目</td></tr>'",
|
||||
": '<tr><td colspan=\"9\">暂无曲目</td></tr>'",
|
||||
),
|
||||
]
|
||||
|
||||
for old, new in replacements:
|
||||
if old not in c:
|
||||
raise SystemExit(f"pattern not found:\n{old[:120]!r}")
|
||||
if old.count(old) != 1 and c.count(old) != 1:
|
||||
raise SystemExit(f"pattern not unique ({c.count(old)}):\n{old[:80]!r}")
|
||||
c = c.replace(old, new, 1)
|
||||
|
||||
handler_old = """ $('content').querySelectorAll('[data-mcover]').forEach((inp) => {
|
||||
inp.onchange = async () => {
|
||||
if (!inp.files?.[0]) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', inp.files[0]);
|
||||
await apiUpload('/spark/music/' + inp.dataset.mcover + '/cover', fd);
|
||||
pages.music();
|
||||
};
|
||||
});
|
||||
$('content').querySelectorAll('[data-maudio]').forEach((inp) => {"""
|
||||
|
||||
handler_new = """ $('content').querySelectorAll('[data-mcover]').forEach((inp) => {
|
||||
inp.onchange = async () => {
|
||||
if (!inp.files?.[0]) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', inp.files[0]);
|
||||
await apiUpload('/spark/music/' + inp.dataset.mcover + '/cover', fd);
|
||||
pages.music();
|
||||
};
|
||||
});
|
||||
$('content').querySelectorAll('[data-mbg]').forEach((inp) => {
|
||||
inp.onchange = async () => {
|
||||
if (!inp.files?.[0]) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', inp.files[0]);
|
||||
await apiUpload('/spark/music/' + inp.dataset.mbg + '/background', fd);
|
||||
pages.music();
|
||||
};
|
||||
});
|
||||
$('content').querySelectorAll('[data-maudio]').forEach((inp) => {"""
|
||||
|
||||
if handler_old not in c:
|
||||
raise SystemExit("cover/audio handlers not found")
|
||||
if "data-mbg" in c and "/background" in c[c.find("async music()") : c.find("async novelCms()")]:
|
||||
print("already patched?")
|
||||
else:
|
||||
c = c.replace(handler_old, handler_new, 1)
|
||||
|
||||
p.write_text(c, encoding="utf-8")
|
||||
music = c[c.find("async music()") : c.find("async novelCms()")]
|
||||
assert "backgroundUrl" in music
|
||||
assert "data-mbg" in music
|
||||
assert "/background" in music
|
||||
print("patched ok", p, "bytes", p.stat().st_size)
|
||||
5087
deploy/popadmin/popadmin.js
Normal file
5087
deploy/popadmin/popadmin.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,28 +3,38 @@
|
||||
## 1. spark-server(音乐 API + OpenAPI)
|
||||
|
||||
```bash
|
||||
# 新加坡机
|
||||
cd /opt/spark-server # 或实际路径
|
||||
# 拉代码后
|
||||
./cmd/migrate # 或项目惯用 migrate,执行 migrations/0020_music.sql
|
||||
# 重启 spark-server 进程 / docker compose
|
||||
# 本机(Windows/WSL):构建并部署到新加坡机
|
||||
make deploy
|
||||
# 会:linux 交叉编译 → docker image → 上传 ECS → spark-migrate(含 0020_music / 0021_music_background)→ 重启容器 → nginx reload
|
||||
```
|
||||
|
||||
新加坡机手工补跑迁移(仅 image 已更新但 migrate 未跑时):
|
||||
|
||||
```bash
|
||||
docker exec spark-server spark-migrate -config /app/configs/config.yaml
|
||||
# 或 compose 目录下:docker compose run --rm spark-server spark-migrate -config /app/configs/config.yaml
|
||||
sudo nginx -t && sudo nginx -s reload
|
||||
# 确认 include 了更新后的 popadmin-spark-admin-locations.conf(含 music)
|
||||
# 确认 include 了 popadmin-spark-admin-locations.conf(含 music,client_max_body_size 55m)
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
- `GET https://sparkapi.wyrs365.com/v1/music/playlist` → `code=0`
|
||||
- `GET https://sparkapi.wyrs365.com/docs` → 可见 music / admin-music / novels / admin-novels / 登录设备 Breaking 说明
|
||||
- popadmin 登录后「音乐列表」:`GET/POST /api/admin/spark/music`
|
||||
- `GET https://sparkapi.wyrs365.com/v1/music/playlist` → `code=0`,曲目含 `coverUrl` / `backgroundUrl`(可空)/ `audioUrl`
|
||||
- `GET https://sparkapi.wyrs365.com/docs` → 可见 music / admin-music
|
||||
- popadmin「音乐列表」:封面 / **背景** / mp3 上传;`POST /api/admin/spark/music/{id}/background`
|
||||
|
||||
## 2. Aliyun wyrs-admin(popadmin 静态)
|
||||
|
||||
热更或发版 `static/popadmin/js/popadmin.js`(侧栏「音乐列表」「小说内容管理」)。
|
||||
热更音乐背景图 UI(与现网一致):
|
||||
|
||||
新加坡若用 static-override,同步覆盖:
|
||||
```bash
|
||||
# 源:spark-server/deploy/popadmin/popadmin.js(已含背景列与上传)
|
||||
scp -i ~/.ssh/wyrs_shanghai_deploy.pem deploy/popadmin/popadmin.js \
|
||||
root@47.236.66.9:/opt/wyrs-admin/static-override/popadmin.js
|
||||
# 若 index 带 ?v= 缓存戳,同步 bump popadmin.index.html
|
||||
```
|
||||
|
||||
`/opt/wyrs-admin/static-override/popadmin.js`(或等价路径)并 bump `?v=`。
|
||||
新加坡若用 static-override,覆盖后强刷浏览器即可。
|
||||
|
||||
## 3. FullStoryNovel CMS(小说 iframe)
|
||||
|
||||
@ -39,3 +49,16 @@ sudo nginx -t && sudo nginx -s reload
|
||||
|
||||
OpenAPI 已按 `Spark资源文档/devices-client.md` 更新契约。
|
||||
**当前 Go 实现仍为旧登录**(Body 必填客户端 `device_id`、字符串 `device_type`,JWT 无 `aid`)。上线新契约前需改 auth/login、JWT、meta 枚举。
|
||||
|
||||
## 5. Demo 曲目(可选)
|
||||
|
||||
仓库旁 `_demo_music/`:`tracks.json` + `01..10.mp3/png` + `upload_demo.py`。
|
||||
背景图**可空**,demo **不强制**补背景;脚本勿删已有曲目(仅 POST 新建)。
|
||||
|
||||
```bash
|
||||
# 在新加坡机(或可访问 popadmin 登录与 spark admin API 的环境)
|
||||
export SPARK_ADMIN_BASE=http://127.0.0.1:18080
|
||||
export POPADMIN_LOGIN=http://127.0.0.1:8090/api/admin/auth/login
|
||||
python3 upload_demo.py
|
||||
curl -sS https://sparkapi.wyrs365.com/v1/music/playlist | jq '.data.items|length,.data.items[0]|{title,coverUrl,backgroundUrl,audioUrl}'
|
||||
```
|
||||
|
||||
@ -86,8 +86,8 @@ func (s *Store) applyCORS(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// applyPublicRead lets browsers load avatars without a presigned GET.
|
||||
// chat/ and perm/fav/ stay private.
|
||||
// applyPublicRead lets browsers load avatars / group icons / music assets without a
|
||||
// presigned GET. chat/ and perm/fav/ stay private.
|
||||
func (s *Store) applyPublicRead(ctx context.Context) {
|
||||
pabCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
@ -117,13 +117,14 @@ func publicReadPolicy(bucket string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Sid": "PublicReadAvatars",
|
||||
"Sid": "PublicReadPerm",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": ["s3:GetObject"],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::%[1]s/perm/avatar/*",
|
||||
"arn:aws:s3:::%[1]s/perm/group/*"
|
||||
"arn:aws:s3:::%[1]s/perm/group/*",
|
||||
"arn:aws:s3:::%[1]s/perm/music/*"
|
||||
]
|
||||
}]
|
||||
}`, bucket)
|
||||
|
||||
@ -104,9 +104,9 @@ func TestGetPutNotReady(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicReadPolicyCoversAvatarsOnly(t *testing.T) {
|
||||
func TestPublicReadPolicyCoversAvatarsAndMusic(t *testing.T) {
|
||||
p := publicReadPolicy("spark")
|
||||
if !strings.Contains(p, "spark/perm/avatar/*") || !strings.Contains(p, "spark/perm/group/*") {
|
||||
if !strings.Contains(p, "spark/perm/avatar/*") || !strings.Contains(p, "spark/perm/group/*") || !strings.Contains(p, "spark/perm/music/*") {
|
||||
t.Fatalf("%s", p)
|
||||
}
|
||||
if strings.Contains(p, "chat/") || strings.Contains(p, "perm/fav") || strings.Contains(p, "perm/moment") {
|
||||
|
||||
@ -7,6 +7,7 @@ const (
|
||||
DefaultPreviewSeconds = 30
|
||||
|
||||
MaxCoverBytes = 5 << 20 // 5 MiB
|
||||
MaxBackgroundBytes = 5 << 20 // 5 MiB
|
||||
MaxAudioBytes = 50 << 20 // 50 MiB
|
||||
)
|
||||
|
||||
@ -16,8 +17,10 @@ type Track struct {
|
||||
Title string `json:"title"`
|
||||
CoverKey string `json:"coverKey"`
|
||||
AudioKey string `json:"audioKey"`
|
||||
BackgroundKey string `json:"backgroundKey"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
AudioURL string `json:"audioUrl"`
|
||||
BackgroundURL string `json:"backgroundUrl"`
|
||||
RequireMember bool `json:"requireMember"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@ -181,36 +181,58 @@ func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
return errs.ErrNotFound.WithMsg("曲目不存在")
|
||||
}
|
||||
_ = s.deleteObject(ctx, t.CoverKey)
|
||||
_ = s.deleteObject(ctx, t.BackgroundKey)
|
||||
_ = s.deleteObject(ctx, t.AudioKey)
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) UploadCover(ctx context.Context, id int64, filename string, body []byte, contentType string) (*entity.Track, error) {
|
||||
return s.uploadImage(ctx, id, filename, body, contentType, "cover")
|
||||
}
|
||||
|
||||
func (s *Service) UploadBackground(ctx context.Context, id int64, filename string, body []byte, contentType string) (*entity.Track, error) {
|
||||
return s.uploadImage(ctx, id, filename, body, contentType, "background")
|
||||
}
|
||||
|
||||
func (s *Service) uploadImage(ctx context.Context, id int64, filename string, body []byte, contentType, kind string) (*entity.Track, error) {
|
||||
if !s.Ready() {
|
||||
return nil, ErrNotReady
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("请选择封面文件")
|
||||
label := "封面"
|
||||
maxBytes := entity.MaxCoverBytes
|
||||
if kind == "background" {
|
||||
label = "背景图"
|
||||
maxBytes = entity.MaxBackgroundBytes
|
||||
}
|
||||
if len(body) > entity.MaxCoverBytes {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("封面不能超过 5MB")
|
||||
if len(body) == 0 {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("请选择" + label + "文件")
|
||||
}
|
||||
if len(body) > maxBytes {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg(label + "不能超过 5MB")
|
||||
}
|
||||
ct := normalizeMIME(contentType, filename)
|
||||
if !isImageMIME(ct) {
|
||||
return nil, errs.ErrInvalidArgument.WithMsg("封面仅支持 jpg/png/webp/gif")
|
||||
return nil, errs.ErrInvalidArgument.WithMsg(label + "仅支持 jpg/png/webp/gif")
|
||||
}
|
||||
t, err := s.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errs.ErrNotFound.WithMsg("曲目不存在")
|
||||
}
|
||||
ext := extFor(ct, filename, "jpg")
|
||||
key := fmt.Sprintf("perm/music/cover/%d/%d.%s", id, time.Now().UnixNano(), ext)
|
||||
if err := s.put(ctx, key, body, ct, entity.MaxCoverBytes); err != nil {
|
||||
key := fmt.Sprintf("perm/music/%s/%d/%d.%s", kind, id, time.Now().UnixNano(), ext)
|
||||
if err := s.put(ctx, key, body, ct, maxBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
old := t.CoverKey
|
||||
var old string
|
||||
if kind == "background" {
|
||||
old = t.BackgroundKey
|
||||
t.BackgroundKey = key
|
||||
t.BackgroundURL = s.publicURL(key)
|
||||
} else {
|
||||
old = t.CoverKey
|
||||
t.CoverKey = key
|
||||
t.CoverURL = s.publicURL(key)
|
||||
}
|
||||
if err := s.repo.Update(ctx, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -31,8 +31,10 @@ type trackRow struct {
|
||||
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"`
|
||||
@ -104,8 +106,10 @@ func (r *Repo) Insert(ctx context.Context, t *entity.Track) error {
|
||||
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,
|
||||
@ -131,8 +135,10 @@ func (r *Repo) Update(ctx context.Context, t *entity.Track) error {
|
||||
"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,
|
||||
@ -192,8 +198,10 @@ func toEntity(row trackRow) entity.Track {
|
||||
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,
|
||||
|
||||
@ -1869,8 +1869,8 @@ paths:
|
||||
summary: 音乐列表(公开)
|
||||
description: |
|
||||
返回已上架曲目与全局「会员音乐免费试听秒数」。无需登录。
|
||||
封面/音频为 sparkr 公网 URL(`https://sparkr.wyrs365.com/spark/perm/music/...`)。
|
||||
客户端应按 `requireMember` 与 `memberPreviewSeconds` 控制试听。
|
||||
封面/背景图/音频为 sparkr 公网 URL(`https://sparkr.wyrs365.com/spark/perm/music/...`)。
|
||||
客户端应按 `requireMember` 与 `memberPreviewSeconds` 控制试听;`backgroundUrl` 可空。
|
||||
operationId: getMusicPlaylist
|
||||
security: []
|
||||
responses:
|
||||
@ -2044,6 +2044,36 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminApiResponse"
|
||||
/api/admin/spark/music/{id}/background:
|
||||
post:
|
||||
tags: [admin-music]
|
||||
summary: 上传背景图(multipart file)
|
||||
description: 字段名 `file`;jpg/png/webp/gif,≤5MB。写入 RustFS `perm/music/background/...`。可空(未上传时 `backgroundUrl` 为空串)。
|
||||
operationId: adminUploadMusicBackground
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer, format: int64 }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminApiResponse"
|
||||
/api/admin/spark/music/{id}/audio:
|
||||
post:
|
||||
tags: [admin-music]
|
||||
@ -2926,8 +2956,10 @@ components:
|
||||
title: { type: string }
|
||||
coverKey: { type: string }
|
||||
audioKey: { type: string }
|
||||
backgroundKey: { type: string, description: 背景图 object_key;可空 }
|
||||
coverUrl: { type: string, description: sparkr 公网 URL }
|
||||
audioUrl: { type: string, description: sparkr 公网 URL }
|
||||
backgroundUrl: { type: string, description: 背景图 sparkr 公网 URL;可空 }
|
||||
requireMember: { type: boolean, description: true=需会员 }
|
||||
sortOrder: { type: integer }
|
||||
enabled: { type: boolean }
|
||||
|
||||
@ -133,14 +133,18 @@ func (h *MusicHandler) Delete(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *MusicHandler) UploadCover(c *gin.Context) {
|
||||
h.upload(c, true)
|
||||
h.upload(c, "cover")
|
||||
}
|
||||
|
||||
func (h *MusicHandler) UploadBackground(c *gin.Context) {
|
||||
h.upload(c, "background")
|
||||
}
|
||||
|
||||
func (h *MusicHandler) UploadAudio(c *gin.Context) {
|
||||
h.upload(c, false)
|
||||
h.upload(c, "audio")
|
||||
}
|
||||
|
||||
func (h *MusicHandler) upload(c *gin.Context, cover bool) {
|
||||
func (h *MusicHandler) upload(c *gin.Context, kind string) {
|
||||
if !h.ready(c) {
|
||||
return
|
||||
}
|
||||
@ -162,9 +166,12 @@ func (h *MusicHandler) upload(c *gin.Context, cover bool) {
|
||||
}
|
||||
ct := hdr.Header.Get("Content-Type")
|
||||
var out any
|
||||
if cover {
|
||||
switch kind {
|
||||
case "cover":
|
||||
out, err = h.svc.UploadCover(c.Request.Context(), id, hdr.Filename, body, ct)
|
||||
} else {
|
||||
case "background":
|
||||
out, err = h.svc.UploadBackground(c.Request.Context(), id, hdr.Filename, body, ct)
|
||||
default:
|
||||
out, err = h.svc.UploadAudio(c.Request.Context(), id, hdr.Filename, body, ct)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@ -84,5 +84,6 @@ func registerMusicAdmin(g *gin.RouterGroup, h *adminhandler.MusicHandler) {
|
||||
g.PATCH("/music/:id", h.Update)
|
||||
g.DELETE("/music/:id", h.Delete)
|
||||
g.POST("/music/:id/cover", h.UploadCover)
|
||||
g.POST("/music/:id/background", h.UploadBackground)
|
||||
g.POST("/music/:id/audio", h.UploadAudio)
|
||||
}
|
||||
|
||||
5
migrations/0021_music_background.sql
Normal file
5
migrations/0021_music_background.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- 曲目背景图(与 cover 同形态:object key + 公网 URL;可空)。
|
||||
|
||||
ALTER TABLE t_music_tracks
|
||||
ADD COLUMN background_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT '背景图 object_key(sparkr)' AFTER cover_url,
|
||||
ADD COLUMN background_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '背景图公网 URL' AFTER background_key;
|
||||
Loading…
Reference in New Issue
Block a user