Files
jdy_fastapi/app/api/routes.py
T
2025-11-14 11:04:01 +08:00

172 lines
6.1 KiB
Python

"""
API 路由定义模块
本模块定义所有 API 路由端点,包括:
- /health: 健康检查端点
- /webhook: Webhook 端点,处理简道云插件的请求
所有路由都使用 FastAPI 的依赖注入系统,通过 dependencies.py 中的函数注入依赖项。
"""
from fastapi import APIRouter, Request, HTTPException, status, Depends
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from typing import Dict, Any
import json
import anyio
import asyncio
import logging
from app.schemas import WebhookRequest, WebhookResponse, HealthResponse
from app.api.dependencies import (
get_logger,
get_app_tools,
get_f6_plugin_module,
get_action_map
)
from app.utils.app_tools import AppTools
# 创建路由器
# 使用 APIRouter 分离路由,便于管理和维护
router = APIRouter()
@router.get("/health", response_model=HealthResponse, tags=["系统"])
async def healthcheck():
"""
健康检查端点
用于检查服务是否正常运行
"""
return HealthResponse(status="ok", version="1.0.0")
@router.post("/webhook", response_model=WebhookResponse, tags=["业务"])
async def webhook(
request: Request,
logger: logging.Logger = Depends(get_logger),
app_tools: AppTools = Depends(get_app_tools),
f6_plugin_module = Depends(get_f6_plugin_module),
action_map: Dict[str, Any] = Depends(get_action_map)
):
"""
接受前端请求后将任务放入消息队列
此端点接收简道云插件的请求,根据请求头中的 Action 字段路由到相应的处理函数。
支持的操作包括:登录、获取公司信息、文件校验、品牌创建等。
Args:
request: FastAPI 请求对象,包含请求体和请求头
logger: 日志记录器
app_tools: 应用工具实例
f6_plugin_module: F6插件模块实例
action_map: 操作映射表
Returns:
WebhookResponse: 任务处理结果
Raises:
HTTPException: 当操作类型无效或任务执行超时时抛出
"""
try:
# 获取请求数据并验证
try:
raw_data = await request.json()
# 使用 Pydantic 进行数据验证(允许额外字段)
webhook_data = WebhookRequest(**raw_data)
data = webhook_data.dict(exclude_none=True)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请求体必须是有效的 JSON 格式"
)
except Exception as e:
logger.warning(f"请求数据验证失败: {str(e)}")
# 如果验证失败,仍然尝试使用原始数据(向后兼容)
data = raw_data if 'raw_data' in locals() else {}
# 获取并解码请求头
header = request.headers
decoded_header = app_tools.decode_headers(header)
# 验证 Action 字段
action = decoded_header.get('Action')
if not action:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="请求头中缺少必需的 Action 字段"
)
# 处理 F6_Plugin 特殊逻辑
if action == 'F6_Plugin':
check = decoded_header.get('Check')
if check == '':
handler = f6_plugin_module.check_file
elif check == '':
sub_action = data.get('Action')
if not sub_action:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="F6_Plugin 操作需要提供 Action 字段"
)
handler = action_map.get(sub_action)
if not handler:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"未知的子操作类型: {sub_action}"
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"F6_Plugin 操作需要提供有效的 Check 字段(是/否),当前值: {check}"
)
else:
handler = action_map.get(action)
if not handler:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"未知的操作类型: {action}。支持的操作: {', '.join(action_map.keys())}"
)
logger.info(f"接收到操作请求: {action}, 数据ID: {data.get('data_id', 'N/A')}")
# 将任务放入消息队列
response_queue = app_tools.enqueue_task(handler, data)
# 等待任务处理结果(添加超时保护,简道云默认60秒)
try:
# 使用 asyncio.wait_for 添加超时
result = await asyncio.wait_for(
anyio.to_thread.run_sync(response_queue.get),
timeout=55.0
)
except asyncio.TimeoutError:
logger.error(f"任务执行超时: {action}, 数据ID: {data.get('data_id', 'N/A')}")
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail="任务执行超时,请稍后重试"
)
# 验证返回结果格式
if not isinstance(result, dict):
result = {"msg": str(result)}
if "msg" not in result:
result["msg"] = "操作完成"
logger.info(f"操作完成: {action}, 结果: {json.dumps(result, ensure_ascii=False)}")
# 返回响应(使用 Pydantic 模型验证)
return WebhookResponse(**result)
except HTTPException:
# 重新抛出 HTTP 异常
raise
except Exception as e:
# 捕获其他未预期的异常
logger.error(f"处理请求时发生未预期的错误: {type(e).__name__} - {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"处理请求时发生错误: {str(e)}"
)