mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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 '未知'}"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user