From fae87d68bff0face997612be8c003878e3e1ed73 Mon Sep 17 00:00:00 2001 From: freeman Date: Fri, 4 Sep 2026 08:55:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(history):=20=E8=AF=B7=E6=B1=82=E7=BA=A7?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=8E=86=E5=8F=B2=E4=BB=93=E5=82=A8=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20async=5Fcount=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=8E=86=E5=8F=B2=E6=8E=A5=E5=8F=A3=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HistoryQueryService.count_download() 依赖 AsyncDownloadHistoryQueryRepository 协议的 async_count(),但 startup 注入的请求级适配器 SessionDownloadHistoryRepository 只实现了 async_list_by_page(),导致 GET /api/v1/history/download 在写入分页总数响应头时抛 AttributeError 并返回 500, Web 端下载历史页面无法加载。 为 SessionDownloadHistoryRepository 补齐 async_count(),在请求持有的 AsyncSession 内调用 DownloadHistoryOper.async_count(),Session 类型校验与 async_list_by_page() 保持一致,并补充回归测试。 --- app/db/adapters/history/download.py | 6 +++++ tests/test_transactional_download_history.py | 24 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/db/adapters/history/download.py b/app/db/adapters/history/download.py index 163846cbd..e4d0ea2dd 100644 --- a/app/db/adapters/history/download.py +++ b/app/db/adapters/history/download.py @@ -287,6 +287,12 @@ class SessionDownloadHistoryRepository: ) return [_project_history(record) for record in records] + async def async_count(self) -> int: + """在请求异步 Session 内统计下载历史总数。""" + if not isinstance(self._session, AsyncSession): + raise RuntimeError("下载历史异步统计需要 AsyncSession") + return await DownloadHistoryOper(self._session).async_count() + def stage_delete_history(self, history_id: int) -> None: """在请求同步 Session 内暂存下载历史删除。""" if not isinstance(self._session, Session): diff --git a/tests/test_transactional_download_history.py b/tests/test_transactional_download_history.py index e1979c6aa..2af55b8ed 100644 --- a/tests/test_transactional_download_history.py +++ b/tests/test_transactional_download_history.py @@ -205,3 +205,27 @@ def test_session_repository_obeys_caller_transaction(db) -> None: repository.stage_delete_history(history.id) session.commit() assert _repository().get_by_hash("request-history-hash") is None + + +def test_session_repository_counts_history_in_caller_async_session(db) -> None: + """请求级 adapter 在调用方 AsyncSession 内统计总数,与分页读取口径一致。""" + repository = _repository() + baseline_count = asyncio.run(repository.async_count()) + history_id = repository.add(_history_write(download_hash="request-async-count-hash")) + + async def exercise() -> tuple[int, list[DownloadHistorySnapshot]]: + """在同一请求 AsyncSession 内先统计总数再分页读取。""" + async with async_session_scope() as session: + session_repository = SessionDownloadHistoryRepository(session) + total = await session_repository.async_count() + records = await session_repository.async_list_by_page(count=10) + return total, records + + total, records = asyncio.run(exercise()) + + assert total == baseline_count + 1 + assert any(record.id == history_id for record in records) + + with SessionFactory() as session: + with pytest.raises(RuntimeError): + asyncio.run(SessionDownloadHistoryRepository(session).async_count())