mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +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(
|
||||
|
||||
@@ -287,6 +287,7 @@ class AuthLoginAdapter(AuthPublicAdapter):
|
||||
error_reason="邮箱或密码错误",
|
||||
)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码错误")
|
||||
|
||||
AuditService.log_login_attempt(
|
||||
@@ -298,6 +299,7 @@ class AuthLoginAdapter(AuthPublicAdapter):
|
||||
user_id=user.id,
|
||||
)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
|
||||
access_token = AuthService.create_access_token(
|
||||
data={
|
||||
@@ -472,6 +474,7 @@ class AuthRegisterAdapter(AuthPublicAdapter):
|
||||
metadata={"username": register_request.username, "reason": "registration_disabled"},
|
||||
)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="系统暂不开放注册")
|
||||
|
||||
email = register_request.email
|
||||
@@ -514,6 +517,7 @@ class AuthRegisterAdapter(AuthPublicAdapter):
|
||||
metadata={"email": email, "reason": "email_suffix_not_allowed"},
|
||||
)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=suffix_error,
|
||||
@@ -550,6 +554,7 @@ class AuthRegisterAdapter(AuthPublicAdapter):
|
||||
)
|
||||
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
|
||||
# 注册成功后清除验证状态(在 commit 后清理,即使清理失败也不影响注册结果)
|
||||
if require_verification and email:
|
||||
@@ -565,6 +570,7 @@ class AuthRegisterAdapter(AuthPublicAdapter):
|
||||
message="注册成功",
|
||||
).model_dump()
|
||||
except ValueError as exc:
|
||||
db.rollback()
|
||||
AuditService.log_event(
|
||||
db=db,
|
||||
event_type=AuditEventType.UNAUTHORIZED_ACCESS,
|
||||
@@ -574,6 +580,7 @@ class AuthRegisterAdapter(AuthPublicAdapter):
|
||||
metadata={"username": register_request.username, "error": str(exc)},
|
||||
)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||
|
||||
|
||||
@@ -611,6 +618,7 @@ class AuthChangePasswordAdapter(AuthenticatedApiAdapter):
|
||||
raise InvalidRequestException("密码长度至少6位")
|
||||
user.set_password(new_password)
|
||||
context.db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
logger.info(f"用户修改密码: {user.email}")
|
||||
return {"message": "密码修改成功"}
|
||||
|
||||
@@ -643,6 +651,7 @@ class AuthLogoutAdapter(AuthenticatedApiAdapter):
|
||||
metadata={"user_id": user.id, "email": user.email},
|
||||
)
|
||||
context.db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
|
||||
logger.info(f"用户登出成功: {user.email}")
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ class AdminDashboardStatsAdapter(AdminApiAdapter):
|
||||
yesterday_stats = db.query(StatsDaily).filter(StatsDaily.date == yesterday).first()
|
||||
if yesterday_stats:
|
||||
requests_yesterday = yesterday_stats.total_requests
|
||||
cost_yesterday = yesterday_stats.total_cost
|
||||
cost_yesterday = float(yesterday_stats.total_cost or 0)
|
||||
input_tokens_yesterday = yesterday_stats.input_tokens
|
||||
output_tokens_yesterday = yesterday_stats.output_tokens
|
||||
cache_creation_yesterday = yesterday_stats.cache_creation_tokens
|
||||
|
||||
@@ -502,6 +502,7 @@ class UpdateProfileAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
db.refresh(user)
|
||||
return {"message": "个人信息更新成功"}
|
||||
|
||||
@@ -544,6 +545,7 @@ class ChangePasswordAdapter(AuthenticatedApiAdapter):
|
||||
user.set_password(request.new_password)
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
action = "修改" if has_password else "设置"
|
||||
logger.info(f"用户{action}密码: {user.email}")
|
||||
return {"message": f"密码{action}成功"}
|
||||
@@ -735,6 +737,7 @@ class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
pre_clean_api_key(context.db, api_key.id)
|
||||
context.db.delete(api_key)
|
||||
context.db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
return {"message": "API密钥已删除"}
|
||||
|
||||
|
||||
@@ -756,6 +759,7 @@ class ToggleMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
raise ForbiddenException("该密钥已被管理员锁定,无法修改状态")
|
||||
api_key.is_active = not api_key.is_active
|
||||
context.db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
context.db.refresh(api_key)
|
||||
return {
|
||||
"id": api_key.id,
|
||||
@@ -1068,7 +1072,7 @@ class GetUsageAdapter(AuthenticatedApiAdapter):
|
||||
"input_tokens": r.input_tokens,
|
||||
"output_tokens": r.output_tokens,
|
||||
"total_tokens": r.total_tokens,
|
||||
"cost": r.total_cost_usd,
|
||||
"cost": float(r.total_cost_usd or 0),
|
||||
"response_time_ms": r.response_time_ms,
|
||||
"first_byte_time_ms": r.first_byte_time_ms,
|
||||
"is_stream": r.is_stream,
|
||||
@@ -1482,6 +1486,7 @@ class UpdateApiKeyProvidersAdapter(AuthenticatedApiAdapter):
|
||||
)
|
||||
api_key.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
logger.debug(f"用户 {user.id} 更新API密钥 {self.api_key_id} 的可用提供商")
|
||||
return {"message": "API密钥可用提供商已更新"}
|
||||
|
||||
@@ -1531,6 +1536,7 @@ class UpdateApiKeyCapabilitiesAdapter(AuthenticatedApiAdapter):
|
||||
api_key.force_capabilities = force_capabilities
|
||||
api_key.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
|
||||
# 记录审计日志
|
||||
audit_service.log_event(
|
||||
@@ -1662,6 +1668,7 @@ class UpdateModelCapabilitySettingsAdapter(AuthenticatedApiAdapter):
|
||||
user.model_capability_settings = settings
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
context.request.state.tx_committed_by_route = True
|
||||
|
||||
# 清除用户缓存,确保下次读取时获取最新数据
|
||||
await UserCacheService.invalidate_user_cache(user.id, user.email)
|
||||
|
||||
Reference in New Issue
Block a user