数据模型设计
本文档基于以下源文件编写:
- internal/models/models.go -- 所有领域模型、DTO、验证逻辑与自定义序列化
- internal/models/backup.go -- 备份/恢复数据模型
- internal/models/lyric.go -- 歌词载荷模型与序列化
- internal/models/constant.go -- 分页常量
- internal/database/sqlc/models.go -- sqlc 生成的数据库行映射模型
- internal/database/migrations/0001_init.sql -- 初始 schema(6 张表 + 索引 + 触发器 + 预置数据;plugin_storage 表见 0016)
- internal/database/migrations/0005_lyric_source_manual.sql -- lyric_source 新增 manual
- internal/database/migrations/0007_songs_year_genre.sql -- 新增 year/genre 列
- internal/database/migrations/0008_songs_fingerprint.sql -- 新增 fingerprint 列
目录
- 概述
- ER 关系图
- Song 歌曲模型
- Playlist 歌单模型
- PlaylistSong 关联模型
- Config 配置模型
- AuthToken 认证令牌模型
- JSPlugin 插件模型
- 歌词模型
- 备份模型
- 请求/响应 DTO
- 模型关系类图
1. 概述
Songloft 的数据持久层基于 SQLite,采用 goose 迁移 + sqlc 固定 SQL + squirrel 动态 SQL 的分层访问栈。领域模型定义在 internal/models/ 包,与 sqlc 自动生成的行映射模型(internal/database/sqlc/models.go)分离,由 Repository 层负责两者之间的转换。
当前 schema 共 7 张表:songs、playlists、playlist_songs、configs、auth_tokens、js_plugins、plugin_storage,通过 25 个迁移文件从初始版本演进到当前状态。核心表的 updated_at 列均由 SQLite TRIGGER 自动维护。
章节来源
- internal/database/migrations/0001_init.sql:1-190 -- 完整初始 schema
2. ER 关系图
图表来源
- internal/database/migrations/0001_init.sql:3-103 -- 表定义
- internal/database/migrations/0007_songs_year_genre.sql -- year/genre
- internal/database/migrations/0008_songs_fingerprint.sql -- fingerprint
3. Song 歌曲模型
Song 是系统最核心的模型,包含 37 个持久化字段 + 3 个运行时计算字段,承载本地歌曲、网络歌曲和电台三种业务形态。类型由 CHECK(type IN ('local', 'remote', 'radio')) 约束。
| 常量 | 值 | 必填字段 |
|---|---|---|
TypeLocal | local | file_path |
TypeRemote | remote | url |
TypeRadio | radio | url |
3.1 字段分组
基础信息:id(PK), type, title, artist, album, year(0007), genre(0007), track(0020), language(0022), style(0022), duration
文件与音频:file_path, url, file_size, format, bit_rate, sample_rate, is_live, is_video(0024), file_modified_at(0019), cache_path(0014)
CUE 分轨:cue_source_path(0018), cue_track_index(0018), cue_audio_path(0018)
封面:cover_path(json:"-"), cover_url(序列化时重写)
歌词:lyric(json:"-", LyricPayload JSON), lyric_source(json:"-"), lyric_remote_url, lyric_url(运行时)
插件与去重:plugin_entry_path, source_data(opaque JSON), dedup_key, source_url(运行时)
识别:fingerprint(0008, Chromaprint), fingerprint_duration(0008), isrc(0009)
时间戳:added_at, updated_at(TRIGGER)
章节来源
- internal/models/models.go:69-101 -- Song 结构体
3.2 自定义 MarshalJSON
Song 实现了 MarshalJSON(),在序列化时重写四个字段,使客户端无需关心内部存储细节:
| 字段 | 重写逻辑 | 示例 |
|---|---|---|
url | PlaybackURL() 统一播放端点 | /api/v1/songs/42/play |
cover_url | CoverURLPath(),无封面为空 | /api/v1/songs/42/cover |
lyric_url | LyricURLPath(),无歌词为空 | /api/v1/songs/42/lyric |
source_url | remote/radio 填原始 URL | https://stream.example.com/live.m3u8 |
通过 type songAlias Song 技巧避免递归。原始 URL 保留在数据库不暴露给客户端,所有播放统一通过 /api/v1/songs/{id}/play 分发。
章节来源
- internal/models/models.go:178-189 -- MarshalJSON
3.3 PlaybackURL 与 HLS 规则
- 普通歌曲:
/api/v1/songs/{id}/play - HLS 电台(URL 后缀
.m3u8/.m3u):/api/v1/songs/{id}/play.m3u8
追加 .m3u8 后缀原因:ExoPlayer/AVPlayer 按 URL 后缀选择 MediaSource 类型,无后缀会落到 ProgressiveMediaSource 导致 HLS 直播流无法播放。IsHLSStream() 解析 URL 路径后缀判定,仅 TypeRadio 生效。
章节来源
- internal/models/models.go:108-139 -- PlaybackURL 与 IsHLSStream
3.4 CoverURLPath 与 LyricURLPath
两个方法遵循"有则返回端点、无则返回空"原则,避免客户端发起注定 404 的请求:
- CoverURLPath:
CoverPath或CoverURL任一非空时返回/api/v1/songs/{id}/cover - LyricURLPath:
Lyric非空,或LyricSource=="url"且LyricRemoteURL非空时返回/api/v1/songs/{id}/lyric
章节来源
- internal/models/models.go:143-168 -- URL 生成逻辑
3.5 DedupKey 去重机制
dedup_key 用于网络歌曲去重,典型形态 <platform>:<platform_id>。与 plugin_entry_path 组成条件唯一索引(partial unique index),仅 dedup_key != '' 的行生效:
CREATE UNIQUE INDEX idx_songs_dedup_key_unique
ON songs(plugin_entry_path, dedup_key) WHERE dedup_key != '';章节来源
- internal/database/migrations/0001_init.sql:122 -- dedup_key 索引
4. Playlist 歌单模型
| 字段 | Go 类型 | JSON | 说明 |
|---|---|---|---|
ID | int64 | id | 自增主键 |
Type | string | type | normal 或 radio(CHECK 约束) |
Name | string | name | 全局唯一(idx_playlists_name_unique) |
Description | string | description | 描述 |
CoverPath | string | -(不暴露) | 封面本地路径 |
CoverURL | string | cover_url | 序列化时重写为 /api/v1/playlists/{id}/cover |
Labels | []string | labels | JSON 数组(DB 默认 '[]') |
SongCount | int | song_count | 运行时计算(非持久化) |
DB 层另有 position(排序用)和时间戳列。MarshalJSON() 同 Song 一样重写 cover_url。
CanAddSong 校验:normal 歌单仅接受 local/remote,radio 歌单仅接受 radio。
Labels:built_in 标记内置不可删除歌单(预置 id=1「收藏」、id=2「电台收藏」),auto_created 标记扫描自动创建的歌单。
章节来源
- internal/models/models.go:253-303 -- Playlist 结构体与 CanAddSong
- internal/database/migrations/0001_init.sql:163-166 -- 预置歌单
5. PlaylistSong 关联模型
实现歌单与歌曲的 M:N 多对多关联:id(PK), playlist_id(FK), song_id(FK), position, added_at。
关键约束:UNIQUE(playlist_id, song_id) 防重复添加;ON DELETE CASCADE 跟随歌单/歌曲删除;(playlist_id, position) 复合索引加速排序。
章节来源
- internal/models/models.go:306-326 -- PlaylistSong
- internal/database/migrations/0001_init.sql:45-55 -- 表约束
6. Config 配置模型
Key-value 模式,key 全局唯一,value 为 JSON 字符串。预置配置项:
| key | 说明 | 迁移 |
|---|---|---|
music_path | 音乐目录与排除目录 | 0001 |
scan_config | 自动扫描、间隔、格式 | 0001 |
jwt_secret | JWT 密钥(randomblob 随机生成) | 0001 |
source_validation / source_fallback / source_metrics | 音源验证/降级/指标 | 0001 |
plugin_registries | 官方插件注册表 URL | 0006 |
scan_auto_create_playlists | 是否自动创建歌单 | 0012 |
章节来源
- internal/models/models.go:329-345 -- Config 结构体
- internal/database/migrations/0001_init.sql:169-178 -- 预置数据
7. AuthToken 认证令牌模型
JWT 双 Token 认证,auth_tokens 表记录令牌元数据与撤销审计。
| 字段 | 说明 |
|---|---|
token_id | 唯一标识(UNIQUE) |
token_type | access(短期)或 refresh(长期),CHECK 约束 |
client_info | User-Agent 追踪 |
expires_at | 过期时间 |
revoked_at | 撤销时间(nullable,sql.NullTime),判空即知是否已撤销 |
revoked_by / revoked_reason | 撤销审计(谁、为什么) |
TokenInfo 是 AuthToken 的客户端友好版本(去掉内部 ID),用于令牌列表 API。
章节来源
- internal/models/models.go:417-439 -- AuthToken 与 TokenInfo
- internal/database/migrations/0001_init.sql:67-78 -- 表定义
8. JSPlugin 插件模型
QuickJS 沙盒插件的元数据与运行状态。核心字段:
| 字段 | 说明 |
|---|---|
entry_path | 路由前缀(UNIQUE),如 "myplugin" |
main | 入口文件(默认 main.js) |
permissions | JSON 数组,如 ["net","storage","fs:music"] |
public_paths | 无需 JWT 的路径前缀(0010 新增) |
external_paths | 可访问的外部绝对路径(0013 新增) |
icon | 插件图标(0011 新增) |
status | active / inactive(默认)/ error,CHECK 约束 |
zip_hash / entry_hash | SHA256 哈希,用于文件指纹热更新检测 |
file_path | ZIP 文件相对路径 |
运行时比对文件系统 hash 与数据库记录,不一致时自动触发重新加载。
章节来源
- internal/models/models.go:509-543 -- JSPlugin 结构体与状态枚举
- internal/database/migrations/0001_init.sql:81-103 -- js_plugins 表
9. 歌词模型
9.1 LyricPayload
songs.lyric 列的结构化存储格式,同时也是 /api/v1/songs/{id}/lyric 的响应形态:
| 字段 | JSON | 说明 |
|---|---|---|
Lyric | lyric | 主歌词(LRC 文本) |
Tlyric | tlyric | 翻译歌词 |
Rlyric | rlyric | 罗马音歌词 |
Lxlyric | lxlyric | 逐字歌词 |
关键方法:MarshalString() 空 payload 返回空字符串(非 "{}")保持 SQL 判空语义;UnmarshalLyric(raw) 兼容空字符串、合法 JSON、裸 LRC 文本三种历史形态;ApplyLyricToSong(s, text, source) 按 source 类型决定存到 Lyric 还是 LyricRemoteURL。
9.2 LyricSource 枚举
| 值 | 说明 |
|---|---|
file | 同目录 .lrc 文件 |
embedded | 音频内嵌歌词 |
url | 运行时按需拉取(LyricRemoteURL 存地址) |
cached | URL 拉取后缓存的文本 |
manual | 用户手动调整,扫描不覆盖(0005 迁移通过重建表扩展 CHECK) |
章节来源
- internal/models/lyric.go:13-79 -- LyricPayload 完整实现
- internal/models/models.go:27-33 -- 来源常量
10. 备份模型
版本化快照设计(当前 BackupVersion = 1),将歌单与歌曲导出为自描述 JSON。
- BackupData:顶层容器,含
version、exported_at、playlists[] - BackupPlaylist:歌单快照(name/type/description/labels/songs[])
- BackupSong:Song 的精简版(16 个核心字段),不含 ID、时间戳、歌词、封面路径等可重建数据
- ImportResult:导入统计 --
playlists_created/playlists_merged/songs_created/songs_matched/songs_skipped
章节来源
- internal/models/backup.go:1-47 -- 备份模型
11. 请求/响应 DTO
领域模型与 HTTP 层通过 DTO 解耦。
认证:LoginRequest(username/password) -> LoginResponse(access_token/refresh_token/expires_in/token_type);RefreshTokenRequest(refresh_token);RevokeTokenRequest(reason)
批量操作:BatchDeleteSongsRequest(ids/delete_files) -> BatchDeleteSongsResponse(deleted);BatchDeletePlaylistsRequest(ids) -> BatchDeletePlaylistsResponse(deleted)
自动创建歌单:AutoCreatePlaylistsRequest(include_subdirs) -> AutoCreatePlaylistsResponse(playlists[]/total);子结构 PlaylistInfo(playlist_id/name/song_count)
配置:CreateConfigRequest(key/value);UpdateConfigRequest(value);ConfigFilter(keyword/limit/offset/order_by/order)
升级:RemoteVersionInfo(version/git_commit/build_time/download_url_prefix/release_notes);UpgradeProgress(status/progress/current_step/error),状态流转 idle -> downloading -> testing -> replacing -> restarting(异常 failed/resetting)
通用:ErrorResponse({error, detail});SuccessResponse({message})
章节来源
- internal/models/models.go:348-507 -- DTO 定义
12. 模型关系类图
图表来源
- internal/models/models.go -- 领域模型
- internal/models/lyric.go -- 歌词模型
- internal/models/backup.go -- 备份模型
