ForcePilot/backend/server/routers/channels/reports_router.py
Kris ddafd95ff0 refactor(routers): 整理并新增多组渠道相关路由功能
1.  移除多个导出接口的显式response_model声明
2.  调整access_rule和test_case的创建接口位置,修复静态路径冲突
3.  优化适配器配置校验的异常处理逻辑
4.  重构集成路由的查询逻辑,统一使用get_integration_or_raise
5.  新增channels路由组下的capability、reports、dashboard、webhook、wizard、doctor、directory、session共8个子路由模块
6.  注册channels_router到全局路由列表
2026-07-02 03:29:06 +08:00

174 lines
6.8 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.

"""Reports 聚合视图域 RouterRPT-05 / RPT-06 / RPT-ONEOFF-01 / RPT-ONEOFF-02
统一采用模板 A控制面端口路由鉴权依赖 get_admin_user调用
use_cases.report_management.<method>raiseOnControlFailure 转译失败,
serialize_control_data 序列化响应。
路径设计(静态先于动态,避免 /{report_id} 误捕获 /oneoff
- POST /reports/oneoff RPT-05 createOneoffReport
- GET /reports/oneoff RPT-ONEOFF-01 listOneoffReports
- GET /reports/oneoff/{task_id}/download RPT-ONEOFF-02 downloadReport
- POST /reports/oneoff/{task_id}/retry RPT-ONEOFF-RETRY retryReport
- GET /reports/{report_id} RPT-06 getReport
子 router 自身不设置 prefix根前缀 ``/channels`` 由 ``channels_router``
聚合 router 统一追加。
"""
from __future__ import annotations
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
get_channel_use_cases,
parse_datetime,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
reports_router = APIRouter(tags=["channels-reports"])
class CreateOneoffReportRequest(BaseModel):
"""创建一次性报告请求体RPT-05
字段:
report_type: 报告类型枚举5 个值)。
params: 报告生成参数(时间范围 / 过滤条件等)。
run_at: 计划执行时间ISO 8601 字符串),为 None 时立即执行。
"""
model_config = ConfigDict(frozen=True)
report_type: Literal[
"message_stats",
"session_stats",
"account_stats",
"delivery_stats",
"dashboard_overview",
] = Field(..., description="报告类型")
params: dict[str, Any] = Field(default_factory=dict, description="报告生成参数")
run_at: str | None = Field(default=None, description="计划执行时间ISO 8601为空时立即执行")
@reports_router.post("/reports/oneoff", response_model=dict)
async def create_oneoff_report(
request: Request,
body: CreateOneoffReportRequest,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建一次性报告RPT-05。对应控制面操作 reports/create_oneoff。
异步生成报告:创建 pending 状态报告记录 + scheduler 一次性任务,
worker 进程通过 ChannelReportHandler 生成内容后状态变 ready。
"""
operator = build_operator(current_user, request)
result = await use_cases.report_management.createOneoffReport(
report_type=body.report_type,
params=body.params,
run_at=body.run_at,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@reports_router.get("/reports/oneoff", response_model=dict)
async def list_oneoff_reports(
request: Request,
status: str | None = Query(default=None, description="按状态过滤pending/generating/ready/failed"),
report_type: str | None = Query(default=None, description="按报告类型过滤"),
start_time: str | None = Query(default=None, description="起始时间ISO 8601"),
end_time: str | None = Query(default=None, description="截止时间ISO 8601"),
limit: int = Query(default=100, ge=1, le=200, description="分页大小1-200"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出一次性报告RPT-ONEOFF-01。对应控制面操作 reports/list_oneoff。
分页查询报告列表,支持按状态 / 类型 / 时间范围过滤。
"""
operator = build_operator(current_user, request)
start_time_dt = parse_datetime("start_time", start_time)
end_time_dt = parse_datetime("end_time", end_time)
result = await use_cases.report_management.listOneoffReports(
operator=operator,
status=status,
report_type=report_type,
start_time=start_time_dt.isoformat() if start_time_dt else None,
end_time=end_time_dt.isoformat() if end_time_dt else None,
limit=limit,
offset=offset,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@reports_router.get("/reports/oneoff/{task_id}/download", response_model=dict)
async def download_oneoff_report(
request: Request,
task_id: str,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""下载一次性报告RPT-ONEOFF-02。对应控制面操作 reports/download。
根据 task_id 下载已就绪报告内容。未就绪pending/generating返回 409
失败failed返回 409。
"""
operator = build_operator(current_user, request)
result = await use_cases.report_management.downloadReport(
task_id=task_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@reports_router.post("/reports/oneoff/{task_id}/retry", response_model=dict)
async def retry_oneoff_report(
request: Request,
task_id: str,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""重试失败报告RPT-ONEOFF-RETRY。对应控制面操作 reports/oneoff_retry。
校验原报告 status=="failed" 后创建新 pending 报告并入队 scheduler
任务,同时标记原报告 ``retried_at``。返回新旧 task_id。
"""
operator = build_operator(current_user, request)
result = await use_cases.report_management.retryReport(
task_id=task_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@reports_router.get("/reports/{report_id}", response_model=dict)
async def get_report(
request: Request,
report_id: str,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询报告详情RPT-06。对应控制面操作 reports/get。
返回报告完整信息(含 content / status / download_url
"""
operator = build_operator(current_user, request)
result = await use_cases.report_management.getReport(
report_id=report_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}