fix(classification): initialize defaults in fresh v3 environments

This commit is contained in:
jxxghp
2026-09-13 11:31:54 +08:00
parent 5f050f35b6
commit 9ba513debc
3 changed files with 75 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ from pydantic import ValidationError
from app.application.classification.configuration import (
ClassificationPolicyConfigurationService,
ClassificationPolicyValidationError,
build_default_classification_policy,
needs_default_music_classification,
with_default_music_classification,
)
@@ -95,7 +96,10 @@ async def compose_classification(
enrichment: ClassificationEnrichmentPort | None = None,
) -> ClassificationComposition:
"""
构造分类策略服务,并仅在新配置键不存在时读取一次 legacy YAML
构造分类策略服务,并仅在新配置键不存在时读取一次 legacy YAML。
新环境没有旧配置文件时,启动层会显式发布内置默认策略;已有策略或合法
legacy 配置始终优先,避免用默认值覆盖用户事实。
已存在但损坏的新策略不会重新读取 YAML 或覆盖数据库事实,运行时保持不可用并暴露诊断。
"""
@@ -189,7 +193,8 @@ async def compose_classification(
legacy_path = Path(settings.CONFIG_PATH) / "category.yaml"
if not await executor.run(legacy_path.exists):
await service.async_initialize()
await service.async_initialize(build_default_classification_policy())
logger.info("未发现 category.yaml,已初始化内置默认分类策略 revision 1")
return finish(
ClassificationRuntime(service),
migrated=False,

View File

@@ -118,7 +118,7 @@ V2 自动分类主要由 TMDB 详情和 `category.yaml` 驱动,只覆盖电影
目录、订阅、下载和整理历史不再只保存易变的分类名称,而是同时记录内部分类编号 `category_id` 和当时的路径快照。分类改名或调整路径后,既有目录和订阅仍能找到同一个分类;已经建立的整理计划会继续使用计划创建时保存的分类目标,不会被后续策略修改。
升级时,如果尚未存在 V3 分类策略,系统会读取现有 `category.yaml` 并自动迁移;旧 TMDB 规则的顺序、排除条件、年份范围和“没有规则命中时使用的默认分类”会保留。迁移完成后不再继续写入 YAML。旧 `GET /api/v1/media/category` 和 `GET /api/v1/media/category/config` 暂时保留为只读投影,旧 `POST /api/v1/media/category/config` 已移除,所有新写入都通过带版本号校验的策略接口完成。
升级时,如果尚未存在 V3 分类策略,系统会优先读取现有 `category.yaml` 并自动迁移;没有旧配置文件的新环境会直接初始化内置的电影、电视剧和音乐默认分类。旧 TMDB 规则的顺序、排除条件、年份范围和“没有规则命中时使用的默认分类”会保留。迁移完成后不再继续写入 YAML。旧 `GET /api/v1/media/category` 和 `GET /api/v1/media/category/config` 暂时保留为只读投影,旧 `POST /api/v1/media/category/config` 已移除,所有新写入都通过带版本号校验的策略接口完成。
旧分类名称中用于表示目录层级的 `/` 会在迁移时转换为多个安全路径段,分类名称和目录显示结果保持不变;已经完成迁移的策略会在数据库升级时自动修复。

View File

@@ -386,6 +386,73 @@ async def test_edited_legacy_default_policy_is_not_automatically_changed(
assert store.write_count == 0
@pytest.mark.asyncio # type: ignore[misc]
async def test_fresh_environment_initializes_builtin_default_policy(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""没有旧配置文件的新环境应持久化完整内置分类,而不是只有未分类兜底。"""
store = _MemoryPolicyStore()
monkeypatch.setattr(
classification_composition,
"SystemConfigClassificationPolicyStore",
lambda *_args: store,
)
composition = await classification_composition.compose_classification(
executor=cast(Any, _InlineExecutor()),
settings=cast(Any, SimpleNamespace(CONFIG_PATH=tmp_path)),
system_config=cast(Any, _SystemConfig({})),
)
policy = composition.runtime.require_policy()
assert composition.migrated is False
assert policy.revision == 1
assert [category.id for category in policy.categories] == [
"movie.animation",
"movie.chinese",
"movie.foreign",
"tv.dongman.cn",
"tv.dongman.jp",
"tv.documentary",
"tv.kids",
"tv.variety",
"tv.chinese",
"tv.western",
"tv.asian",
"tv.uncategorized",
"music.uncategorized",
"music.album",
"music.compilation",
"music.ep",
"music.single",
]
assert [rule.id for rule in policy.rules] == [
"movie.animation.default",
"movie.chinese.default",
"tv.dongman.cn.default",
"tv.dongman.jp.default",
"tv.documentary.default",
"tv.kids.default",
"tv.variety.default",
"tv.chinese.default",
"tv.western.default",
"tv.asian.default",
"music.compilation.default",
"music.ep.default",
"music.single.default",
"music.album.default",
]
assert policy.fallbacks == {
"电影": "movie.foreign",
"电视剧": "tv.uncategorized",
"音乐": "music.uncategorized",
}
assert store.state is not None
assert store.state.active == policy
assert store.write_count == 1
@pytest.mark.asyncio # type: ignore[misc]
async def test_absent_policy_migrates_yaml_once_without_rewriting_file(
monkeypatch: pytest.MonkeyPatch,