Compare commits

...

1 Commits

Author SHA1 Message Date
xiaoxia 8612e183ad fix(config): 运行时读取 SessionLocal 修复后台配置接口500 #2246 (#2248)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 1m4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m7s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m22s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m27s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m19s
AI Code Review / AI Code Review (pull_request) Successful in 7m17s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m43s
CI/CD Pipeline / Integration Tests (push) Successful in 11m37s
CI/CD Pipeline / Validate - Style (push) Successful in 13m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 15m3s
CI/CD Pipeline / Unit Tests (push) Failing after 18m32s
CI/CD Pipeline / Validate - Security (push) Successful in 31m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 25m44s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
## 背景
#2247 合并部署到 staging 后,`GET /api/v1/admin/ditto-emotion/config` 返回 500。

## 根因
`system_config_service.py` 在**模块导入时**执行 `from ...session import SessionLocal`,此刻 `SessionLocal` 还是 `None`;之后 `initialize_database()` 才给它赋值。import 时绑定把旧的 `None` 固化,服务 fallback 取到的始终是 None,抛出 `RuntimeError(数据库会话工厂未初始化)`。

## 修复
- 改为 `from packages.adapters.sqlalchemy_impl import session as db_session`,运行时读取 `db_session.SessionLocal`。
- 补 2 个回归用例:模拟「导入时 None、启动后才赋值」的真实启动顺序;无工厂时抛 RuntimeError。

## 测试
- `tests/unit/test_system_config.py`:**29 passed**(原 27 + 新增 2)。
- black/isort/ruff 通过。

关联 #2246。
2026-10-08 23:04:59 +08:00
2 changed files with 35 additions and 2 deletions
@@ -12,7 +12,7 @@ import logging
import threading
from typing import Any
from packages.adapters.sqlalchemy_impl.session import SessionLocal
from packages.adapters.sqlalchemy_impl import session as db_session
from packages.adapters.sqlalchemy_impl.system_setting_repository import (
SQLAlchemySystemSettingRepository,
)
@@ -35,7 +35,9 @@ class SystemConfigService:
# ── 会话 ─────────────────────────────────────────────────────
def _get_session_factory(self):
factory = self._session_factory or SessionLocal
# 必须运行时读取模块属性:模块导入时 SessionLocal 还是 None,
# initialize_database() 之后才被赋值,import 时绑定会拿到旧值。
factory = self._session_factory or db_session.SessionLocal
if factory is None:
raise RuntimeError("数据库会话工厂未初始化")
return factory
+31
View File
@@ -91,6 +91,37 @@ class TestConfigService:
def test_default_when_missing(self, config_service):
assert config_service.get_config("not_exist", "fallback") == "fallback"
def test_uses_module_session_factory_assigned_after_import(self):
# 回归:模块导入时 session.SessionLocal 为 None,initialize_database()
# 之后才赋值;服务必须运行时读取模块属性,而不是 import 时绑定旧值。
from packages.adapters.sqlalchemy_impl import session as db_session
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine)
writer = SystemConfigService(session_factory=factory)
writer.set_config("late_key", 7, setting_type=SETTING_TYPE_INT)
reader = SystemConfigService() # 不注入工厂,依赖模块级 SessionLocal
old = db_session.SessionLocal
try:
db_session.SessionLocal = factory
assert reader.get_config("late_key", 0) == 7
finally:
db_session.SessionLocal = old
def test_raises_when_no_session_factory(self):
from packages.adapters.sqlalchemy_impl import session as db_session
svc = SystemConfigService()
old = db_session.SessionLocal
try:
db_session.SessionLocal = None
with pytest.raises(RuntimeError):
svc.get_config("anything", 1)
finally:
db_session.SessionLocal = old
def test_db_overrides_default(self, config_service):
config_service.set_config("k", 20, setting_type=SETTING_TYPE_INT)
# 再次读取应命中 DB 值,而非传入的默认