Files
ai-talk-callback/app/routes.py

297 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Request, HTTPException, Depends, Path
from sqlalchemy.ext.asyncio import AsyncSession
import httpx
import asyncio
from typing import Dict, Any, Optional
from app.database import get_db, CallbackLog, ExternalApiLog
from app.models import CallbackRequest, CallbackResponse
from app.redis_lock import redis_manager
from app.config import settings
from app.logger import get_logger
logger = get_logger("routes")
router = APIRouter()
async def log_callback_request(
db: AsyncSession,
request: Request,
site_id: str,
callback_data: CallbackRequest
) -> Optional[int]:
"""记录回调请求到数据库,返回记录ID"""
import json
# 获取客户端IP地址
client_ip = request.client.host if request.client else None
client_port = request.client.port if request.client else None
# 获取服务器IP地址
server_ip = None
server_port = None
if hasattr(request, 'scope') and 'server' in request.scope:
server_host, server_port_info = request.scope['server']
server_ip = server_host
server_port = server_port_info
# 准备请求头信息(过滤敏感信息)
request_headers = dict(request.headers)
safe_headers = {}
sensitive_headers = {'authorization', 'token', 'api-key', 'x-api-key', 'cookie'}
for key, value in request_headers.items():
if key.lower() in sensitive_headers:
safe_headers[key] = "***REDACTED***"
else:
safe_headers[key] = value
# 准备请求体信息
request_body = callback_data.model_dump()
# 记录site_id(JSON格式)
logger.info(f"📝 site_id: {json.dumps(site_id, ensure_ascii=False)}")
# 记录请求头(JSON格式)
logger.info(f"📋 请求头: {json.dumps(safe_headers, ensure_ascii=False, indent=2)}")
# 记录请求体(JSON格式)
logger.info(f"📄 请求体: {json.dumps(request_body, ensure_ascii=False, indent=2)}")
# 记录server_ip(JSON格式)
server_info = {
"ip": server_ip,
"port": server_port
}
logger.info(f"🏠 server_ip: {json.dumps(server_info, ensure_ascii=False)}")
# 记录client_ip(JSON格式)
client_info = {
"ip": client_ip,
"port": client_port
}
logger.info(f"🖥️ client_ip: {json.dumps(client_info, ensure_ascii=False)}")
# 记录callback_data(JSON格式)
callback_data_json = {
"count": callback_data.count,
"data_count": len(callback_data.data) if callback_data.data else 0,
"data_sample": callback_data.data[0].model_dump() if callback_data.data else None
}
logger.info(f"📦 callback_data: {json.dumps(callback_data_json, ensure_ascii=False, indent=2)}")
try:
# 保存到数据库
callback_log = CallbackLog(
site_id=site_id,
remote_address=f"{client_ip}:{client_port}" if client_ip and client_port else client_ip,
server_ip=f"{server_ip}:{server_port}" if server_ip and server_port else server_ip,
request_url=str(request.url),
request_headers=safe_headers, # 保存过滤后的请求头
request_body=request_body
)
db.add(callback_log)
await db.commit()
logger.info(f"✅ 回调请求记录成功保存到数据库,ID: {callback_log.id}")
# 返回记录ID
return callback_log.id
except Exception as e:
logger.error(f"❌ 保存回调请求到数据库失败: {e}", exc_info=True)
# 不重新抛出异常,避免影响主业务流程
return None
async def log_external_api_request(
db: AsyncSession,
callback_logs_id: int,
request_url: str,
request_headers: Dict[str, Any],
request_body: Dict[str, Any],
response_status: Optional[int],
response_headers: Optional[Dict[str, Any]],
response_body: Optional[str],
retry_count: int = 0
):
"""记录外部API请求到数据库"""
external_api_log = ExternalApiLog(
callback_logs_id=callback_logs_id,
request_url=request_url,
request_headers=request_headers,
request_body=request_body,
response_status=response_status,
response_headers=response_headers,
response_body=response_body,
retry_count=retry_count
)
db.add(external_api_log)
await db.commit()
async def call_external_api_with_retry(
db: AsyncSession,
request_body: Dict[str, Any],
max_retries: int = None,
callback_logs_id: int = None
) -> tuple[bool, int]:
"""
调用外部API并支持重试机制
Args:
db: 数据库会话
request_body: 请求体
max_retries: 最大重试次数
Returns:
tuple[是否成功, 实际重试次数]
"""
if max_retries is None:
max_retries = settings.external_api_retry_max
logger.info(f"🌐 开始调用外部API: {settings.external_api_url}, 最大重试次数: {max_retries}")
headers = {
"Content-Type": "application/json",
"User-Agent": "AITalkCallbackService/1.0"
}
for attempt in range(1, max_retries+1):
try:
logger.debug(f"📤 第{attempt + 1}次尝试调用外部API")
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
settings.external_api_url,
headers=headers,
json=request_body
)
logger.debug(f"📥 外部API响应: status={response.status_code}")
# 记录每次尝试的结果
await log_external_api_request(
db=db,
callback_logs_id=callback_logs_id,
request_url=settings.external_api_url,
request_headers=headers,
request_body=request_body,
response_status=response.status_code,
response_headers=dict(response.headers),
response_body=response.text,
retry_count=attempt
)
# 检查响应状态
if response.status_code < 400:
logger.info(f"✅ 外部API调用成功,状态码: {response.status_code}")
return True, attempt
else:
logger.warning(f"⚠️ 外部API返回错误状态码: {response.status_code}")
# 如果是最后一次尝试,直接返回失败
if attempt == max_retries:
logger.error(f"❌ 外部API调用最终失败,状态码: {response.status_code}")
return False, attempt
# 否则等待一段时间后重试
wait_time = 1 * (attempt + 1) # 递增延迟
logger.info(f"⏳ 等待 {wait_time}秒 后重试...")
await asyncio.sleep(wait_time)
except httpx.RequestError as e:
logger.warning(f"⚠️ 外部API网络错误 (第{attempt + 1}次尝试): {e}")
# 记录网络错误
await log_external_api_request(
db=db,
callback_logs_id=callback_logs_id,
request_url=settings.external_api_url,
request_headers=headers,
request_body=request_body,
response_status=None,
response_headers=None,
response_body=f"RequestError: {str(e)}",
retry_count=attempt
)
# 如果是最后一次尝试,直接返回失败
if attempt == max_retries:
logger.error(f"❌ 外部API调用最终失败: {e}")
return False, attempt
# 否则等待一段时间后重试
wait_time = 1 * (attempt + 1) # 递增延迟
logger.info(f"⏳ 等待 {wait_time}秒 后重试...")
await asyncio.sleep(wait_time)
return False, max_retries
@router.post("/ai-talk/callback/{siteId}/failure", response_model=CallbackResponse)
async def ai_talk_callback(
callback_data: CallbackRequest,
request: Request,
siteId: str = Path(..., description="站点ID"),
db: AsyncSession = Depends(get_db)
):
"""
AI Talk回调接口处理
"""
logger.info(f"🔥 收到AI Talk回调请求: siteId={siteId}, count={callback_data.count}, data_count={len(callback_data.data)}")
try:
# 记录回调请求(包含siteId)
callback_log_id = await log_callback_request(db, request, siteId, callback_data)
# 判断count是否大于等于阈值,如果是直接返回
if callback_data.count >= settings.count_threshold:
logger.info(f"✅ count={callback_data.count} >= {settings.count_threshold},直接返回")
return CallbackResponse(
success=True,
message=f"count={callback_data.count} >= {settings.count_threshold},直接返回",
processed=False,
retry_count=0,
site_id=siteId
)
logger.info(f"📞 count={callback_data.count} < {settings.count_threshold},调用外部API")
# count < 3,调用外部API,使用分布式锁防止并发调用
async with redis_manager.create_lock(f"external_api_call_{siteId}_{callback_data.count}"):
logger.debug(f"🔒 获取Redis锁成功: external_api_call_{siteId}_{callback_data.count}")
# 调用外部API并支持重试
request_body = callback_data.model_dump()
success, retry_count = await call_external_api_with_retry(
db=db,
request_body=request_body,
max_retries=settings.external_api_retry_max,
callback_logs_id=callback_log_id
)
if success:
logger.info(f"✅ 外部API调用成功,重试次数: {retry_count}")
return CallbackResponse(
success=True,
message="外部API调用成功",
processed=True,
retry_count=retry_count,
site_id=siteId
)
else:
logger.error(f"❌ 外部API调用失败,已重试{retry_count}次")
raise HTTPException(
status_code=500,
detail=f"外部API调用失败,已重试{retry_count}次"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"💥 服务器内部错误: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"服务器内部错误: {str(e)}"
)