perf: 优化 MySQL 查询并接入 pytest
This commit is contained in:
@@ -42,6 +42,10 @@ MYSQL_PASSWORD=
|
|||||||
MYSQL_ROOT_PASSWORD=
|
MYSQL_ROOT_PASSWORD=
|
||||||
MYSQL_BIND_HOST=127.0.0.1
|
MYSQL_BIND_HOST=127.0.0.1
|
||||||
MYSQL_HOST_PORT=3307
|
MYSQL_HOST_PORT=3307
|
||||||
|
# MySQL 性能观测;同机部署资源紧张时可先使用 256M。
|
||||||
|
MYSQL_INNODB_BUFFER_POOL_SIZE=512M
|
||||||
|
MYSQL_SLOW_QUERY_LOG=1
|
||||||
|
MYSQL_LONG_QUERY_TIME=1
|
||||||
|
|
||||||
# 本地调试后端连接 MySQL(./dev.sh 使用;默认连接上面的宿主机端口)
|
# 本地调试后端连接 MySQL(./dev.sh 使用;默认连接上面的宿主机端口)
|
||||||
DEV_DB_HOST=127.0.0.1
|
DEV_DB_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ services:
|
|||||||
command:
|
command:
|
||||||
- --character-set-server=utf8mb4
|
- --character-set-server=utf8mb4
|
||||||
- --collation-server=utf8mb4_0900_ai_ci
|
- --collation-server=utf8mb4_0900_ai_ci
|
||||||
|
# 业务表已超过 300MiB;可通过 .env 调整,默认给 MySQL 512MiB 缓冲池。
|
||||||
|
- --innodb-buffer-pool-size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
||||||
|
# 默认记录 1 秒以上查询,便于验证查询优化;稳定后可设 MYSQL_SLOW_QUERY_LOG=0。
|
||||||
|
- --slow-query-log=${MYSQL_SLOW_QUERY_LOG:-1}
|
||||||
|
- --long-query-time=${MYSQL_LONG_QUERY_TIME:-1}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
|
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -34,3 +34,11 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["core", "utils", "web"]
|
packages = ["core", "utils", "web"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.1.1",
|
||||||
|
]
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "douyu-login-py"
|
name = "douyu-login-py"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
@@ -252,6 +252,11 @@ dependencies = [
|
|||||||
{ name = "websockets" },
|
{ name = "websockets" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.18.4" },
|
{ name = "alembic", specifier = ">=1.18.4" },
|
||||||
@@ -277,6 +282,9 @@ requires-dist = [
|
|||||||
{ name = "websockets", specifier = ">=16.0" },
|
{ name = "websockets", specifier = ">=16.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=9.1.1" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecdsa"
|
name = "ecdsa"
|
||||||
version = "0.19.2"
|
version = "0.19.2"
|
||||||
@@ -362,6 +370,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "loguru"
|
name = "loguru"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -489,6 +506,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "7.35.1"
|
version = "7.35.1"
|
||||||
@@ -595,6 +621,15 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/8e/aedef81641c8dca6fd0fb7294de5bed9c45f3397d67fddf755c1042c2642/PyExecJS-1.5.1.tar.gz", hash = "sha256:34cc1d070976918183ff7bdc0ad71f8157a891c92708c00c5fbbff7a769f505c", size = 13344, upload-time = "2018-01-18T04:33:55.126Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/8e/aedef81641c8dca6fd0fb7294de5bed9c45f3397d67fddf755c1042c2642/PyExecJS-1.5.1.tar.gz", hash = "sha256:34cc1d070976918183ff7bdc0ad71f8157a891c92708c00c5fbbff7a769f505c", size = 13344, upload-time = "2018-01-18T04:33:55.126Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pymysql"
|
name = "pymysql"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -613,6 +648,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|||||||
@@ -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
|
total = None
|
||||||
if page is not 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)
|
query = query.order_by(Account.id)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||||
@@ -173,13 +173,18 @@ def accounts_summary(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=403, detail="无权查看账号")
|
raise HTTPException(status_code=403, detail="无权查看账号")
|
||||||
|
|
||||||
total = query.count()
|
total = query.with_entities(func.count(Account.id)).scalar() or 0
|
||||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
assigned_count = (
|
||||||
|
query.filter(Account.assigned_to.isnot(None))
|
||||||
|
.with_entities(func.count(Account.id))
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
tag_count = (
|
tag_count = (
|
||||||
query.filter(Account.tag != "", Account.tag.isnot(None))
|
query.filter(Account.tag != "", Account.tag.isnot(None))
|
||||||
.with_entities(Account.tag)
|
.with_entities(func.count(func.distinct(Account.tag)))
|
||||||
.distinct()
|
.scalar()
|
||||||
.count()
|
or 0
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy import case, or_
|
from sqlalchemy import case, func, or_
|
||||||
from sqlalchemy.orm import Session, defer, joinedload
|
from sqlalchemy.orm import Session, defer, joinedload
|
||||||
import io
|
import io
|
||||||
import csv
|
import csv
|
||||||
@@ -145,7 +145,7 @@ def list_cookies(
|
|||||||
|
|
||||||
total = None
|
total = None
|
||||||
if page is not 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)
|
query = _order_cookie_tasks(query, selected_names)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||||
@@ -209,8 +209,13 @@ def cookies_summary(
|
|||||||
query = _visible_cookie_tasks_query(db, current)
|
query = _visible_cookie_tasks_query(db, current)
|
||||||
if user_has_permission(current, "login:view_all"):
|
if user_has_permission(current, "login:view_all"):
|
||||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
total = query.count()
|
total = query.with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
assigned_count = (
|
||||||
|
query.filter(Account.assigned_to.isnot(None))
|
||||||
|
.with_entities(func.count(LoginTask.id))
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
"assigned_count": assigned_count,
|
"assigned_count": assigned_count,
|
||||||
@@ -242,7 +247,7 @@ def list_cookie_operations(
|
|||||||
Account.tag.ilike(f"%{search_text}%"),
|
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 = (
|
tasks = (
|
||||||
query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||||
.offset((page - 1) * page_size)
|
.offset((page - 1) * page_size)
|
||||||
|
|||||||
@@ -55,9 +55,14 @@ def dashboard_summary(
|
|||||||
"""返回首页所需的全部轻量统计。"""
|
"""返回首页所需的全部轻量统计。"""
|
||||||
douyu_accounts = 0
|
douyu_accounts = 0
|
||||||
if user_has_permission(current, "account:view_all"):
|
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"):
|
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()
|
login_tasks = _empty_task_summary()
|
||||||
if any(user_has_permission(current, permission) for permission in (
|
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"):
|
if not user_has_permission(current, "login:view_all"):
|
||||||
cookie_query = cookie_query.filter(Account.assigned_to == current.id)
|
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
|
huya_accounts = 0
|
||||||
can_view_huya_accounts = any(user_has_permission(current, permission) for permission in (
|
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)
|
huya_account_query = db.query(HuyaAccount)
|
||||||
if not _can_view_huya_all(current):
|
if not _can_view_huya_all(current):
|
||||||
huya_account_query = huya_account_query.filter(HuyaAccount.assigned_to == current.id)
|
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_tasks = _empty_task_summary()
|
||||||
huya_goods = 0
|
huya_goods = 0
|
||||||
@@ -101,8 +106,8 @@ def dashboard_summary(
|
|||||||
if not _can_view_huya_all(current):
|
if not _can_view_huya_all(current):
|
||||||
huya_task_query = huya_task_query.filter(HuyaAccount.assigned_to == current.id)
|
huya_task_query = huya_task_query.filter(HuyaAccount.assigned_to == current.id)
|
||||||
huya_tasks = _task_summary(huya_task_query, HuyaTask)
|
huya_tasks = _task_summary(huya_task_query, HuyaTask)
|
||||||
huya_goods = db.query(HuyaGoodsSnapshot).count()
|
huya_goods = db.query(func.count(HuyaGoodsSnapshot.id)).scalar() or 0
|
||||||
huya_recharge_goods = db.query(HuyaRechargeGoodsSnapshot).count()
|
huya_recharge_goods = db.query(func.count(HuyaRechargeGoodsSnapshot.id)).scalar() or 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"douyu": {
|
"douyu": {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import func, or_
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload, load_only
|
||||||
|
|
||||||
from ..database import SessionLocal, get_db
|
from ..database import SessionLocal, get_db
|
||||||
from ..deps import authenticate_websocket, get_current_user, require_permission
|
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):
|
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):
|
if _can_view_all(current):
|
||||||
return query
|
return query
|
||||||
if user_has_permission(current, "account:view_assigned"):
|
if user_has_permission(current, "account:view_assigned"):
|
||||||
@@ -589,7 +597,13 @@ def list_tasks(
|
|||||||
))
|
))
|
||||||
total = None
|
total = None
|
||||||
if page is not 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())
|
query = query.order_by(DouyuTask.id.desc())
|
||||||
if page is not None:
|
if page is not None:
|
||||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import threading
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
from sqlalchemy import func
|
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 ..database import get_db, SessionLocal
|
||||||
from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel
|
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]
|
account_ids = [t.account_id for t in rows]
|
||||||
accounts_map = {}
|
accounts_map = {}
|
||||||
if account_ids:
|
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}
|
accounts_map = {a.id: a.username for a in accs}
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|||||||
Reference in New Issue
Block a user