本次提交包含多项代码优化与规范修正: 1. 文档与注释优化:修正注释术语、补充注解与FR编号 2. 代码格式调整:统一空格、换行与缩进规范 3. 类型与接口完善:补充__all__导出、修正返回类型注解 4. 错误处理增强:新增领域错误类与校验逻辑 5. 依赖与导入调整:修复路径引用、统一时区导入 6. 协议与契约更新:完善接口文档与一致性注解
177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
"""报告 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:
|
||
"""报告聚合根 DTO(RPT-001)。
|
||
|
||
字段:
|
||
report_id: 报告唯一标识(UUID,``rpt_`` 前缀)。
|
||
task_id: 关联的 scheduler 任务 ID(UUID)。
|
||
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 → dict(dispatch 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
|