1、增加调用 AI 视频生成能力和虚拟素材库管理的对外api
2、增加后台apikkey管理 3、增加apikey单独的模型定价 4、增加apikey调用情况 5、完善所有数据的注释增加
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_admin_user, get_db
|
||||
from app.models.api.api_key import ApiKey
|
||||
from app.models.api.api_key_upscale_config import ApiKeyUpscaleConfig
|
||||
from app.models.api.api_usage_log import ApiUsageLog
|
||||
from app.models.user import User
|
||||
from app.schemas.admin_api.api_key import (
|
||||
ApiKeyCallableModel,
|
||||
ApiKeyCreateRequest,
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyListItem,
|
||||
ApiKeyListOut,
|
||||
ApiKeyRevealResponse,
|
||||
ApiKeyResponse,
|
||||
ApiKeyUpdateRequest,
|
||||
)
|
||||
from app.schemas.admin_api.api_upscale import (
|
||||
ApiUpscaleConfigData,
|
||||
ApiUpscaleConfigResponse,
|
||||
ApiUpscaleConfigSaveRequest,
|
||||
)
|
||||
from app.schemas.admin_api.api_usage import ApiUsageLogResponse, ApiUsageSummaryResponse
|
||||
from app.services.api_v3 import (
|
||||
key_service,
|
||||
upscale_service,
|
||||
usage_log_service,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("videogen")
|
||||
|
||||
router = APIRouter(prefix="/admin/api-keys", tags=["admin-api-keys"])
|
||||
|
||||
|
||||
def _key_to_list_item(key: ApiKey) -> ApiKeyListItem:
|
||||
"""将 ApiKey 模型转为列表项 Schema。"""
|
||||
try:
|
||||
callable_models = json.loads(key.callable_models) if key.callable_models else []
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
callable_models = []
|
||||
|
||||
return ApiKeyListItem(
|
||||
id=key.id,
|
||||
company_name=key.company_name,
|
||||
api_key_prefix=f"{key.api_key_prefix}****",
|
||||
description=key.description,
|
||||
callable_models=[ApiKeyCallableModel(**m) for m in callable_models],
|
||||
quota_limit=key.quota_limit,
|
||||
quota_cycle=key.quota_cycle,
|
||||
quota_used=key.quota_used,
|
||||
is_active=key.is_active,
|
||||
valid_from=key.valid_from,
|
||||
valid_until=key.valid_until,
|
||||
max_concurrent_video_tasks=key.max_concurrent_video_tasks,
|
||||
last_used_at=key.last_used_at,
|
||||
created_at=key.created_at,
|
||||
)
|
||||
|
||||
|
||||
# === API Key CRUD ===
|
||||
|
||||
@router.get("", response_model=ApiKeyListOut, summary="列出 API Key")
|
||||
async def list_keys(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
company_name: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyListOut:
|
||||
"""列出所有 API Key(分页+筛选)。"""
|
||||
total, keys = await key_service.list_api_keys(
|
||||
db, skip=skip, limit=limit,
|
||||
company_name=company_name, is_active=is_active,
|
||||
)
|
||||
return ApiKeyListOut(
|
||||
total=total,
|
||||
items=[_key_to_list_item(k) for k in keys],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=ApiKeyCreateResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="创建 API Key",
|
||||
)
|
||||
async def create_key(
|
||||
req: ApiKeyCreateRequest,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyCreateResponse:
|
||||
"""创建新的 API Key。
|
||||
|
||||
返回包含完整明文 api_key,仅此一次。
|
||||
"""
|
||||
callable_models = [m.model_dump() for m in req.callable_models] if req.callable_models else []
|
||||
|
||||
key, raw_key = await key_service.create_api_key(
|
||||
db=db,
|
||||
company_name=req.company_name,
|
||||
callable_models=callable_models,
|
||||
quota_limit=req.quota_limit,
|
||||
quota_cycle=req.quota_cycle,
|
||||
valid_from=req.valid_from,
|
||||
valid_until=req.valid_until,
|
||||
max_concurrent_video_tasks=req.max_concurrent_video_tasks,
|
||||
description=req.description,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return ApiKeyCreateResponse(
|
||||
id=key.id,
|
||||
company_name=key.company_name,
|
||||
api_key=raw_key,
|
||||
api_key_prefix=key.api_key_prefix,
|
||||
valid_until=key.valid_until,
|
||||
created_at=key.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{key_id}/reveal", response_model=ApiKeyRevealResponse, summary="揭秘 API Key")
|
||||
async def reveal_key(
|
||||
key_id: str = Path(..., description="API Key ID"),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyRevealResponse:
|
||||
"""揭秘 API Key(随时可获取完整明文 Key)。"""
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
|
||||
plaintext = key.decrypt_api_key()
|
||||
if not plaintext:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="该 API Key 创建时未启用加密存储,无法揭秘。请重新创建 Key。",
|
||||
)
|
||||
|
||||
return ApiKeyRevealResponse(
|
||||
id=key.id,
|
||||
company_name=key.company_name,
|
||||
api_key=plaintext,
|
||||
api_key_prefix=key.api_key_prefix,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{key_id}", response_model=ApiKeyListItem, summary="获取 API Key 详情")
|
||||
async def get_key(
|
||||
key_id: str = Path(..., description="API Key ID"),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyListItem:
|
||||
"""获取单个 API Key 详情。"""
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
return _key_to_list_item(key)
|
||||
|
||||
|
||||
@router.put("/{key_id}", response_model=ApiKeyListItem, summary="更新 API Key")
|
||||
async def update_key(
|
||||
req: ApiKeyUpdateRequest,
|
||||
key_id: str = Path(...),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiKeyResponse:
|
||||
"""更新 API Key 配置。"""
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
|
||||
# model_dump 默认输出 snake_case 字段名,直接传给 service
|
||||
update_data = req.model_dump(exclude_none=True)
|
||||
if "callable_models" in update_data and update_data["callable_models"] is not None:
|
||||
update_data["callable_models"] = [
|
||||
m.model_dump() if hasattr(m, "model_dump") else m
|
||||
for m in update_data["callable_models"]
|
||||
]
|
||||
|
||||
key = await key_service.update_api_key(db, key, **update_data)
|
||||
await db.commit()
|
||||
return _key_to_list_item(key)
|
||||
|
||||
|
||||
@router.delete("/{key_id}", summary="删除 API Key")
|
||||
async def delete_key(
|
||||
key_id: str = Path(...),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
"""软删除 API Key。"""
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
|
||||
await key_service.delete_api_key(db, key)
|
||||
await db.commit()
|
||||
return {"status": "deleted", "id": key_id}
|
||||
|
||||
|
||||
# === 超分配置 ===
|
||||
|
||||
@router.get("/{key_id}/upscale", response_model=ApiUpscaleConfigResponse, summary="获取 API Key 超分配置")
|
||||
async def get_upscale_config(
|
||||
key_id: str = Path(...),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiUpscaleConfigResponse:
|
||||
"""获取 API Key 的超分配置。"""
|
||||
config = await upscale_service.get_or_create_upscale_config(db, key_id)
|
||||
try:
|
||||
rules = json.loads(config.rules_json) if config.rules_json else []
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
rules = []
|
||||
|
||||
return ApiUpscaleConfigResponse(
|
||||
data=ApiUpscaleConfigData(
|
||||
enabled=config.enabled,
|
||||
delete_source_after_success=config.delete_source_after_success,
|
||||
rules=rules,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{key_id}/upscale", response_model=ApiUpscaleConfigResponse, summary="保存 API Key 超分配置")
|
||||
async def save_upscale_config(
|
||||
req: ApiUpscaleConfigSaveRequest,
|
||||
key_id: str = Path(...),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiUpscaleConfigResponse:
|
||||
"""保存 API Key 的超分配置。"""
|
||||
config = await upscale_service.save_upscale_config(
|
||||
db=db,
|
||||
api_key_id=key_id,
|
||||
enabled=req.data.enabled,
|
||||
delete_source_after_success=req.data.delete_source_after_success,
|
||||
rules=[r.model_dump() for r in req.data.rules],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return ApiUpscaleConfigResponse(
|
||||
data=ApiUpscaleConfigData(
|
||||
enabled=config.enabled,
|
||||
delete_source_after_success=config.delete_source_after_success,
|
||||
rules=json.loads(config.rules_json) if config.rules_json else [],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# === 使用日志 ===
|
||||
|
||||
@router.get("/{key_id}/usage", response_model=ApiUsageSummaryResponse, summary="获取 API Key 使用统计")
|
||||
async def get_usage(
|
||||
key_id: str = Path(...),
|
||||
days: int = Query(30, ge=1, le=365),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ApiUsageSummaryResponse:
|
||||
"""获取 API Key 的使用统计和明细。"""
|
||||
# 验证 key 存在
|
||||
key = await key_service.get_api_key(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="API Key 不存在")
|
||||
|
||||
summary = await usage_log_service.get_usage_summary(db, api_key_id=key_id, days=days)
|
||||
total, logs = await usage_log_service.list_usage_logs(db, api_key_id=key_id, limit=100)
|
||||
|
||||
return ApiUsageSummaryResponse(
|
||||
total_requests=summary["total_requests"],
|
||||
total_credits_cost=summary["total_credits_cost"],
|
||||
total_tokens_used=summary["total_tokens_used"],
|
||||
success_count=summary["success_count"],
|
||||
failed_count=summary["failed_count"],
|
||||
avg_duration_ms=summary["avg_duration_ms"],
|
||||
items=[
|
||||
ApiUsageLogResponse(
|
||||
id=log.id,
|
||||
api_key_id=log.api_key_id,
|
||||
api_generation_task_id=log.api_generation_task_id,
|
||||
request_type=log.request_type,
|
||||
model_name=log.model_name,
|
||||
gen_type=log.gen_type,
|
||||
credits_cost=log.credits_cost,
|
||||
tokens_used=log.tokens_used,
|
||||
request_duration_ms=log.request_duration_ms,
|
||||
status=log.status,
|
||||
error_message=log.error_message,
|
||||
error_code=log.error_code,
|
||||
created_at=log.created_at,
|
||||
)
|
||||
for log in logs
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# === 整体消耗列表 ===
|
||||
|
||||
@router.get("/usage/all", response_model=dict, summary="获取整体 API 消耗列表")
|
||||
async def list_all_usage(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
api_key_id: str | None = None,
|
||||
gen_type: str | None = None,
|
||||
status_filter: str | None = Query(None, alias="status"),
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
"""获取整体 API 消耗列表(跨所有 Key,支持筛选和分页)。"""
|
||||
# 构建查询
|
||||
query = select(ApiUsageLog, ApiKey.company_name, ApiKey.api_key_prefix).join(
|
||||
ApiKey, ApiUsageLog.api_key_id == ApiKey.id
|
||||
)
|
||||
count_query = select(func.count(ApiUsageLog.id)).join(
|
||||
ApiKey, ApiUsageLog.api_key_id == ApiKey.id
|
||||
)
|
||||
|
||||
# 筛选条件
|
||||
filters = []
|
||||
if api_key_id:
|
||||
filters.append(ApiUsageLog.api_key_id == api_key_id)
|
||||
if gen_type:
|
||||
filters.append(ApiUsageLog.gen_type == gen_type)
|
||||
if status_filter:
|
||||
filters.append(ApiUsageLog.status == status_filter)
|
||||
if start_date:
|
||||
filters.append(ApiUsageLog.created_at >= start_date)
|
||||
if end_date:
|
||||
filters.append(ApiUsageLog.created_at <= end_date)
|
||||
|
||||
for f in filters:
|
||||
query = query.where(f)
|
||||
count_query = count_query.where(f)
|
||||
|
||||
# 总数
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
# 分页查询
|
||||
query = query.order_by(ApiUsageLog.created_at.desc()).offset(skip).limit(limit)
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
items = []
|
||||
for log, company_name, key_prefix in rows:
|
||||
items.append({
|
||||
"id": log.id,
|
||||
"apiKeyId": log.api_key_id,
|
||||
"companyName": company_name,
|
||||
"apiKeyPrefix": f"{key_prefix}****" if key_prefix else None,
|
||||
"taskId": log.api_generation_task_id,
|
||||
"requestType": log.request_type,
|
||||
"modelName": log.model_name,
|
||||
"genType": log.gen_type,
|
||||
"creditsCost": log.credits_cost,
|
||||
"tokensUsed": log.tokens_used,
|
||||
"requestDurationMs": log.request_duration_ms,
|
||||
"status": log.status,
|
||||
"errorMessage": log.error_message,
|
||||
"errorCode": log.error_code,
|
||||
"createdAt": log.created_at.isoformat() if log.created_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"items": items,
|
||||
}
|
||||
Reference in New Issue
Block a user