ForcePilot/backend/package/yuxi/channels/contract/dtos/report.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

177 lines
6.3 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.

"""报告 DTO 定义。
包含报告聚合根 DTO、查询条件 DTO 与报告类型/状态枚举常量,以及 Report ↔ dict
互转的工具函数(供应用层响应序列化使用,避免反向依赖 adapters/mappers
对齐《19-聚合视图域-reports-router-设计方案.md》§5.3。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Literal
from yuxi.channels.contract.errors import ValidationError
#: 报告类型枚举
REPORT_TYPES = frozenset(
{
"message_stats",
"session_stats",
"account_stats",
"delivery_stats",
"dashboard_overview",
}
)
#: 报告状态枚举单向流转pending → generating → ready / failed
REPORT_STATUSES = frozenset(
{
"pending",
"generating",
"ready",
"failed",
}
)
#: reports/list_oneoff 默认分页大小
DEFAULT_LIST_LIMIT: int = 100
#: reports/list_oneoff 分页下界
MIN_LIST_LIMIT: int = 1
#: reports/list_oneoff 分页上界
MAX_LIST_LIMIT: int = 200
@dataclass(frozen=True)
class Report:
"""报告聚合根 DTORPT-001
字段:
report_id: 报告唯一标识UUID``rpt_`` 前缀)。
task_id: 关联的 scheduler 任务 IDUUID
report_type: 报告类型(取值 ``REPORT_TYPES``)。
status: 报告状态(取值 ``REPORT_STATUSES``)。
params: 报告生成参数JSON如时间范围、过滤条件
content: 报告内容JSON仅 status=ready 时非空)。
error_message: 失败原因(仅 status=failed 时非空)。
created_at: 创建时间。
ready_at: 就绪时间status=ready 时非空)。
created_by: 创建人operator.user_id
retried_from: 重试来源报告的 task_id仅重试生成的新报告非空
retried_at: 原报告被重试的时间(原报告标记字段,新报告为 None
"""
report_id: str
task_id: str
report_type: Literal["message_stats", "session_stats", "account_stats", "delivery_stats", "dashboard_overview"]
status: Literal["pending", "generating", "ready", "failed"]
params: dict[str, Any] = field(default_factory=dict)
content: dict[str, Any] | None = None
error_message: str | None = None
created_at: datetime | None = None
ready_at: datetime | None = None
created_by: str = "system"
retried_from: str | None = None
retried_at: datetime | None = None
@dataclass(frozen=True)
class ReportQuery:
"""报告查询条件RPT-001
字段:
status: 按状态过滤(可选)。
report_type: 按报告类型过滤(可选)。
start_time: 起始时间(按 created_at 过滤,可选)。
end_time: 截止时间(按 created_at 过滤,可选)。
limit: 分页大小(默认 100上限 200
offset: 分页偏移(默认 0
"""
status: Literal["pending", "generating", "ready", "failed"] | None = None
report_type: (
Literal["message_stats", "session_stats", "account_stats", "delivery_stats", "dashboard_overview"] | None
) = None
start_time: datetime | None = None
end_time: datetime | None = None
limit: int = DEFAULT_LIST_LIMIT
offset: int = 0
def __post_init__(self) -> None:
"""校验时间范围与分页参数。
``start_time`` 与 ``end_time`` 同时提供时,``start_time`` 必须早于
``end_time````limit`` 必须在 ``MIN_LIST_LIMIT``-``MAX_LIST_LIMIT``
之间,``offset`` 必须为非负整数。在构造时即抛出 ``ValidationError``
adapter 不再做该校验INV-8
"""
if self.start_time is not None and self.end_time is not None:
if self.start_time >= self.end_time:
raise ValidationError(
"time_range",
"start_time must be earlier than end_time",
)
if self.limit < MIN_LIST_LIMIT or self.limit > MAX_LIST_LIMIT:
raise ValidationError(
"limit",
f"limit must be in [{MIN_LIST_LIMIT}, {MAX_LIST_LIMIT}]",
)
if self.offset < 0:
raise ValidationError("offset", "must be a non-negative integer")
def reportToDict(report: Report) -> dict[str, Any]:
"""Report DTO → dictdispatch handler 返回值序列化)。
纯 DTO 序列化函数,不依赖 ORM供应用层控制面分派与 router 响应使用。
包含 error_message失败原因与 retried_at重试时间供前端详情页展示。
"""
return {
"report_id": report.report_id,
"task_id": report.task_id,
"report_type": report.report_type,
"status": report.status,
"params": report.params,
"content": report.content,
"error_message": report.error_message,
"created_at": report.created_at.isoformat() if report.created_at else None,
"ready_at": report.ready_at.isoformat() if report.ready_at else None,
"created_by": report.created_by,
"retried_at": report.retried_at.isoformat() if report.retried_at else None,
"download_url": f"/api/channels/reports/oneoff/{report.task_id}/download",
}
def reportSummaryToDict(report: Report) -> dict[str, Any]:
"""Report DTO → 摘要 dict列表项序列化
纯 DTO 序列化函数,不依赖 ORM供应用层列表响应使用。
"""
return {
"report_id": report.report_id,
"task_id": report.task_id,
"report_type": report.report_type,
"status": report.status,
"created_at": report.created_at.isoformat() if report.created_at else None,
"ready_at": report.ready_at.isoformat() if report.ready_at else None,
"created_by": report.created_by,
}
@dataclass(frozen=True)
class RetryReportResult:
"""重试失败报告结果RPT-ONEOFF-RETRY
描述重试操作的返回结果,包括原任务 ID 与新生成任务 ID
``ReportManagementPort.retryReport`` 引用。
字段:
task_id: 原失败报告任务 ID。
new_task_id: 重试生成的新报告任务 ID。
retried_at: 重试时间戳。
"""
task_id: str
new_task_id: str
retried_at: datetime