refactor(cost,perf): 成本字段 Float 改 Numeric 并优化多处查询性能

- 数据库所有 cost/price 字段从 Float 改为 Numeric(20,8),解决浮点精度问题
- API 响应中 Decimal 值统一用 float() 转换确保 JSON 序列化
- 路由中手动 commit 后标记 tx_committed_by_route 防止中间件重复提交
- 多处查询优化:SQL 聚合替代 Python 遍历、load_only/defer 减少字段加载、
  批量 DELETE 替代逐条 ORM 删除、N+1 查询消除、UNION ALL 合并多表日期查询
- 新增 provider_api_keys (provider_id, is_active) 复合索引
- 候选构建热路径 defer 冷字段,钱包扣费合并解析与加锁查询
This commit is contained in:
fawney19
2026-03-08 16:44:16 +08:00
parent bd3f73c2fc
commit 48d13762d9
25 changed files with 474 additions and 171 deletions

View File

@@ -16,7 +16,8 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy.orm import Session
from sqlalchemy import func
from sqlalchemy.orm import Session, load_only
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
@@ -360,18 +361,35 @@ class SetRPMLimitAdapter(AdminApiAdapter):
class AdaptiveSummaryAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
# 自适应模式rpm_limit = NULL
adaptive_keys = (
context.db.query(ProviderAPIKey).filter(ProviderAPIKey.rpm_limit.is_(None)).all()
db = context.db
is_adaptive = ProviderAPIKey.rpm_limit.is_(None)
# SQL 聚合获取 count / sum避免全表 ORM 加载
total_keys, total_concurrent_429, total_rpm_429 = (
db.query(
func.count(ProviderAPIKey.id),
func.coalesce(func.sum(ProviderAPIKey.concurrent_429_count), 0),
func.coalesce(func.sum(ProviderAPIKey.rpm_429_count), 0),
)
.filter(is_adaptive)
.one()
)
total_keys = len(adaptive_keys)
total_concurrent_429 = sum(key.concurrent_429_count or 0 for key in adaptive_keys)
total_rpm_429 = sum(key.rpm_429_count or 0 for key in adaptive_keys)
total_adjustments = sum(len(key.adjustment_history or []) for key in adaptive_keys)
# adjustment_history 是 JSON 列,长度只能在 Python 侧统计;
# 只加载有历史记录的 key 的必要列
keys_with_history = (
db.query(ProviderAPIKey)
.options(
load_only(ProviderAPIKey.id, ProviderAPIKey.name, ProviderAPIKey.adjustment_history)
)
.filter(is_adaptive, ProviderAPIKey.adjustment_history.isnot(None))
.all()
)
total_adjustments = sum(len(key.adjustment_history or []) for key in keys_with_history)
recent_adjustments = []
for key in adaptive_keys:
for key in keys_with_history:
if key.adjustment_history:
for adj in key.adjustment_history[-3:]:
recent_adjustments.append(
@@ -386,8 +404,8 @@ class AdaptiveSummaryAdapter(AdminApiAdapter):
return {
"total_adaptive_keys": total_keys,
"total_concurrent_429_errors": total_concurrent_429,
"total_rpm_429_errors": total_rpm_429,
"total_concurrent_429_errors": int(total_concurrent_429),
"total_rpm_429_errors": int(total_rpm_429),
"total_adjustments": total_adjustments,
"recent_adjustments": recent_adjustments[:10],
}

View File

@@ -298,6 +298,7 @@ class AdminListStandaloneKeysAdapter(AdminApiAdapter):
wallet_initialized = True
if wallet_initialized:
db.commit()
context.request.state.tx_committed_by_route = True
for api_key in api_keys:
db.refresh(api_key)
@@ -390,6 +391,7 @@ class AdminCreateStandaloneKeyAdapter(AdminApiAdapter):
if wallet is None:
raise InvalidRequestException("独立密钥钱包初始化失败")
db.commit()
context.request.state.tx_committed_by_route = True
db.refresh(api_key)
wallet_summary = WalletService.serialize_wallet_summary(wallet)
@@ -533,6 +535,7 @@ class AdminToggleApiKeyAdapter(AdminApiAdapter):
api_key.is_active = not api_key.is_active
api_key.updated_at = datetime.now(timezone.utc)
db.commit()
context.request.state.tx_committed_by_route = True
db.refresh(api_key)
logger.info(
@@ -569,6 +572,7 @@ class AdminDeleteApiKeyAdapter(AdminApiAdapter):
pre_clean_api_key(db, api_key.id)
db.delete(api_key)
db.commit()
context.request.state.tx_committed_by_route = True
logger.info(
f"管理员删除API密钥: Key ID {self.key_id}, 用户 {user.email if user else '未知'}"

View File

@@ -1244,10 +1244,13 @@ class AdminCleanupBannedKeysAdapter(AdminApiAdapter):
return BatchActionResponse(affected=0, message="未发现已知封号账号")
banned_key_ids = [str(key.id) for key in banned_keys]
for key in banned_keys:
db.delete(key)
try:
db.execute(
sa_delete(ProviderAPIKey).where(
ProviderAPIKey.provider_id == pid,
ProviderAPIKey.id.in_(banned_key_ids),
)
)
db.commit()
except Exception as exc:
db.rollback()

View File

@@ -10,6 +10,7 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy import func
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
@@ -228,20 +229,26 @@ class AdminProviderStatsAdapter(AdminApiAdapter):
raise HTTPException(status_code=404, detail="Provider not found")
since = datetime.now(timezone.utc) - timedelta(hours=self.hours)
stats = (
db.query(ProviderUsageTracking)
row = (
db.query(
func.coalesce(func.sum(ProviderUsageTracking.total_requests), 0),
func.coalesce(func.sum(ProviderUsageTracking.successful_requests), 0),
func.coalesce(func.sum(ProviderUsageTracking.failed_requests), 0),
func.coalesce(func.avg(ProviderUsageTracking.avg_response_time_ms), 0),
func.coalesce(func.sum(ProviderUsageTracking.total_cost_usd), 0),
)
.filter(
ProviderUsageTracking.provider_id == self.provider_id,
ProviderUsageTracking.window_start >= since,
)
.all()
.one()
)
total_requests = sum(s.total_requests for s in stats)
total_success = sum(s.successful_requests for s in stats)
total_failures = sum(s.failed_requests for s in stats)
avg_response_time = sum(s.avg_response_time_ms for s in stats) / len(stats) if stats else 0
total_cost = sum(s.total_cost_usd for s in stats)
total_requests = int(row[0])
total_success = int(row[1])
total_failures = int(row[2])
avg_response_time = float(row[3])
total_cost = float(row[4])
return JSONResponse(
{
@@ -250,10 +257,14 @@ class AdminProviderStatsAdapter(AdminApiAdapter):
"period_hours": self.hours,
"billing_info": {
"billing_type": provider.billing_type.value,
"monthly_quota_usd": provider.monthly_quota_usd,
"monthly_used_usd": provider.monthly_used_usd,
"monthly_quota_usd": (
float(provider.monthly_quota_usd)
if provider.monthly_quota_usd is not None
else None
),
"monthly_used_usd": float(provider.monthly_used_usd or 0),
"quota_remaining_usd": (
provider.monthly_quota_usd - provider.monthly_used_usd
float(provider.monthly_quota_usd - provider.monthly_used_usd)
if provider.monthly_quota_usd is not None
else None
),

View File

@@ -41,8 +41,8 @@ class AdminQuotaUsageAdapter(AdminApiAdapter):
result = []
for provider in providers:
quota = provider.monthly_quota_usd or 0.0
used = float(provider.monthly_used_usd or 0.0)
quota = float(provider.monthly_quota_usd or 0)
used = float(provider.monthly_used_usd or 0)
remaining = max(quota - used, 0.0)
usage_percent = round((used / quota) * 100, 2) if quota > 0 else 0.0

View File

@@ -1312,22 +1312,42 @@ class AdminUsageDetailAdapter(AdminApiAdapter):
"total": usage_record.total_tokens,
},
"cost": {
"input": usage_record.input_cost_usd,
"output": usage_record.output_cost_usd,
"total": usage_record.total_cost_usd,
"input": float(usage_record.input_cost_usd or 0),
"output": float(usage_record.output_cost_usd or 0),
"total": float(usage_record.total_cost_usd or 0),
},
"cache_creation_input_tokens": usage_record.cache_creation_input_tokens,
"cache_read_input_tokens": usage_record.cache_read_input_tokens,
"cache_creation_input_tokens_5m": usage_record.cache_creation_input_tokens_5m or 0,
"cache_creation_input_tokens_1h": usage_record.cache_creation_input_tokens_1h or 0,
"cache_creation_cost": getattr(usage_record, "cache_creation_cost_usd", 0.0),
"cache_read_cost": getattr(usage_record, "cache_read_cost_usd", 0.0),
"request_cost": getattr(usage_record, "request_cost_usd", 0.0),
"input_price_per_1m": usage_record.input_price_per_1m,
"output_price_per_1m": usage_record.output_price_per_1m,
"cache_creation_price_per_1m": usage_record.cache_creation_price_per_1m,
"cache_read_price_per_1m": usage_record.cache_read_price_per_1m,
"price_per_request": usage_record.price_per_request,
"cache_creation_cost": float(getattr(usage_record, "cache_creation_cost_usd", 0) or 0),
"cache_read_cost": float(getattr(usage_record, "cache_read_cost_usd", 0) or 0),
"request_cost": float(getattr(usage_record, "request_cost_usd", 0) or 0),
"input_price_per_1m": (
float(usage_record.input_price_per_1m)
if usage_record.input_price_per_1m is not None
else None
),
"output_price_per_1m": (
float(usage_record.output_price_per_1m)
if usage_record.output_price_per_1m is not None
else None
),
"cache_creation_price_per_1m": (
float(usage_record.cache_creation_price_per_1m)
if usage_record.cache_creation_price_per_1m is not None
else None
),
"cache_read_price_per_1m": (
float(usage_record.cache_read_price_per_1m)
if usage_record.cache_read_price_per_1m is not None
else None
),
"price_per_request": (
float(usage_record.price_per_request)
if usage_record.price_per_request is not None
else None
),
"request_type": usage_record.request_type,
"is_stream": usage_record.is_stream,
"status_code": usage_record.status_code,

View File

@@ -588,6 +588,7 @@ class AdminDeleteUserKeyAdapter(AdminApiAdapter):
pre_clean_api_key(db, api_key.id)
db.delete(api_key)
db.commit()
context.request.state.tx_committed_by_route = True
logger.info(f"管理员删除用户API Key: 用户ID {self.user_id}, Key ID {self.key_id}")
@@ -624,6 +625,7 @@ class AdminToggleUserKeyLockAdapter(AdminApiAdapter):
api_key.is_locked = not api_key.is_locked
db.commit()
context.request.state.tx_committed_by_route = True
db.refresh(api_key)
logger.info(