perf: 优化 MySQL 查询并接入 pytest
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Add composite indexes for task polling, ordering, and retention queries.
|
||||
|
||||
Revision ID: 20260830_0031
|
||||
Revises: 20260829_0030
|
||||
Create Date: 2026-08-30
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260830_0031"
|
||||
down_revision: Union[str, None] = "20260829_0030"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
INDEXES = (
|
||||
# /api/douyu/tasks filters by scope and returns the newest rows first.
|
||||
("ix_douyu_tasks_handbook_scope_id", "douyu_tasks", ["handbook_scope", "id"]),
|
||||
# The legacy compatibility branch also filters by task_type.
|
||||
(
|
||||
"ix_douyu_tasks_handbook_scope_task_type_id",
|
||||
"douyu_tasks",
|
||||
["handbook_scope", "task_type", "id"],
|
||||
),
|
||||
# Supports terminal-task retention and finished-time ordered Cookie views.
|
||||
("ix_douyu_tasks_status_finished_at_id", "douyu_tasks", ["status", "finished_at", "id"]),
|
||||
("ix_login_tasks_status_finished_at_id", "login_tasks", ["status", "finished_at", "id"]),
|
||||
)
|
||||
|
||||
|
||||
def _indexes(bind, table_name: str) -> set[str]:
|
||||
if not sa.inspect(bind).has_table(table_name):
|
||||
return set()
|
||||
return {index["name"] for index in sa.inspect(bind).get_indexes(table_name)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
for name, table_name, columns in INDEXES:
|
||||
if name not in _indexes(bind, table_name):
|
||||
op.create_index(name, table_name, columns)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
for name, table_name, _ in reversed(INDEXES):
|
||||
if name in _indexes(bind, table_name):
|
||||
op.drop_index(name, table_name=table_name)
|
||||
@@ -126,7 +126,7 @@ def list_accounts(
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
total = query.order_by(None).with_entities(func.count(Account.id)).scalar() or 0
|
||||
query = query.order_by(Account.id)
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
@@ -173,13 +173,18 @@ def accounts_summary(
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="无权查看账号")
|
||||
|
||||
total = query.count()
|
||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
||||
total = query.with_entities(func.count(Account.id)).scalar() or 0
|
||||
assigned_count = (
|
||||
query.filter(Account.assigned_to.isnot(None))
|
||||
.with_entities(func.count(Account.id))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
tag_count = (
|
||||
query.filter(Account.tag != "", Account.tag.isnot(None))
|
||||
.with_entities(Account.tag)
|
||||
.distinct()
|
||||
.count()
|
||||
.with_entities(func.count(func.distinct(Account.tag)))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
return {
|
||||
"total": total,
|
||||
|
||||
@@ -5,7 +5,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import case, or_
|
||||
from sqlalchemy import case, func, or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
import io
|
||||
import csv
|
||||
@@ -145,7 +145,7 @@ def list_cookies(
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
total = query.order_by(None).with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
query = _order_cookie_tasks(query, selected_names)
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
@@ -209,8 +209,13 @@ def cookies_summary(
|
||||
query = _visible_cookie_tasks_query(db, current)
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
total = query.count()
|
||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
||||
total = query.with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
assigned_count = (
|
||||
query.filter(Account.assigned_to.isnot(None))
|
||||
.with_entities(func.count(LoginTask.id))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
return {
|
||||
"total": total,
|
||||
"assigned_count": assigned_count,
|
||||
@@ -242,7 +247,7 @@ def list_cookie_operations(
|
||||
Account.tag.ilike(f"%{search_text}%"),
|
||||
))
|
||||
|
||||
total = query.order_by(None).count()
|
||||
total = query.order_by(None).with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
tasks = (
|
||||
query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
|
||||
@@ -55,9 +55,14 @@ def dashboard_summary(
|
||||
"""返回首页所需的全部轻量统计。"""
|
||||
douyu_accounts = 0
|
||||
if user_has_permission(current, "account:view_all"):
|
||||
douyu_accounts = db.query(Account).count()
|
||||
douyu_accounts = db.query(func.count(Account.id)).scalar() or 0
|
||||
elif user_has_permission(current, "account:view_assigned"):
|
||||
douyu_accounts = db.query(Account).filter(Account.assigned_to == current.id).count()
|
||||
douyu_accounts = (
|
||||
db.query(func.count(Account.id))
|
||||
.filter(Account.assigned_to == current.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
login_tasks = _empty_task_summary()
|
||||
if any(user_has_permission(current, permission) for permission in (
|
||||
@@ -79,7 +84,7 @@ def dashboard_summary(
|
||||
)
|
||||
if not user_has_permission(current, "login:view_all"):
|
||||
cookie_query = cookie_query.filter(Account.assigned_to == current.id)
|
||||
cookies = cookie_query.count()
|
||||
cookies = cookie_query.with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
|
||||
huya_accounts = 0
|
||||
can_view_huya_accounts = any(user_has_permission(current, permission) for permission in (
|
||||
@@ -91,7 +96,7 @@ def dashboard_summary(
|
||||
huya_account_query = db.query(HuyaAccount)
|
||||
if not _can_view_huya_all(current):
|
||||
huya_account_query = huya_account_query.filter(HuyaAccount.assigned_to == current.id)
|
||||
huya_accounts = huya_account_query.count()
|
||||
huya_accounts = huya_account_query.with_entities(func.count(HuyaAccount.id)).scalar() or 0
|
||||
|
||||
huya_tasks = _empty_task_summary()
|
||||
huya_goods = 0
|
||||
@@ -101,8 +106,8 @@ def dashboard_summary(
|
||||
if not _can_view_huya_all(current):
|
||||
huya_task_query = huya_task_query.filter(HuyaAccount.assigned_to == current.id)
|
||||
huya_tasks = _task_summary(huya_task_query, HuyaTask)
|
||||
huya_goods = db.query(HuyaGoodsSnapshot).count()
|
||||
huya_recharge_goods = db.query(HuyaRechargeGoodsSnapshot).count()
|
||||
huya_goods = db.query(func.count(HuyaGoodsSnapshot.id)).scalar() or 0
|
||||
huya_recharge_goods = db.query(func.count(HuyaRechargeGoodsSnapshot.id)).scalar() or 0
|
||||
|
||||
return {
|
||||
"douyu": {
|
||||
|
||||
@@ -8,8 +8,8 @@ from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect
|
||||
from loguru import logger
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session, joinedload, load_only
|
||||
|
||||
from ..database import SessionLocal, get_db
|
||||
from ..deps import authenticate_websocket, get_current_user, require_permission
|
||||
@@ -152,7 +152,15 @@ def _visible_task_accounts_query(db: Session, current: User):
|
||||
|
||||
def _visible_tasks_query(db: Session, current: User):
|
||||
"""返回当前用户可查看的斗鱼任务查询。"""
|
||||
query = db.query(DouyuTask).options(joinedload(DouyuTask.account))
|
||||
# 任务列表只展示账号识别信息,避免 joinedload 把密码、邮箱等加密大字段带出。
|
||||
account_loader = joinedload(DouyuTask.account).load_only(
|
||||
Account.id,
|
||||
Account.username,
|
||||
Account.uid,
|
||||
Account.nickname,
|
||||
Account.assigned_to,
|
||||
)
|
||||
query = db.query(DouyuTask).options(account_loader)
|
||||
if _can_view_all(current):
|
||||
return query
|
||||
if user_has_permission(current, "account:view_assigned"):
|
||||
@@ -589,7 +597,13 @@ def list_tasks(
|
||||
))
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.enable_eagerloads(False).order_by(None).count()
|
||||
total = (
|
||||
query.enable_eagerloads(False)
|
||||
.order_by(None)
|
||||
.with_entities(func.count(DouyuTask.id))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
query = query.order_by(DouyuTask.id.desc())
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
@@ -5,7 +5,7 @@ import threading
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session, defer
|
||||
from sqlalchemy.orm import Session, defer, load_only
|
||||
|
||||
from ..database import get_db, SessionLocal
|
||||
from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel
|
||||
@@ -120,7 +120,12 @@ def list_tasks(
|
||||
account_ids = [t.account_id for t in rows]
|
||||
accounts_map = {}
|
||||
if account_ids:
|
||||
accs = db.query(Account).filter(Account.id.in_(account_ids)).all()
|
||||
accs = (
|
||||
db.query(Account)
|
||||
.options(load_only(Account.id, Account.username))
|
||||
.filter(Account.id.in_(account_ids))
|
||||
.all()
|
||||
)
|
||||
accounts_map = {a.id: a.username for a in accs}
|
||||
|
||||
result = []
|
||||
|
||||
Reference in New Issue
Block a user