From 895635c277928a7d30ecbde7800e2f81218bc3a0 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 14 Aug 2026 06:59:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(db):=20=E5=81=87=E5=80=BC=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=90=BD=E5=BA=93=E8=80=8C=E9=9D=9E=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E8=AE=B0=E5=BD=95=EF=BC=8C=E5=B8=83=E5=B0=94=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E5=85=B3=E9=97=AD=E6=80=81=E5=8F=AF=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=EF=BC=88#6309=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SystemConfigOper.set/async_set 原先对假值(False/0/None/空容器)执行删除记录, 读取端以「无记录」表示未配置并回落默认值,导致「挂载盘删除空目录」等布尔开关 关闭后刷新自动弹回开启。现改为假值同样落库更新,显式删除仍走 delete 方法。 --- app/db/systemconfig_oper.py | 14 +++--- tests/test_systemconfig_oper.py | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/app/db/systemconfig_oper.py b/app/db/systemconfig_oper.py index a6c89c2d5..35dc3e9ce 100644 --- a/app/db/systemconfig_oper.py +++ b/app/db/systemconfig_oper.py @@ -41,10 +41,9 @@ class SystemConfigOper(DbOper, metaclass=Singleton): conf = SystemConfig.get_by_key(self._db, key) if conf: if old_value != value: - if value: - conf.update(self._db, {"value": value}) - else: - conf.delete(self._db, conf.id) + # 假值(False/0/None/空容器)同样落库而不是删除记录: + # 读取端以「无记录」表示未配置并回落默认值,删除会使布尔开关的关闭态无法持久化 + conf.update(self._db, {"value": value}) return True return None else: @@ -77,10 +76,9 @@ class SystemConfigOper(DbOper, metaclass=Singleton): return None # 执行数据库更新 if conf: - if value: - await conf.async_update(self._db, {"value": value}) - else: - await conf.async_delete(self._db, conf.id) + # 假值(False/0/None/空容器)同样落库而不是删除记录: + # 读取端以「无记录」表示未配置并回落默认值,删除会使布尔开关的关闭态无法持久化 + await conf.async_update(self._db, {"value": value}) else: conf = SystemConfig(key=key, value=value) await conf.async_create(self._db) diff --git a/tests/test_systemconfig_oper.py b/tests/test_systemconfig_oper.py index e28e7f0f8..58663cf67 100644 --- a/tests/test_systemconfig_oper.py +++ b/tests/test_systemconfig_oper.py @@ -1,8 +1,24 @@ import threading +import uuid from concurrent.futures import ThreadPoolExecutor +import pytest + +from app.db.models.systemconfig import SystemConfig from app.db.systemconfig_oper import SystemConfigOper from app.schemas.types import SystemConfigKey +from app.utils.singleton import Singleton + + +def _unique_key() -> str: + """生成独立测试键,避免用例间相互污染。""" + return f"__test__{uuid.uuid4().hex}" + + +def _fresh_oper() -> SystemConfigOper: + """重置单例并从数据库重新加载配置缓存。""" + Singleton._instances.pop((SystemConfigOper, (), frozenset()), None) + return SystemConfigOper() def test_increment_serializes_concurrent_counter_updates(monkeypatch): @@ -49,3 +65,76 @@ def test_increment_supports_custom_step(monkeypatch): assert result == 7 assert stored_value["value"] == 7 + + +def test_set_creates_record_for_falsy_value(): + """无记录时写入假值应创建记录,而不是丢弃配置。""" + key = _unique_key() + oper = _fresh_oper() + + assert oper.set(key, False) is True + assert oper.get(key) is False + assert SystemConfig.get_by_key(oper._db, key) is not None + + +def test_set_persists_falsy_value_on_existing_record(): + """已存在记录时写入假值应更新记录,而不是删除记录回落默认值。""" + key = _unique_key() + oper = _fresh_oper() + + oper.set(key, True) + assert oper.set(key, False) is True + assert oper.get(key) is False + assert SystemConfig.get_by_key(oper._db, key).value is False + + +@pytest.mark.asyncio +async def test_async_set_persists_falsy_value_on_existing_record(): + """异步写入假值同样应更新记录而不是删除记录。""" + key = _unique_key() + oper = _fresh_oper() + + oper.set(key, True) + assert await oper.async_set(key, False) is True + assert oper.get(key) is False + assert SystemConfig.get_by_key(oper._db, key).value is False + + +@pytest.mark.asyncio +async def test_async_set_creates_record_for_falsy_value(): + """异步写入假值且无记录时应创建记录。""" + key = _unique_key() + oper = _fresh_oper() + + assert await oper.async_set(key, 0) is True + assert oper.get(key) == 0 + assert SystemConfig.get_by_key(oper._db, key).value == 0 + + +def test_delete_removes_record_explicitly(): + """显式 delete 仍是删除配置的唯一途径。""" + key = _unique_key() + oper = _fresh_oper() + + oper.set(key, False) + assert oper.delete(key) is True + assert oper.get(key) is None + assert SystemConfig.get_by_key(oper._db, key) is None + + +def test_mounted_local_disk_delete_empty_dirs_off_is_persisted(): + """回归:挂载盘删除空目录开关关闭后,读取端不应回落默认开启(issue #6309)。""" + key = SystemConfigKey.MountedLocalDiskDeleteEmptyDirs.value + oper = _fresh_oper() + + # 模拟前端保存关闭:已有记录(开启)→ 关闭 + oper.set(key, True) + oper.set(key, False) + # 复刻 app/chain/transfer.py 的读取语义:无记录视为默认开启 + assert (oper.get(key) is not False) is False + + # 模拟全新安装首次保存关闭:无记录 → 创建关闭记录 + Singleton._instances.pop((SystemConfigOper, (), frozenset()), None) + oper.delete(key) + _fresh_oper().set(key, False) + assert _fresh_oper().get(key) is False