perf: 全栈查询优化、前端缓存去重与页面可见性优化

后端:
- SQL count 查询统一改用 func.count() 子查询替代 query.count()
- Dashboard/Audit 等页面多次独立查询合并为单次聚合查询
- Provider summary 列表改为批量查询消除 N+1 问题
- DailyStats 逐天循环查询改为 CASE 分桶单次查询
- 使用 load_only() 减少不必要的列加载
- cache_decorator 支持嵌套属性路径解析(dotted vary_by)
- 多个管理/公共端点新增 @cache_result 缓存装饰

前端:
- cache.ts 新增 in-flight 请求复用、dedupedRequest、buildCacheKey
- 大量 API 调用添加前端缓存或去重
- 多个页面定时器在标签页隐藏时暂停、可见时恢复
- Auth 检查从 setInterval 改为 storage + visibilitychange 事件驱动
- 请求竞态防护(requestId 模式)

数据库:
- Usage 表新增 idx_usage_status_user_created 复合索引
This commit is contained in:
fawney19
2026-03-03 22:04:40 +08:00
parent 0a60492146
commit 97b0146ce9
66 changed files with 2306 additions and 657 deletions

View File

@@ -18,11 +18,13 @@ from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import ApiRequestPipeline
from src.api.dashboard.routes import DashboardAdapter
from src.clients.http_client import HTTPClientPool
from src.config.constants import CacheTTL
from src.core.crypto import crypto_service
from src.core.enums import UserRole
from src.core.logger import logger
from src.database import get_db
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint, User, VideoTask
from src.utils.cache_decorator import cache_result
router = APIRouter(prefix="/api/admin/video-tasks", tags=["Admin - Video Tasks"])
pipeline = ApiRequestPipeline()
@@ -247,6 +249,12 @@ class VideoTaskListAdapter(DashboardAdapter):
page: int
page_size: int
@cache_result(
key_prefix="admin:video_tasks:list",
ttl=min(5, CacheTTL.ADMIN_USAGE_RECORDS),
user_specific=True,
vary_by=["status", "user_id", "model", "page", "page_size"],
)
async def handle(self, context: ApiRequestContext) -> Any:
db = context.db
user = context.user
@@ -270,8 +278,8 @@ class VideoTaskListAdapter(DashboardAdapter):
escaped = self.model.replace("%", "\\%").replace("_", "\\_")
query = query.filter(VideoTask.model.ilike(f"%{escaped}%"))
# 统计总数
total = query.count()
# 统计总数(避免 Query.count() 生成大子查询)
total = int(query.with_entities(func.count(VideoTask.id)).scalar() or 0)
# 分页
offset = (self.page - 1) * self.page_size
@@ -341,6 +349,11 @@ class VideoTaskListAdapter(DashboardAdapter):
class VideoTaskStatsAdapter(DashboardAdapter):
"""视频任务统计适配器"""
@cache_result(
key_prefix="admin:video_tasks:stats",
ttl=min(5, CacheTTL.ADMIN_USAGE_RECORDS),
user_specific=True,
)
async def handle(self, context: ApiRequestContext) -> Any:
db = context.db
user = context.user
@@ -350,8 +363,8 @@ class VideoTaskStatsAdapter(DashboardAdapter):
if not is_admin:
base_query = base_query.filter(VideoTask.user_id == user.id)
# 总数
total = base_query.count()
# 总数(避免 Query.count() 生成大子查询)
total = int(base_query.with_entities(func.count(VideoTask.id)).scalar() or 0)
# 按状态分组
status_stats = (
@@ -379,7 +392,12 @@ class VideoTaskStatsAdapter(DashboardAdapter):
# 今日任务数
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
today_count = base_query.filter(VideoTask.created_at >= today).count()
today_count = int(
base_query.filter(VideoTask.created_at >= today)
.with_entities(func.count(VideoTask.id))
.scalar()
or 0
)
# 管理员额外统计
result = {