From 52ca375f3d9bc000984f92c437f993133c13f50f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 3 Aug 2026 08:45:35 +0800 Subject: [PATCH] fix(history): stabilize download history pagination --- app/api/endpoints/history.py | 2 +- app/db/models/downloadhistory.py | 15 +++++- skills/moviepilot-api/SKILL.md | 2 +- tests/test_history_search.py | 89 ++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py index e4d451e7..a6e3622c 100644 --- a/app/api/endpoints/history.py +++ b/app/api/endpoints/history.py @@ -140,7 +140,7 @@ async def download_history( _: schemas.TokenPayload = Depends(verify_token), ) -> Any: """ - 查询下载历史记录 + 按下载时间倒序查询下载历史记录 """ return await DownloadHistory.async_list_by_page(db, page, count) diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index efe91c06..c39b6b84 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -148,14 +148,25 @@ class DownloadHistory(Base): def list_by_page( cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30 ): - return db.query(DownloadHistory).offset((page - 1) * count).limit(count).all() + return ( + db.query(DownloadHistory) + .order_by(DownloadHistory.date.desc(), DownloadHistory.id.desc()) + .offset((page - 1) * count) + .limit(count) + .all() + ) @classmethod @async_db_query async def async_list_by_page( cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30 ): - result = await db.execute(select(cls).offset((page - 1) * count).limit(count)) + result = await db.execute( + select(cls) + .order_by(cls.date.desc(), cls.id.desc()) + .offset((page - 1) * count) + .limit(count) + ) return result.scalars().all() @classmethod diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index 376f80ae..17439c6e 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -251,7 +251,7 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business | Method | Path | Description | |--------|------|-------------| -| GET | `/api/v1/history/download` | Download history. Params: `page`, `count` | +| GET | `/api/v1/history/download` | Download history, newest first. Params: `page`, `count` | | DELETE | `/api/v1/history/download` | Delete download history. Body: DownloadHistory JSON | | GET | `/api/v1/history/transfer` | Transfer history. Params: `title`, `page`, `count`, `status` | | DELETE | `/api/v1/history/transfer` | Delete transfer history. Params: `deletesrc`, `deletedest`. Body: TransferHistory | diff --git a/tests/test_history_search.py b/tests/test_history_search.py index 3e174f4c..58e97b35 100644 --- a/tests/test_history_search.py +++ b/tests/test_history_search.py @@ -98,3 +98,92 @@ def test_download_history_title_search_is_case_insensitive(tmp_path: Path): await engine.dispose() asyncio.run(run_case()) + + +def test_download_history_page_is_newest_first(tmp_path: Path): + """下载历史分页应按时间和 ID 倒序稳定返回。""" + engine = create_engine(f"sqlite:///{tmp_path / 'download_history_page.db'}") + SessionFactory = sessionmaker(bind=engine) + Base.metadata.create_all(bind=engine) + + try: + with SessionFactory() as db: + db.add_all( + [ + DownloadHistory( + path="/downloads/oldest", + type="电影", + title="Oldest", + date="2026-06-01 00:00:00", + ), + DownloadHistory( + path="/downloads/newer-first", + type="电影", + title="Newer First", + date="2026-06-02 00:00:00", + ), + DownloadHistory( + path="/downloads/newer-second", + type="电影", + title="Newer Second", + date="2026-06-02 00:00:00", + ), + ] + ) + db.commit() + + first_page = DownloadHistory.list_by_page(db, page=1, count=2) + second_page = DownloadHistory.list_by_page(db, page=2, count=2) + + assert [item.title for item in first_page] == ["Newer Second", "Newer First"] + assert [item.title for item in second_page] == ["Oldest"] + finally: + engine.dispose() + + +def test_async_download_history_page_is_newest_first(tmp_path: Path): + """异步下载历史分页应按时间和 ID 倒序稳定返回。""" + + async def run_case(): + """执行异步分页顺序断言。""" + engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'async_download_history_page.db'}") + SessionFactory = async_sessionmaker(bind=engine) + + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + async with SessionFactory() as db: + db.add_all( + [ + DownloadHistory( + path="/downloads/oldest", + type="电影", + title="Oldest", + date="2026-06-01 00:00:00", + ), + DownloadHistory( + path="/downloads/newer-first", + type="电影", + title="Newer First", + date="2026-06-02 00:00:00", + ), + DownloadHistory( + path="/downloads/newer-second", + type="电影", + title="Newer Second", + date="2026-06-02 00:00:00", + ), + ] + ) + await db.commit() + + first_page = await DownloadHistory.async_list_by_page(db, page=1, count=2) + second_page = await DownloadHistory.async_list_by_page(db, page=2, count=2) + + assert [item.title for item in first_page] == ["Newer Second", "Newer First"] + assert [item.title for item in second_page] == ["Oldest"] + finally: + await engine.dispose() + + asyncio.run(run_case())