"""报告 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