1. config_router: 为expected_version添加ge=1校验 2. directory_router: 补充scope校验逻辑与注释 3. login_router: 拆分强制下线权限,添加参数校验与注释更新 4. reports_router: 统一时间参数处理,修复分页限制使用契约常量 5. dashboard_router: 更新文档与响应格式,修正参数传递逻辑 6. health_router: 缩减健康检查响应字段,修复响应结构与参数校验 7. plugin_router: 新增插件目录端点,补充枚举校验与注释 8. pairing_router: 新增时间过滤参数,补充参数校验 9. __init__.py: 修复异常映射,更新trace_id获取逻辑与工具类 10. doctor_router: 重构单项检查端点,修正注释与校验逻辑 11. account_router: 新增恢复降级账户端点,补充批量操作校验 12. webhook_router: 优化webhook处理逻辑,修复流式读取与响应逻辑 13. content_review_router: 补充批量审核端点,完善参数校验与注释 14. analytics_router: 修正管道阶段描述,统一参数传递 15. wizard_router: 新增OAuth相关端点,重构路由路径与校验逻辑
195 lines
7.9 KiB
Python
195 lines
7.9 KiB
Python
"""Reports 聚合视图域 Router(RPT-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 datetime import datetime, timezone
|
||
from typing import Any, Literal
|
||
|
||
from fastapi import APIRouter, Depends, Query, Request
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from yuxi.channels.contract.dtos.report import (
|
||
DEFAULT_LIST_LIMIT,
|
||
MAX_LIST_LIMIT,
|
||
MIN_LIST_LIMIT,
|
||
)
|
||
from yuxi.channels.contract.errors import ValidationError
|
||
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)
|
||
# 时间参数统一在 Router 层校验格式与业务约束,与 list_oneoff_reports
|
||
# 的 start_time / end_time 处理一致(FR-34 / RPT-05 @pre):
|
||
# - 格式校验:parse_datetime 翻译为 aware UTC datetime,非法格式抛
|
||
# ValidationError(400),避免原生异常在控制面管道内被兜底为 500。
|
||
# - 过去时间校验:run_at 非空时不得为过去时间(Port 契约 @pre),
|
||
# 避免创建永远不会触发的 scheduler 任务。
|
||
# - datetime 直接透传至 UseCase(Port 契约要求 datetime | None),
|
||
# 消除 str → datetime → str → datetime 的冗余往返转换。
|
||
run_at_dt = parse_datetime("run_at", body.run_at)
|
||
if run_at_dt is not None and run_at_dt <= datetime.now(timezone.utc):
|
||
raise ValidationError(
|
||
field="run_at",
|
||
message="run_at must be a future time",
|
||
)
|
||
result = await use_cases.report_management.createOneoffReport(
|
||
report_type=body.report_type,
|
||
params=body.params,
|
||
run_at=run_at_dt,
|
||
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: Literal["pending", "generating", "ready", "failed"] | None = Query(default=None, description="按状态过滤"),
|
||
report_type: Literal["message_stats", "session_stats", "account_stats", "delivery_stats", "dashboard_overview"] | 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=DEFAULT_LIST_LIMIT, ge=MIN_LIST_LIMIT, le=MAX_LIST_LIMIT, 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,
|
||
end_time=end_time_dt,
|
||
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)} |