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

@@ -6,6 +6,7 @@ import ipaddress
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
@@ -159,7 +160,12 @@ class ManagementTokenService:
ValueError: 如果名称已存在或超过数量限制
"""
# 检查用户 Token 数量限制
token_count = db.query(ManagementToken).filter(ManagementToken.user_id == user_id).count()
token_count = int(
db.query(func.count(ManagementToken.id))
.filter(ManagementToken.user_id == user_id)
.scalar()
or 0
)
max_tokens = config.management_token_max_per_user
if token_count >= max_tokens:
raise ValueError(f"已达到 Token 数量上限({max_tokens}")
@@ -245,7 +251,7 @@ class ManagementTokenService:
if is_active is not None:
query = query.filter(ManagementToken.is_active == is_active)
total = query.count()
total = int(query.with_entities(func.count(ManagementToken.id)).scalar() or 0)
tokens = query.order_by(ManagementToken.created_at.desc()).offset(skip).limit(limit).all()
return tokens, total

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
import asyncio
from sqlalchemy import and_
from sqlalchemy import and_, func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
@@ -255,14 +255,15 @@ class ModelService:
# 检查这是否是该 GlobalModel 的最后一个关联提供商
if model.global_model_id:
other_implementations = (
db.query(Model)
other_implementations = int(
db.query(func.count(Model.id))
.filter(
Model.global_model_id == model.global_model_id,
Model.id != model_id,
Model.is_active == True,
)
.count()
.scalar()
or 0
)
if other_implementations == 0:

View File

@@ -14,7 +14,7 @@ from typing import Any
from urllib.parse import urlparse
import httpx
from sqlalchemy import update
from sqlalchemy import func, update
from sqlalchemy.orm import Session
from src.core.exceptions import InvalidRequestException, NotFoundException
@@ -452,7 +452,7 @@ class ProxyNodeService:
raise InvalidRequestException(f"status 必须是以下之一: {sorted(allowed)}", "status")
query = query.filter(ProxyNode.status == ProxyNodeStatus(normalized))
total = query.count()
total = int(query.with_entities(func.count(ProxyNode.id)).scalar() or 0)
nodes = query.order_by(ProxyNode.name.asc()).offset(skip).limit(limit).all()
return nodes, total

View File

@@ -6,7 +6,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import or_
from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from src.core.exceptions import ForbiddenException, NotFoundException
@@ -87,7 +87,7 @@ class AnnouncementService:
)
# 分页
total = query.count()
total = int(query.with_entities(func.count(Announcement.id)).scalar() or 0)
announcements = query.offset(offset).limit(limit).all()
# 获取已读状态

View File

@@ -338,8 +338,8 @@ class AuditService:
)
# 获取最近的可疑活动
recent_suspicious = (
db.query(AuditLog)
recent_suspicious = int(
db.query(func.count(AuditLog.id))
.filter(
AuditLog.user_id == user_id,
AuditLog.event_type.in_(
@@ -350,7 +350,8 @@ class AuditService:
),
AuditLog.created_at >= cutoff_time,
)
.count()
.scalar()
or 0
)
return {

View File

@@ -116,8 +116,8 @@ class SystemConfigService:
"description": "详细日志保留天数,超过此天数后压缩 request_body 和 response_body 到压缩字段",
},
"compressed_log_retention_days": {
"value": 90,
"description": "压缩日志保留天数,超过此天数后删除压缩的 body 字段保留headers和统计",
"value": 30,
"description": "压缩记录保留天数,超过此天数后删除压缩的 body 字段保留headers和统计",
},
"header_retention_days": {
"value": 90,
@@ -125,7 +125,7 @@ class SystemConfigService:
},
"log_retention_days": {
"value": 365,
"description": "完整日志保留天数,超过此天数后删除整条记录(保留核心统计)",
"description": "请求记录保存天数,超过此天数后删除整条使用记录",
},
"enable_auto_cleanup": {
"value": True,

View File

@@ -1052,7 +1052,7 @@ class MaintenanceScheduler:
# 获取配置参数
detail_retention = SystemConfigService.get_config(db, "detail_log_retention_days", 7)
compressed_retention = SystemConfigService.get_config(
db, "compressed_log_retention_days", 90
db, "compressed_log_retention_days", 30
)
header_retention = SystemConfigService.get_config(db, "header_retention_days", 90)
log_retention = SystemConfigService.get_config(db, "log_retention_days", 365)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import func
from sqlalchemy.orm import Session
from src.core.api_format.metadata import can_passthrough_endpoint
@@ -104,13 +105,14 @@ class UsageActiveRequestsMixin:
"""
cutoff_time = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
return (
db.query(Usage)
return int(
db.query(func.count(Usage.id))
.filter(
Usage.status.in_(["pending", "streaming"]),
Usage.created_at < cutoff_time,
)
.count()
.scalar()
or 0
)
@classmethod

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import func
from sqlalchemy import case, func
from sqlalchemy.orm import Session
from src.core.logger import logger
@@ -239,6 +239,21 @@ class UsageQueryMixin:
func.sum(Usage.total_tokens).label("total_tokens"),
func.sum(Usage.total_cost_usd).label("total_cost_usd"),
func.avg(Usage.response_time_ms).label("avg_response_time"),
func.sum(
case(
(
(Usage.status_code == 200) & Usage.response_time_ms.isnot(None),
Usage.response_time_ms,
),
else_=0,
)
).label("success_response_time_sum"),
func.sum(
case(
((Usage.status_code == 200) & Usage.response_time_ms.isnot(None), 1),
else_=0,
)
).label("success_response_time_count"),
)
# 过滤掉 pending/streaming 状态的请求(与上方明细查询一致)
@@ -268,6 +283,8 @@ class UsageQueryMixin:
"avg_response_time_ms": (
float(row.avg_response_time) if row.avg_response_time else 0
),
"success_response_time_sum_ms": float(row.success_response_time_sum or 0.0),
"success_response_time_count": int(row.success_response_time_count or 0),
}
for row in summary
]

View File

@@ -266,7 +266,9 @@ class UserService:
db.query(AnnouncementRead).filter(AnnouncementRead.user_id == user_id).delete(
synchronize_session=False
)
api_key_count = db.query(ApiKey).filter(ApiKey.user_id == user_id).count()
api_key_count = int(
db.query(func.count(ApiKey.id)).filter(ApiKey.user_id == user_id).scalar() or 0
)
db.query(ApiKey).filter(ApiKey.user_id == user_id).delete(synchronize_session=False)
# 现在删除用户Usage, AuditLog, RequestAttempt 会通过数据库 SET NULL 保留)