Skip to content

配置管理

本文档基于以下源文件编写:

目录

  1. 数据模型与存储
  2. ConfigService 设计
  3. 三种配置接口体系
  4. 业务端点清单(/settings/*)
  5. 配置变更回调机制
  6. Tab 配置详解
  7. 模块聚合端点示例:/cache-manage/config
  8. 接口选型决策树

1. 数据模型与存储

章节来源internal/models/models.gointernal/database/config_repository.go

所有配置持久化在 SQLite 的 configs 表中,每行一个键值对:

go
type Config struct {
    ID        int64     `json:"id"`
    Key       string    `json:"key"`        // 唯一键,如 "music_path"、"hls_proxy_enabled"
    Value     string    `json:"value"`       // 字符串值,复杂结构以 JSON 序列化存储
    UpdatedAt time.Time `json:"updated_at"`
}

仓储层 ConfigRepository 遵循项目的 sqlc + squirrel 混合策略:固定 SQL(Get/Set/Delete)走 sqlc,动态过滤的 List/Count 走 squirrel(支持 key LIKE 关键词搜索 + 白名单排序 + 分页)。Set 实现为 UPSERT on key,未命中的 Get/Delete 返回 ErrNotFound


2. ConfigService 设计

章节来源internal/services/config_service.go

ConfigService 是配置的唯一业务入口,所有 handler 通过它读写配置。核心设计要点:

2.1 内存缓存

使用 sync.Map 作为并发安全的内存缓存(key 为配置键,value 为原始字符串值):

  • 读路径:先查缓存,命中直接返回;未命中则查数据库,写入缓存后返回
  • 写路径:先写数据库,成功后更新缓存(Set)或清除缓存(CreateConfig/UpdateConfig/DeleteConfig
  • ClearCache() 重置整个缓存,ClearCacheKey(key) 清除单个 key

2.2 类型化辅助方法

ConfigService 提供一组类型化的读写方法,将 configs 表的 string value 透明转换为业务所需类型:

方法读/写说明
GetString(key, default)返回原始字符串,缺失时返回默认值
GetInt(key, default)strconv.Atoi 转换,失败返回默认值
GetBool(key, default)strconv.ParseBool 转换,失败返回默认值
GetJSON(key, target)json.Unmarshal 到任意结构体,缓存无效时自动清除
Set(key, value)写入字符串值并更新缓存
SetJSON(key, value)json.Marshal 后调用 Set

所有 Get* 方法在数据库查询失败时静默降级(slog.Warn 后返回默认值),不中断业务流程。GetJSON 是例外:它返回 error,由调用方决定降级策略。

面向通用 KV 端点另有完整 CRUD 方法(GetConfig/CreateConfig/UpdateConfig/DeleteConfig/ListConfigs/CountConfigs),直接透传到仓储层,写操作后清除对应 key 的缓存。


3. 三种配置接口体系

章节来源internal/app/routers.goAGENTS.md 配置接口规范章节

项目中存在三种配置接口,各有定位:

┌─────────────────────────────────────────────────────────────────┐
│                      配置接口体系                                │
├─────────────────┬──────────────────┬────────────────────────────┤
│  /settings/*    │  /module/config  │  /configs/{key}            │
│  业务端点        │  模块聚合端点     │  通用 KV                   │
├─────────────────┼──────────────────┼────────────────────────────┤
│  强类型 JSON     │  强类型 JSON     │  裸 key/value 字符串       │
│  自带默认值      │  自带默认值       │  key 不存在 → 404          │
│  内联副作用      │  内联副作用       │  无副作用(除回调)         │
│  前端业务功能    │  模块内部配置     │  admin 编辑器专用           │
└─────────────────┴──────────────────┴────────────────────────────┘
  • /settings/<name>(孤立业务端点):强类型 JSON,handler 内自带默认值与副作用。路径为 kebab-case,归属到对应业务模块的 handler
  • /module/config(模块聚合端点):配置与同模块动作端点强相关时使用(已有实例:/cache-manage/config
  • /configs/{key}(通用 KV):admin 配置编辑器(config_manager.dart)的后端入口,提供完整 CRUD。PUT 时 key 不存在返回 404,无强类型校验,副作用仅通过 onConfigChanged 回调触发。新增业务功能不应直调此接口

4. 业务端点清单(/settings/*)

章节来源internal/app/routers.go(路由注册)、各 handler 文件

所有业务端点均为 GET + PUT 双方法,GET 返回当前值(含默认值),PUT 写入并可能触发副作用。

4.1 /settings/music-path

图表来源internal/handlers/scan.go MusicPathSetting 结构体

字段类型默认值说明
pathstring"music"音乐根目录路径
exclude_dirsstring[]["@eaDir", "tmp"]按目录名排除(递归匹配)
exclude_pathsstring[][]按完整路径排除
  • config keymusic_path(JSON 值)
  • handlerScanHandler
  • 副作用:PUT 后异步调用 onMusicPathChanged 回调,重建 Scanner 并清理排除目录中的歌曲
  • 验证path 不能为空

4.2 /settings/hls-proxy

字段类型默认值说明
enabledboolfalseHLS 电台反代开关
  • config keyhls_proxy_enabled(字符串 "true"/"false"
  • handlerHLSHandler
  • 业务语义false 时电台 m3u8 直接 302 给 player;true 时服务端拉取并改写 m3u8、代理所有切片

4.3 /settings/auto-scan

字段类型默认值说明
enabledboolfalse自动扫描开关
interval_secondsint3600扫描间隔(秒),范围 [60, 86400]
  • config keyauto_scan(JSON 值)
  • handlerScanHandler
  • 副作用:PUT 后异步调用 onAutoScanChanged 回调,通过 autoScanner.ApplyConfig 重启调度器
  • 验证interval_seconds 必须在 60 到 86400 之间

4.4 /settings/scan-title-source

字段类型默认值说明
title_sourcestring"tag"扫描标题来源,tagfilename
  • config keyscan_title_source(字符串值)
  • handlerScanHandler
  • 副作用:PUT 后触发 onMusicPathChanged(需以「重新导入」模式扫描才能生效)
  • 验证:值必须为 "tag""filename"

4.5 /settings/scan-auto-create-playlists

字段类型默认值说明
enabledbooltrue扫描后是否自动创建歌单
  • config keyscan_auto_create_playlists(字符串 "true"/"false"
  • handlerScanHandler

4.6 /settings/scan-playlist-mode

字段类型默认值说明
modestring"directory"目录歌单归并模式,取值 directory / top_level / bubble_up

三种模式语义(与 PlaylistRepository.AutoCreate 一致):

  • directory:每个歌曲所在文件夹各自生成一个独立歌单

  • top_level:只按顶层(一级)目录归并,同一顶层目录下的所有歌曲进同一个歌单

  • bubble_up:歌曲加入自己所在目录歌单的同时,向上冒泡加入其所有父目录对应的歌单

  • config keyscan_playlist_mode(字符串值)

  • handlerScanHandler

  • 验证mode 必须为 directory / top_level / bubble_up 之一,否则返回 400

该模式仅在 §4.5 scan-auto-create-playlists 开启(默认 true)时生效;两者配合决定"是否创建目录歌单"与"如何归并目录"。

4.7 /settings/log-level

字段类型默认值说明
levelstring"info"日志等级:debug/info/warn/error
  • config keylog_level(字符串值)
  • handlerLogHandler(独立 handler,持有 *slog.LevelVar
  • 副作用:PUT 时通过 levelVar.Set(lvl) 即时切换运行时日志等级,无需重启
  • 验证ParseLogLevel 映射表校验,非法等级返回 400

4.8 /settings/plugin-registries

字段类型默认值说明
registriesRegistryConfig[]官方插件源插件订阅源列表

每个 RegistryConfig 包含 url(string)、name(string)、enabled(bool)。

  • config keyplugin_registries(JSON 值)
  • handlerJSPluginHandler
  • 默认值:未配置时返回包含 Songloft 官方插件源的单元素列表

4.9 /settings/http-proxy

字段类型默认值说明
proxystring""HTTP 代理地址,如 http://192.168.1.1:7890
  • config keyhttp_proxy(JSON 值)
  • handlerJSPluginHandler
  • 副作用:PUT 时调用 httputil.SetGlobalProxy(proxy) 即时切换全局 HTTP 代理
  • 验证SetGlobalProxy 内部校验代理地址格式

4.10 /settings/tab-config

字段类型默认值说明
show_librarybooltrue是否显示歌曲库 Tab
show_playlistsbooltrue是否显示歌单 Tab
plugin_tabspluginTabEntry[][]插件 Tab 列表
  • config keytab_config(JSON 值)
  • handlerConfigHandler
  • 详细规则见下方 Tab 配置详解

5. 配置变更回调机制

章节来源internal/handlers/config.goSetOnConfigChanged)、internal/app/routers.go(回调注册)

通用 KV 端点 PUT /configs/{key} 更新配置后会触发 onConfigChanged 回调。这是保持通用端点与业务端点副作用对齐的关键机制。

5.1 回调注册

go
// routers.go 中的注册逻辑
configHandler.SetOnConfigChanged(func(key string) {
    switch key {
    case "music_path":
        musicPathChanged()       // 重建 Scanner + 清理排除歌曲
    case "auto_scan":
        cfg := a.autoScanner.GetConfig()
        a.autoScanner.ApplyConfig(cfg)  // 重启自动扫描调度
    }
})

5.2 双入口一致性

回调通过 go 异步执行,不阻塞 PUT 响应。业务端点和通用端点写同一 config key 时,必须触发相同的副作用。例如 PUT /settings/music-path 在 handler 内直接调用 onMusicPathChanged(),而 PUT /configs/music_path 通过 configHandler.onConfigChanged("music_path") 间接触发同一函数。


6. Tab 配置详解

章节来源internal/handlers/tab_config_setting.go

底部导航栏配置控制前端 App 底部 Tab 的组成,最多 5 个 Tab。

6.1 Tab 结构

┌───────────────────────────────────────────────────┐
│  底部导航栏(最多 5 个 Tab)                        │
├──────────┬──────────┬──────────┬──────────┬────────┤
│  首页    │  歌曲库  │  歌单    │  插件X   │ 设置   │
│ (固定)   │ (可选)   │ (可选)   │ (可选)   │(固定)  │
└──────────┴──────────┴──────────┴──────────┴────────┘
    固定项        可选项(最多 3 个)         固定项
  • 固定项:首页(Home)和设置(Settings)始终显示,不在配置中
  • 可选项:歌曲库(show_library)、歌单(show_playlists)和插件 Tab(plugin_tabs),三者总数不超过 3 个

6.2 插件 Tab 条目

go
type pluginTabEntry struct {
    PluginID  int    `json:"plugin_id"`    // 插件 ID
    EntryPath string `json:"entry_path"`   // 插件入口路径(不可为空,不可重复)
    Name      string `json:"name"`         // 显示名称(不可为空)
}

6.3 校验规则

  1. 可选 Tab 总数(show_library 启用算 1 + show_playlists 启用算 1 + plugin_tabs 长度)不超过 3
  2. 每个插件 Tab 的 entry_path 不能为空
  3. 每个插件 Tab 的 name 不能为空
  4. 插件 Tab 之间的 entry_path 不能重复

6.4 默认配置

未配置时返回默认值:show_library=trueshow_playlists=trueplugin_tabs=[],即 4 个 Tab(首页、歌曲库、歌单、设置)。


7. 模块聚合端点示例:/cache-manage/config

章节来源internal/handlers/cache.go

缓存管理模块将配置端点与动作端点聚合在同一前缀 /api/v1/cache-manage/ 下,是模块聚合模式的典型实例:

端点方法说明
/cache-manage/statsGET获取缓存统计(总大小、文件数、最大限制)
/cache-manage/cleanPOST清理全部缓存
/cache-manage/configGET获取缓存配置(max_sizecache_dir
/cache-manage/configPUT更新缓存配置
/cache-manage/validate-dirPOST验证目录可用性(自动创建 + 可写性检查 + 磁盘空间)

配置端点与 stats/clean 共用同一个 CacheService,属于强关联场景,因此保留在模块前缀下而不拆到 /settings/

PUT 更新缓存配置时的校验逻辑:

  • max_size 不能为负数(0 表示不限制)
  • cache_dir 为空字符串时恢复默认目录
  • cache_dir 非空时必须为绝对路径,且通过可写性验证

8. 接口选型决策树

图表来源:综合 AGENTS.md 配置接口规范与各 handler 实现

为新增配置选择接口风格时,参考以下决策流程:

新增一个配置项

    ├── 该配置属于某个业务模块,且该模块已有动作端点?
    │       │
    │       ├── 是 → 模块已有 /module/config 端点?
    │       │       │
    │       │       ├── 是 → 扩展现有 config 结构体
    │       │       └── 否 → 评估是否值得新建聚合端点
    │       │               否则走 /settings/<name>
    │       │
    │       └── 否 → /settings/<name>

    └── 仅供 admin 调试用?

            ├── 是 → 直接写 configs 表,通过通用 KV 端点访问
            └── 否 → /settings/<name>

核心原则:业务端点是用户可见入口的唯一来源,通用 KV 退化为 admin 后门。两条入口写同一 key 时,副作用必须通过 SetOnConfigChanged 回调保持一致。