From 5db96744906f7ff1c199d619dcceca16309d082d Mon Sep 17 00:00:00 2001 From: gr Date: Fri, 31 Jul 2026 16:37:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 36 ++++++++++++++++++++++++++++++------ backend/models/db_migrate.py | 6 ++++++ frontend/src/views/Logs.vue | 23 ++++++++++++++++++----- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/backend/main.py b/backend/main.py index 098bead..cf6f1c1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2567,14 +2567,25 @@ async def get_logs( if account_id is not None: await get_owned_account(db, user, account_id) limit = max(1, min(int(limit or 50), 500)) - stmt = ( + # Fetch only primary keys while MySQL performs the cross-account sort. + # Selecting the full ORM row here includes MEDIUMTEXT/TEXT columns, which + # makes MySQL 5.7 materialize a huge on-disk temporary table for non-admin + # users. Under polling load that grew ibtmp1 by tens of gigabytes. + id_stmt = ( logs_for_user(user, account_id) + .with_only_columns(MessageLog.id) .order_by(MessageLog.created_at.desc()) .offset(max(0, int(offset or 0))) .limit(limit) ) - result = await db.execute(stmt) - return result.scalars().all() + ordered_ids = list((await db.execute(id_stmt)).scalars().all()) + if not ordered_ids: + return [] + rows = ( + await db.execute(select(MessageLog).where(MessageLog.id.in_(ordered_ids))) + ).scalars().all() + rows_by_id = {row.id: row for row in rows} + return [rows_by_id[row_id] for row_id in ordered_ids if row_id in rows_by_id] @app.get("/api/received-messages", response_model=List[ReceivedMessageLogResponse]) @@ -2588,13 +2599,26 @@ async def get_received_messages( if account_id is not None: await get_owned_account(db, user, account_id) limit = max(1, min(int(limit or 100), 500)) - stmt = ( + # Keep the global sort narrow for the same reason as /api/logs. raw_content + # can be large and must only be loaded after LIMIT has selected the IDs. + id_stmt = ( received_logs_for_user(user, account_id) + .with_only_columns(ReceivedMessageLog.id) .order_by(ReceivedMessageLog.created_at.desc()) .limit(limit) ) - result = await db.execute(stmt) - return result.scalars().all() + ordered_ids = list((await db.execute(id_stmt)).scalars().all()) + if not ordered_ids: + return [] + rows = ( + await db.execute( + select(ReceivedMessageLog).where( + ReceivedMessageLog.id.in_(ordered_ids) + ) + ) + ).scalars().all() + rows_by_id = {row.id: row for row in rows} + return [rows_by_id[row_id] for row_id in ordered_ids if row_id in rows_by_id] @app.get("/api/system-logs", response_model=List[SystemLogResponse]) diff --git a/backend/models/db_migrate.py b/backend/models/db_migrate.py index 34a66e8..898aa4d 100644 --- a/backend/models/db_migrate.py +++ b/backend/models/db_migrate.py @@ -126,6 +126,12 @@ def migrate_accounts_table(conn) -> None: "douyin_uid", {"default": "ALTER TABLE accounts ADD COLUMN douyin_uid VARCHAR(64)"}, ) + add_index_if_missing( + conn, + "accounts", + "ix_accounts_owner_id", + ("owner_id",), + ) def migrate_account_videos_table(conn) -> None: diff --git a/frontend/src/views/Logs.vue b/frontend/src/views/Logs.vue index 7ed090b..3e44c94 100644 --- a/frontend/src/views/Logs.vue +++ b/frontend/src/views/Logs.vue @@ -39,9 +39,23 @@ const LOGS_PAGE_SIZE = 20 const hasMoreLogs = ref(true) const loadingMore = ref(false) let pollTimer = null +let pollStopped = false // 上次日志数据的签名;轮询时数据没变化就跳过赋值,避免无谓的重算与重渲染 let lastLogsSignature = '' +// Use a self-scheduling timeout instead of setInterval. A slow request must +// finish before the next poll is scheduled, otherwise overlapping requests can +// exhaust the database pool and amplify one slow query into dozens. +const scheduleLogPoll = () => { + if (pollStopped) return + pollTimer = setTimeout(async () => { + if (!document.hidden) { + await fetchLogs(true) + } + scheduleLogPoll() + }, POLL_INTERVAL_MS) +} + const dedupeMessages = (messages) => { const map = new Map() for (const item of messages) { @@ -593,10 +607,8 @@ onMounted(async () => { } await fetchAccounts() await fetchLogs() - pollTimer = setInterval(() => { - if (document.hidden) return - fetchLogs(true) - }, POLL_INTERVAL_MS) + pollStopped = false + scheduleLogPoll() }) watch( @@ -613,8 +625,9 @@ watch( ) onUnmounted(() => { + pollStopped = true if (pollTimer) { - clearInterval(pollTimer) + clearTimeout(pollTimer) pollTimer = null } })