简道云V2.0
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
FastAPI 依赖注入模块
|
||||
|
||||
本模块提供 FastAPI 的依赖注入函数,用于在路由中注入可复用的依赖项。
|
||||
使用依赖注入可以减少代码重复,提高可测试性和可维护性。
|
||||
|
||||
提供的依赖项:
|
||||
- get_logger: 获取日志记录器
|
||||
- get_app_tools: 获取应用工具实例
|
||||
- get_f6_module: 获取 F6Module 实例
|
||||
- get_f6_plugin_module: 获取 F6PluginModule 实例
|
||||
- get_other_module: 获取 OtherPluginModule 实例
|
||||
- get_action_map: 获取操作映射表
|
||||
"""
|
||||
from fastapi import Request
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
|
||||
def get_logger(request: Request) -> logging.Logger:
|
||||
"""
|
||||
获取日志记录器依赖项
|
||||
|
||||
从应用状态中获取日志记录器,如果未初始化则返回一个基本的 logger。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
logging.Logger: 日志记录器实例
|
||||
"""
|
||||
logger = getattr(request.app.state, 'logger', None)
|
||||
if logger is None:
|
||||
# 如果 logger 未初始化,返回一个基本的 logger
|
||||
logger = logging.getLogger('app')
|
||||
return logger
|
||||
|
||||
|
||||
def get_app_tools(request: Request):
|
||||
"""
|
||||
获取应用工具实例依赖项
|
||||
|
||||
从应用状态中获取 AppTools 实例,用于任务队列管理等操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
AppTools: 应用工具实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 AppTools 未初始化
|
||||
"""
|
||||
from app.utils.app_tools import AppTools
|
||||
app_tools = getattr(request.app.state, 'app_tools', None)
|
||||
if app_tools is None:
|
||||
raise RuntimeError("AppTools 未初始化")
|
||||
return app_tools
|
||||
|
||||
|
||||
def get_f6_module(request: Request):
|
||||
"""
|
||||
获取 F6Module 实例依赖项
|
||||
|
||||
从应用状态中获取 F6Module 实例,用于 F6 系统相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
F6Module: F6Module 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 F6Module 未初始化
|
||||
"""
|
||||
f6_module = getattr(request.app.state, 'f6_module', None)
|
||||
if f6_module is None:
|
||||
raise RuntimeError("F6Module 未初始化")
|
||||
return f6_module
|
||||
|
||||
|
||||
def get_f6_plugin_module(request: Request):
|
||||
"""
|
||||
获取 F6PluginModule 实例依赖项
|
||||
|
||||
从应用状态中获取 F6PluginModule 实例,用于 F6 插件相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
F6PluginModule: F6PluginModule 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 F6PluginModule 未初始化
|
||||
"""
|
||||
f6_plugin_module = getattr(request.app.state, 'f6_plugin_module', None)
|
||||
if f6_plugin_module is None:
|
||||
raise RuntimeError("F6PluginModule 未初始化")
|
||||
return f6_plugin_module
|
||||
|
||||
|
||||
def get_other_module(request: Request):
|
||||
"""
|
||||
获取 OtherPluginModule 实例依赖项
|
||||
|
||||
从应用状态中获取 OtherPluginModule 实例,用于其他插件相关操作。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
OtherPluginModule: OtherPluginModule 实例
|
||||
|
||||
Raises:
|
||||
RuntimeError: 如果 OtherPluginModule 未初始化
|
||||
"""
|
||||
other_module = getattr(request.app.state, 'other_module', None)
|
||||
if other_module is None:
|
||||
raise RuntimeError("OtherPluginModule 未初始化")
|
||||
return other_module
|
||||
|
||||
|
||||
def get_action_map(request: Request) -> dict:
|
||||
"""
|
||||
获取操作映射表依赖项
|
||||
|
||||
从模块注册表中获取所有注册的操作,并转换为字典格式(操作名 -> 处理函数)。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
dict: 操作映射表,格式为 {操作名: 处理函数}
|
||||
"""
|
||||
from app.core.module_registry import registry
|
||||
|
||||
# 从 registry 获取所有注册的操作
|
||||
actions = registry.get_all_actions()
|
||||
|
||||
# 转换为字典格式(handler 函数)
|
||||
action_map = {
|
||||
action_name: config.handler
|
||||
for action_name, config in actions.items()
|
||||
}
|
||||
|
||||
return action_map
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
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)}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user