mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 19:22:26 +08:00
- 新增 Management Token(访问令牌)功能,支持创建、更新、删除和管理 - 前端添加访问令牌管理页面,支持普通用户和管理员 - 后端实现完整的令牌生命周期管理 API - 添加数据库迁移脚本创建 management_tokens 表 - Nginx 配置添加 gzip 压缩,优化响应传输 - Dialog 组件添加 persistent 属性,防止意外关闭 - 为管理后台 API 添加详细的中文文档注释 - 简化多处类型注解,统一代码风格
680 lines
25 KiB
Python
680 lines
25 KiB
Python
"""管理员独立余额 API Key 管理路由。
|
||
|
||
独立余额Key:不关联用户配额,有独立余额限制,用于给非注册用户使用。
|
||
"""
|
||
|
||
import os
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Optional
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.api.base.admin_adapter import AdminApiAdapter
|
||
from src.api.base.pipeline import ApiRequestPipeline
|
||
from src.core.exceptions import InvalidRequestException, NotFoundException
|
||
from src.core.logger import logger
|
||
from src.database import get_db
|
||
from src.models.api import CreateApiKeyRequest
|
||
from src.models.database import ApiKey
|
||
from src.services.user.apikey import ApiKeyService
|
||
|
||
|
||
# 应用时区配置,默认为 Asia/Shanghai
|
||
APP_TIMEZONE = ZoneInfo(os.getenv("APP_TIMEZONE", "Asia/Shanghai"))
|
||
|
||
|
||
def parse_expiry_date(date_str: Optional[str]) -> Optional[datetime]:
|
||
"""解析过期日期字符串为 datetime 对象。
|
||
|
||
Args:
|
||
date_str: 日期字符串,支持 "YYYY-MM-DD" 或 ISO 格式
|
||
|
||
Returns:
|
||
datetime 对象(当天 23:59:59.999999,应用时区),或 None 如果输入为空
|
||
|
||
Raises:
|
||
BadRequestException: 日期格式无效
|
||
"""
|
||
if not date_str or not date_str.strip():
|
||
return None
|
||
|
||
date_str = date_str.strip()
|
||
|
||
# 尝试 YYYY-MM-DD 格式
|
||
try:
|
||
parsed_date = datetime.strptime(date_str, "%Y-%m-%d")
|
||
# 设置为当天结束时间 (23:59:59.999999,应用时区)
|
||
return parsed_date.replace(
|
||
hour=23, minute=59, second=59, microsecond=999999, tzinfo=APP_TIMEZONE
|
||
)
|
||
except ValueError:
|
||
pass
|
||
|
||
# 尝试完整 ISO 格式
|
||
try:
|
||
return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||
except ValueError:
|
||
pass
|
||
|
||
raise InvalidRequestException(f"无效的日期格式: {date_str},请使用 YYYY-MM-DD 格式")
|
||
|
||
|
||
router = APIRouter(prefix="/api/admin/api-keys", tags=["Admin - API Keys (Standalone)"])
|
||
pipeline = ApiRequestPipeline()
|
||
|
||
|
||
@router.get("")
|
||
async def list_standalone_api_keys(
|
||
request: Request,
|
||
skip: int = Query(0, ge=0),
|
||
limit: int = Query(100, ge=1, le=500),
|
||
is_active: Optional[bool] = None,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
列出所有独立余额 API Keys
|
||
|
||
获取系统中所有独立余额 API Key 的列表。独立余额 Key 不关联用户配额,
|
||
有独立的余额限制,主要用于给非注册用户使用。
|
||
|
||
**查询参数**:
|
||
- `skip`: 跳过的记录数(分页偏移量),默认 0
|
||
- `limit`: 返回的记录数(分页限制),默认 100,最大 500
|
||
- `is_active`: 可选,根据启用状态筛选(true/false)
|
||
|
||
**返回字段**:
|
||
- `api_keys`: API Key 列表,包含 id, name, key_display, is_active, current_balance_usd,
|
||
balance_used_usd, total_requests, total_cost_usd, rate_limit, allowed_providers,
|
||
allowed_api_formats, allowed_models, last_used_at, expires_at, created_at, updated_at,
|
||
auto_delete_on_expiry 等字段
|
||
- `total`: 符合条件的总记录数
|
||
- `limit`: 当前分页限制
|
||
- `skip`: 当前分页偏移量
|
||
"""
|
||
adapter = AdminListStandaloneKeysAdapter(skip=skip, limit=limit, is_active=is_active)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.post("")
|
||
async def create_standalone_api_key(
|
||
request: Request,
|
||
key_data: CreateApiKeyRequest,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
创建独立余额 API Key
|
||
|
||
创建一个新的独立余额 API Key。独立余额 Key 必须设置初始余额限制。
|
||
|
||
**请求体字段**:
|
||
- `name`: API Key 的名称
|
||
- `initial_balance_usd`: 必需,初始余额(美元),必须大于 0
|
||
- `allowed_providers`: 可选,允许使用的提供商列表
|
||
- `allowed_api_formats`: 可选,允许使用的 API 格式列表
|
||
- `allowed_models`: 可选,允许使用的模型列表
|
||
- `rate_limit`: 可选,速率限制配置(请求数/秒)
|
||
- `expire_days`: 可选,过期天数(兼容旧版)
|
||
- `expires_at`: 可选,过期时间(ISO 格式或 YYYY-MM-DD 格式,优先级高于 expire_days)
|
||
- `auto_delete_on_expiry`: 可选,过期后是否自动删除
|
||
|
||
**返回字段**:
|
||
- `id`: API Key ID
|
||
- `key`: 完整的 API Key(仅在创建时返回一次)
|
||
- `name`: API Key 名称
|
||
- `key_display`: 脱敏显示的 Key
|
||
- `is_standalone`: 是否为独立余额 Key(始终为 true)
|
||
- `current_balance_usd`: 当前余额
|
||
- `balance_used_usd`: 已使用余额
|
||
- `rate_limit`: 速率限制配置
|
||
- `expires_at`: 过期时间
|
||
- `created_at`: 创建时间
|
||
- `message`: 提示信息
|
||
"""
|
||
adapter = AdminCreateStandaloneKeyAdapter(key_data=key_data)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.put("/{key_id}")
|
||
async def update_api_key(
|
||
key_id: str, request: Request, key_data: CreateApiKeyRequest, db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
更新独立余额 API Key
|
||
|
||
更新指定 ID 的独立余额 API Key 的配置信息。
|
||
|
||
**路径参数**:
|
||
- `key_id`: API Key ID
|
||
|
||
**请求体字段**:
|
||
- `name`: 可选,API Key 的名称
|
||
- `rate_limit`: 可选,速率限制配置(null 表示无限制)
|
||
- `allowed_providers`: 可选,允许使用的提供商列表
|
||
- `allowed_api_formats`: 可选,允许使用的 API 格式列表
|
||
- `allowed_models`: 可选,允许使用的模型列表
|
||
- `expire_days`: 可选,过期天数(兼容旧版)
|
||
- `expires_at`: 可选,过期时间(ISO 格式或 YYYY-MM-DD 格式,优先级高于 expire_days,null 或空字符串表示永不过期)
|
||
- `auto_delete_on_expiry`: 可选,过期后是否自动删除
|
||
|
||
**返回字段**:
|
||
- `id`: API Key ID
|
||
- `name`: API Key 名称
|
||
- `key_display`: 脱敏显示的 Key
|
||
- `is_active`: 是否启用
|
||
- `current_balance_usd`: 当前余额
|
||
- `balance_used_usd`: 已使用余额
|
||
- `rate_limit`: 速率限制配置
|
||
- `expires_at`: 过期时间
|
||
- `updated_at`: 更新时间
|
||
- `message`: 提示信息
|
||
"""
|
||
adapter = AdminUpdateApiKeyAdapter(key_id=key_id, key_data=key_data)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.patch("/{key_id}")
|
||
async def toggle_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
切换 API Key 启用状态
|
||
|
||
切换指定 API Key 的启用/禁用状态。
|
||
|
||
**路径参数**:
|
||
- `key_id`: API Key ID
|
||
|
||
**返回字段**:
|
||
- `id`: API Key ID
|
||
- `is_active`: 新的启用状态
|
||
- `message`: 提示信息
|
||
"""
|
||
adapter = AdminToggleApiKeyAdapter(key_id=key_id)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.delete("/{key_id}")
|
||
async def delete_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
删除 API Key
|
||
|
||
删除指定的 API Key。此操作不可逆。
|
||
|
||
**路径参数**:
|
||
- `key_id`: API Key ID
|
||
|
||
**返回字段**:
|
||
- `message`: 提示信息
|
||
"""
|
||
adapter = AdminDeleteApiKeyAdapter(key_id=key_id)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.patch("/{key_id}/balance")
|
||
async def add_balance_to_key(
|
||
key_id: str,
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
调整独立余额 API Key 的余额
|
||
|
||
为指定的独立余额 API Key 增加或扣除余额。
|
||
|
||
**路径参数**:
|
||
- `key_id`: API Key ID
|
||
|
||
**请求体字段**:
|
||
- `amount_usd`: 调整金额(美元),正数为充值,负数为扣除
|
||
|
||
**返回字段**:
|
||
- `id`: API Key ID
|
||
- `name`: API Key 名称
|
||
- `current_balance_usd`: 调整后的当前余额
|
||
- `balance_used_usd`: 已使用余额
|
||
- `message`: 提示信息
|
||
"""
|
||
# 从请求体获取调整金额
|
||
body = await request.json()
|
||
amount_usd = body.get("amount_usd")
|
||
|
||
# 参数校验
|
||
if amount_usd is None:
|
||
raise HTTPException(status_code=400, detail="缺少必需参数: amount_usd")
|
||
|
||
if amount_usd == 0:
|
||
raise HTTPException(status_code=400, detail="调整金额不能为 0")
|
||
|
||
# 类型校验
|
||
try:
|
||
amount_usd = float(amount_usd)
|
||
except (ValueError, TypeError):
|
||
raise HTTPException(status_code=400, detail="调整金额必须是有效数字")
|
||
|
||
# 如果是扣除操作,检查Key是否存在以及余额是否充足
|
||
if amount_usd < 0:
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == key_id).first()
|
||
if not api_key:
|
||
raise HTTPException(status_code=404, detail="API密钥不存在")
|
||
|
||
if not api_key.is_standalone:
|
||
raise HTTPException(status_code=400, detail="只能为独立余额Key调整余额")
|
||
|
||
if api_key.current_balance_usd is not None:
|
||
if abs(amount_usd) > api_key.current_balance_usd:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"扣除金额 ${abs(amount_usd):.2f} 超过当前余额 ${api_key.current_balance_usd:.2f}",
|
||
)
|
||
|
||
adapter = AdminAddBalanceAdapter(key_id=key_id, amount_usd=amount_usd)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.get("/{key_id}")
|
||
async def get_api_key_detail(
|
||
key_id: str,
|
||
request: Request,
|
||
include_key: bool = Query(False, description="Include full decrypted key in response"),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
获取 API Key 详情
|
||
|
||
获取指定 API Key 的详细信息。可选择是否返回完整的解密密钥。
|
||
|
||
**路径参数**:
|
||
- `key_id`: API Key ID
|
||
|
||
**查询参数**:
|
||
- `include_key`: 是否包含完整的解密密钥,默认 false
|
||
|
||
**返回字段**:
|
||
- 当 include_key=false 时,返回基本信息:id, user_id, name, key_display, is_active,
|
||
is_standalone, current_balance_usd, balance_used_usd, total_requests, total_cost_usd,
|
||
rate_limit, allowed_providers, allowed_api_formats, allowed_models, last_used_at,
|
||
expires_at, created_at, updated_at
|
||
- 当 include_key=true 时,返回完整密钥:key
|
||
"""
|
||
if include_key:
|
||
adapter = AdminGetFullKeyAdapter(key_id=key_id)
|
||
else:
|
||
# Return basic key info without full key
|
||
adapter = AdminGetKeyDetailAdapter(key_id=key_id)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
class AdminListStandaloneKeysAdapter(AdminApiAdapter):
|
||
"""列出独立余额Keys"""
|
||
|
||
def __init__(
|
||
self,
|
||
skip: int,
|
||
limit: int,
|
||
is_active: Optional[bool],
|
||
):
|
||
self.skip = skip
|
||
self.limit = limit
|
||
self.is_active = is_active
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
# 只查询独立余额Keys
|
||
query = db.query(ApiKey).filter(ApiKey.is_standalone == True)
|
||
|
||
if self.is_active is not None:
|
||
query = query.filter(ApiKey.is_active == self.is_active)
|
||
|
||
total = query.count()
|
||
api_keys = (
|
||
query.order_by(ApiKey.created_at.desc()).offset(self.skip).limit(self.limit).all()
|
||
)
|
||
|
||
context.add_audit_metadata(
|
||
action="list_standalone_api_keys",
|
||
filter_is_active=self.is_active,
|
||
limit=self.limit,
|
||
skip=self.skip,
|
||
total=total,
|
||
)
|
||
|
||
return {
|
||
"api_keys": [
|
||
{
|
||
"id": api_key.id,
|
||
"user_id": api_key.user_id, # 创建者ID
|
||
"name": api_key.name,
|
||
"key_display": api_key.get_display_key(),
|
||
"is_active": api_key.is_active,
|
||
"is_standalone": api_key.is_standalone,
|
||
"current_balance_usd": api_key.current_balance_usd,
|
||
"balance_used_usd": float(api_key.balance_used_usd or 0),
|
||
"total_requests": api_key.total_requests,
|
||
"total_cost_usd": float(api_key.total_cost_usd or 0),
|
||
"rate_limit": api_key.rate_limit,
|
||
"allowed_providers": api_key.allowed_providers,
|
||
"allowed_api_formats": api_key.allowed_api_formats,
|
||
"allowed_models": api_key.allowed_models,
|
||
"last_used_at": (
|
||
api_key.last_used_at.isoformat() if api_key.last_used_at else None
|
||
),
|
||
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
|
||
"created_at": api_key.created_at.isoformat(),
|
||
"updated_at": api_key.updated_at.isoformat() if api_key.updated_at else None,
|
||
"auto_delete_on_expiry": api_key.auto_delete_on_expiry,
|
||
}
|
||
for api_key in api_keys
|
||
],
|
||
"total": total,
|
||
"limit": self.limit,
|
||
"skip": self.skip,
|
||
}
|
||
|
||
|
||
class AdminCreateStandaloneKeyAdapter(AdminApiAdapter):
|
||
"""创建独立余额Key"""
|
||
|
||
def __init__(self, key_data: CreateApiKeyRequest):
|
||
self.key_data = key_data
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
|
||
# 独立Key必须设置初始余额
|
||
if not self.key_data.initial_balance_usd or self.key_data.initial_balance_usd <= 0:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="创建独立余额Key必须设置有效的初始余额(initial_balance_usd > 0)",
|
||
)
|
||
|
||
# 独立Key需要关联到管理员用户(从context获取)
|
||
admin_user_id = context.user.id
|
||
|
||
# 解析过期时间(优先使用 expires_at,其次使用 expire_days)
|
||
expires_at_dt = parse_expiry_date(self.key_data.expires_at)
|
||
|
||
# 创建独立Key
|
||
api_key, plain_key = ApiKeyService.create_api_key(
|
||
db=db,
|
||
user_id=admin_user_id, # 关联到创建者
|
||
name=self.key_data.name,
|
||
allowed_providers=self.key_data.allowed_providers,
|
||
allowed_api_formats=self.key_data.allowed_api_formats,
|
||
allowed_models=self.key_data.allowed_models,
|
||
rate_limit=self.key_data.rate_limit, # None 表示不限制
|
||
expire_days=self.key_data.expire_days, # 兼容旧版
|
||
expires_at=expires_at_dt, # 优先使用
|
||
initial_balance_usd=self.key_data.initial_balance_usd,
|
||
is_standalone=True, # 标记为独立Key
|
||
auto_delete_on_expiry=self.key_data.auto_delete_on_expiry,
|
||
)
|
||
|
||
logger.info(f"管理员创建独立余额Key: ID {api_key.id}, 初始余额 ${self.key_data.initial_balance_usd}")
|
||
|
||
context.add_audit_metadata(
|
||
action="create_standalone_api_key",
|
||
key_id=api_key.id,
|
||
initial_balance_usd=self.key_data.initial_balance_usd,
|
||
)
|
||
|
||
return {
|
||
"id": api_key.id,
|
||
"key": plain_key, # 只在创建时返回完整密钥
|
||
"name": api_key.name,
|
||
"key_display": api_key.get_display_key(),
|
||
"is_standalone": True,
|
||
"current_balance_usd": api_key.current_balance_usd,
|
||
"balance_used_usd": 0.0,
|
||
"rate_limit": api_key.rate_limit,
|
||
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
|
||
"created_at": api_key.created_at.isoformat(),
|
||
"message": "独立余额Key创建成功,请妥善保存完整密钥,后续将无法查看",
|
||
}
|
||
|
||
|
||
class AdminUpdateApiKeyAdapter(AdminApiAdapter):
|
||
"""更新独立余额Key"""
|
||
|
||
def __init__(self, key_id: str, key_data: CreateApiKeyRequest):
|
||
self.key_id = key_id
|
||
self.key_data = key_data
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == self.key_id).first()
|
||
if not api_key:
|
||
raise NotFoundException("API密钥不存在", "api_key")
|
||
|
||
# 构建更新数据
|
||
update_data = {}
|
||
if self.key_data.name is not None:
|
||
update_data["name"] = self.key_data.name
|
||
# rate_limit: 显式传递时更新(包括 null 表示无限制)
|
||
if "rate_limit" in self.key_data.model_fields_set:
|
||
update_data["rate_limit"] = self.key_data.rate_limit
|
||
if (
|
||
hasattr(self.key_data, "auto_delete_on_expiry")
|
||
and self.key_data.auto_delete_on_expiry is not None
|
||
):
|
||
update_data["auto_delete_on_expiry"] = self.key_data.auto_delete_on_expiry
|
||
|
||
# 访问限制配置(允许设置为空数组来清除限制)
|
||
if hasattr(self.key_data, "allowed_providers"):
|
||
update_data["allowed_providers"] = self.key_data.allowed_providers
|
||
if hasattr(self.key_data, "allowed_api_formats"):
|
||
update_data["allowed_api_formats"] = self.key_data.allowed_api_formats
|
||
if hasattr(self.key_data, "allowed_models"):
|
||
update_data["allowed_models"] = self.key_data.allowed_models
|
||
|
||
# 处理过期时间
|
||
# 优先使用 expires_at(如果显式传递且有值)
|
||
if self.key_data.expires_at and self.key_data.expires_at.strip():
|
||
update_data["expires_at"] = parse_expiry_date(self.key_data.expires_at)
|
||
elif "expires_at" in self.key_data.model_fields_set:
|
||
# expires_at 明确传递为 null 或空字符串,设为永不过期
|
||
update_data["expires_at"] = None
|
||
# 兼容旧版 expire_days
|
||
elif "expire_days" in self.key_data.model_fields_set:
|
||
if self.key_data.expire_days is not None and self.key_data.expire_days > 0:
|
||
update_data["expires_at"] = datetime.now(timezone.utc) + timedelta(
|
||
days=self.key_data.expire_days
|
||
)
|
||
else:
|
||
# expire_days = None/0/负数 表示永不过期
|
||
update_data["expires_at"] = None
|
||
|
||
# 使用 ApiKeyService 更新
|
||
updated_key = ApiKeyService.update_api_key(db, self.key_id, **update_data)
|
||
if not updated_key:
|
||
raise NotFoundException("更新失败", "api_key")
|
||
|
||
logger.info(f"管理员更新独立余额Key: ID {self.key_id}, 更新字段 {list(update_data.keys())}")
|
||
|
||
context.add_audit_metadata(
|
||
action="update_standalone_api_key",
|
||
key_id=self.key_id,
|
||
updated_fields=list(update_data.keys()),
|
||
)
|
||
|
||
return {
|
||
"id": updated_key.id,
|
||
"name": updated_key.name,
|
||
"key_display": updated_key.get_display_key(),
|
||
"is_active": updated_key.is_active,
|
||
"current_balance_usd": updated_key.current_balance_usd,
|
||
"balance_used_usd": float(updated_key.balance_used_usd or 0),
|
||
"rate_limit": updated_key.rate_limit,
|
||
"expires_at": updated_key.expires_at.isoformat() if updated_key.expires_at else None,
|
||
"updated_at": updated_key.updated_at.isoformat() if updated_key.updated_at else None,
|
||
"message": "API密钥已更新",
|
||
}
|
||
|
||
|
||
class AdminToggleApiKeyAdapter(AdminApiAdapter):
|
||
def __init__(self, key_id: str):
|
||
self.key_id = key_id
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == self.key_id).first()
|
||
if not api_key:
|
||
raise NotFoundException("API密钥不存在", "api_key")
|
||
|
||
api_key.is_active = not api_key.is_active
|
||
api_key.updated_at = datetime.now(timezone.utc)
|
||
db.commit()
|
||
db.refresh(api_key)
|
||
|
||
logger.info(f"管理员切换API密钥状态: Key ID {self.key_id}, 新状态 {'启用' if api_key.is_active else '禁用'}")
|
||
|
||
context.add_audit_metadata(
|
||
action="toggle_api_key",
|
||
target_key_id=api_key.id,
|
||
user_id=api_key.user_id,
|
||
new_status="enabled" if api_key.is_active else "disabled",
|
||
)
|
||
|
||
return {
|
||
"id": api_key.id,
|
||
"is_active": api_key.is_active,
|
||
"message": f"API密钥已{'启用' if api_key.is_active else '禁用'}",
|
||
}
|
||
|
||
|
||
class AdminDeleteApiKeyAdapter(AdminApiAdapter):
|
||
def __init__(self, key_id: str):
|
||
self.key_id = key_id
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == self.key_id).first()
|
||
if not api_key:
|
||
raise HTTPException(status_code=404, detail="API密钥不存在")
|
||
|
||
user = api_key.user
|
||
db.delete(api_key)
|
||
db.commit()
|
||
|
||
logger.info(f"管理员删除API密钥: Key ID {self.key_id}, 用户 {user.email if user else '未知'}")
|
||
|
||
context.add_audit_metadata(
|
||
action="delete_api_key",
|
||
target_key_id=self.key_id,
|
||
user_id=user.id if user else None,
|
||
user_email=user.email if user else None,
|
||
)
|
||
return {"message": "API密钥已删除"}
|
||
|
||
|
||
class AdminAddBalanceAdapter(AdminApiAdapter):
|
||
"""为独立余额Key增加余额"""
|
||
|
||
def __init__(self, key_id: str, amount_usd: float):
|
||
self.key_id = key_id
|
||
self.amount_usd = amount_usd
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
|
||
# 使用 ApiKeyService 增加余额
|
||
updated_key = ApiKeyService.add_balance(db, self.key_id, self.amount_usd)
|
||
|
||
if not updated_key:
|
||
raise NotFoundException("余额充值失败:Key不存在或不是独立余额Key", "api_key")
|
||
|
||
logger.info(f"管理员为独立余额Key充值: ID {self.key_id}, 充值 ${self.amount_usd:.4f}")
|
||
|
||
context.add_audit_metadata(
|
||
action="add_balance_to_key",
|
||
key_id=self.key_id,
|
||
amount_usd=self.amount_usd,
|
||
new_current_balance=updated_key.current_balance_usd,
|
||
)
|
||
|
||
return {
|
||
"id": updated_key.id,
|
||
"name": updated_key.name,
|
||
"current_balance_usd": updated_key.current_balance_usd,
|
||
"balance_used_usd": float(updated_key.balance_used_usd or 0),
|
||
"message": f"余额充值成功,充值 ${self.amount_usd:.2f},当前余额 ${updated_key.current_balance_usd:.2f}",
|
||
}
|
||
|
||
|
||
class AdminGetFullKeyAdapter(AdminApiAdapter):
|
||
"""获取完整的API密钥"""
|
||
|
||
def __init__(self, key_id: str):
|
||
self.key_id = key_id
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
from src.core.crypto import crypto_service
|
||
|
||
db = context.db
|
||
|
||
# 查找API密钥
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == self.key_id).first()
|
||
if not api_key:
|
||
raise NotFoundException("API密钥不存在", "api_key")
|
||
|
||
# 解密完整密钥
|
||
if not api_key.key_encrypted:
|
||
raise HTTPException(status_code=400, detail="该密钥没有存储完整密钥信息")
|
||
|
||
try:
|
||
full_key = crypto_service.decrypt(api_key.key_encrypted)
|
||
except Exception as e:
|
||
logger.error(f"解密API密钥失败: Key ID {self.key_id}, 错误: {e}")
|
||
raise HTTPException(status_code=500, detail="解密密钥失败")
|
||
|
||
logger.info(f"管理员查看完整API密钥: Key ID {self.key_id}")
|
||
|
||
context.add_audit_metadata(
|
||
action="view_full_api_key",
|
||
key_id=self.key_id,
|
||
key_name=api_key.name,
|
||
)
|
||
|
||
return {
|
||
"key": full_key,
|
||
}
|
||
|
||
|
||
class AdminGetKeyDetailAdapter(AdminApiAdapter):
|
||
"""Get API key detail without full key"""
|
||
|
||
def __init__(self, key_id: str):
|
||
self.key_id = key_id
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
|
||
api_key = db.query(ApiKey).filter(ApiKey.id == self.key_id).first()
|
||
if not api_key:
|
||
raise NotFoundException("API密钥不存在", "api_key")
|
||
|
||
context.add_audit_metadata(
|
||
action="get_api_key_detail",
|
||
key_id=self.key_id,
|
||
)
|
||
|
||
return {
|
||
"id": api_key.id,
|
||
"user_id": api_key.user_id,
|
||
"name": api_key.name,
|
||
"key_display": api_key.get_display_key(),
|
||
"is_active": api_key.is_active,
|
||
"is_standalone": api_key.is_standalone,
|
||
"current_balance_usd": api_key.current_balance_usd,
|
||
"balance_used_usd": float(api_key.balance_used_usd or 0),
|
||
"total_requests": api_key.total_requests,
|
||
"total_cost_usd": float(api_key.total_cost_usd or 0),
|
||
"rate_limit": api_key.rate_limit,
|
||
"allowed_providers": api_key.allowed_providers,
|
||
"allowed_api_formats": api_key.allowed_api_formats,
|
||
"allowed_models": api_key.allowed_models,
|
||
"last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None,
|
||
"expires_at": api_key.expires_at.isoformat() if api_key.expires_at else None,
|
||
"created_at": api_key.created_at.isoformat(),
|
||
"updated_at": api_key.updated_at.isoformat() if api_key.updated_at else None,
|
||
}
|