"""scheduler 限界上下文用例 DTO 定义。 包含: - 任务管理 Input/Output DTO(CRUD / 暂停 / 恢复 / 手动触发) - 执行日志 Input/Output DTO(列表查询) - 可观测性 Output DTO(handler 摘要 / 状态计数 / 即将执行 / 健康检查) - 运维恢复 Input/Output DTO(回收僵尸执行) 字段约束(``min_length`` / ``max_length`` / ``pattern`` / ``ge`` / ``le``)在 DTO 中 声明,由 Pydantic 在构造时校验。DateTime 字段在 Output DTO 中以 ISO 格式字符串 表达(``str | None``),由 Router 层在序列化时格式化。JSON 字段(如 ``payload``) 使用 ``dict[str, Any]``。 对齐 ``external_systems/use_cases/dto/system.py`` 写法。 """ from __future__ import annotations from typing import Any from pydantic import BaseModel, ConfigDict, Field # --------------------------------------------------------------------------- # 任务管理 DTO # --------------------------------------------------------------------------- class CreateTaskInput(BaseModel): """创建定时任务输入 DTO。 ``schedule_kind`` 决定调度类型:``cron`` 必填 ``cron_expression``, ``at`` 必填 ``run_at``。``task_id`` 由系统生成(UUID4),不接受客户端指定。 """ model_config = ConfigDict(frozen=True) handler_name: str = Field( ..., min_length=1, max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]*$", description="handler 标识,与 TaskHandler.name 引用一致", ) owner_scope: str = Field( ..., min_length=1, max_length=64, description="任务归属范围:system(平台级)/ business(业务方)", ) owner_id: str = Field( ..., min_length=1, max_length=128, description="业务方标识,格式如 channel_type:account_id", ) schedule_kind: str = Field( ..., pattern=r"^(cron|at)$", description="调度类型:cron(周期)/ at(一次性)", ) cron_expression: str | None = Field( default=None, max_length=128, description="cron 表达式(标准 5 字段),schedule_kind=cron 时必填", ) run_at: str | None = Field( default=None, description="一次性执行时间(ISO 格式字符串),schedule_kind=at 时必填且须为未来时间", ) tz: str = Field( default="Asia/Shanghai", max_length=64, description="时区标识,如 Asia/Shanghai", ) payload: dict[str, Any] = Field( default_factory=dict, description="任务执行时传给 handler 的参数,序列化后不超过 64KB", ) enabled: bool = Field(default=True, description="是否启用") delete_after_run: bool = Field( default=False, description="一次性任务执行完成后是否自动删除(schedule_kind=at 时有意义)", ) block_strategy: str = Field( default="discard_later", pattern=r"^(discard_later)$", description="阻塞策略:discard_later(存在 running 执行时丢弃本次触发)", ) created_by: str = Field(default="system", max_length=128) class UpdateTaskInput(BaseModel): """更新定时任务输入 DTO。 允许更新字段(PRD §FR-ST-05):``cron_expression`` / ``run_at`` / ``tz`` / ``payload`` / ``enabled`` / ``delete_after_run`` / ``owner_scope`` / ``owner_id``。 ``schedule_kind`` 与 ``handler_name`` 不可修改。修改 ``cron_expression`` 时 由用例层重新计算 ``next_run_at`` 与 ``stagger_seconds``。 """ model_config = ConfigDict(frozen=True) task_id: str = Field( ..., min_length=1, max_length=64, description="任务标识(UUID4)", ) cron_expression: str | None = Field( default=None, max_length=128, description="新的 cron 表达式,传入时重算 next_run_at 与 stagger_seconds", ) run_at: str | None = Field( default=None, description="新的一次性执行时间(ISO 格式字符串)", ) tz: str | None = Field( default=None, max_length=64, description="新的时区标识", ) payload: dict[str, Any] | None = Field( default=None, description="新的 payload,传入时整体替换", ) enabled: bool | None = Field(default=None, description="是否启用") delete_after_run: bool | None = Field( default=None, description="一次性任务执行完成后是否自动删除", ) owner_scope: str | None = Field( default=None, min_length=1, max_length=64, ) owner_id: str | None = Field( default=None, min_length=1, max_length=128, ) updated_by: str | None = Field(default=None, max_length=128) class GetTaskInput(BaseModel): """获取任务详情输入 DTO。""" model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) class DeleteTaskInput(BaseModel): """删除任务输入 DTO(软删除)。""" model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) updated_by: str | None = Field(default=None, max_length=128) class PauseTaskInput(BaseModel): """暂停任务输入 DTO。""" model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) updated_by: str | None = Field(default=None, max_length=128) class ResumeTaskInput(BaseModel): """恢复任务输入 DTO。 对 ``status=paused`` 任务调用 ``pause`` 的逆操作;对 ``status=dead_letter`` 任务重置 ``consecutive_errors`` / ``last_error`` 并恢复为 ``active``。 """ model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) updated_by: str | None = Field(default=None, max_length=128) class TriggerTaskInput(BaseModel): """手动触发任务输入 DTO。 ``triggered_by`` 标识触发来源(``auto`` / ``manual``),手动触发不入正常 ``next_run_at`` 流,不影响下一次自动执行。死信任务允许手动触发用于验证 handler 是否已修复,但不重置 ``status``。 """ model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) triggered_by: str = Field( default="manual", pattern=r"^(auto|manual)$", description="触发来源:auto(tick 自动)/ manual(管理员手动)", ) payload: dict[str, Any] | None = Field( default=None, description="可选 payload 覆盖,None 时使用任务定义中的 payload", ) class TaskOutput(BaseModel): """任务输出 DTO。字段与 ``ScheduledTask`` dataclass 对齐。 DateTime 字段(``run_at`` / ``last_run_at`` / ``next_run_at`` / ``created_at`` / ``updated_at`` / ``deleted_at``)以 ISO 格式字符串表达,由 Router 层格式化。 """ model_config = ConfigDict(frozen=True) task_id: str handler_name: str owner_scope: str owner_id: str schedule_kind: str cron_expression: str | None = None run_at: str | None = None tz: str = "Asia/Shanghai" payload: dict[str, Any] = Field(default_factory=dict) enabled: bool = True delete_after_run: bool = False block_strategy: str = "discard_later" stagger_seconds: int | None = None consecutive_errors: int = 0 status: str = "active" last_run_at: str | None = None next_run_at: str | None = None last_error: str | None = None created_by: str = "system" updated_by: str | None = None id: int | None = None created_at: str | None = None updated_at: str | None = None is_deleted: int = 0 deleted_at: str | None = None class ListTasksInput(BaseModel): """列出任务输入 DTO。 过滤字段对齐 PRD §FR-ST-05 列表 API 查询参数与仓储层 ``list`` 方法签名。 """ model_config = ConfigDict(frozen=True) page: int = Field(default=1, ge=1, description="页码,从 1 开始") page_size: int = Field(default=20, ge=1, le=100, description="每页条数,最大 100") owner_scope: str | None = Field(default=None, max_length=64) owner_id: str | None = Field(default=None, max_length=128) handler_name: str | None = Field(default=None, max_length=128) enabled: bool | None = None status: str | None = Field( default=None, pattern=r"^(active|paused|dead_letter)$", description="任务状态过滤", ) class ListTasksOutput(BaseModel): """列出任务输出 DTO。""" model_config = ConfigDict(frozen=True) items: list[TaskOutput] = Field(default_factory=list) total: int = 0 page: int = 1 page_size: int = 20 # --------------------------------------------------------------------------- # 执行日志 DTO # --------------------------------------------------------------------------- class ListRunLogsInput(BaseModel): """列出执行日志输入 DTO。 ``task_id`` 必填,按任务维度查询执行明细。``status`` / ``start_date`` / ``end_date`` 为可选过滤字段,``start_date`` / ``end_date`` 为 ISO 格式字符串, 由用例层解析为 ``datetime`` 后传入仓储层。 """ model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100) status: str | None = Field( default=None, pattern=r"^(running|success|failure|timeout|skipped)$", description="执行状态过滤", ) start_date: str | None = Field( default=None, description="起始时间(ISO 格式字符串),按 started_at 过滤", ) end_date: str | None = Field( default=None, description="截止时间(ISO 格式字符串),按 started_at 过滤", ) class GetRunLogInput(BaseModel): """获取执行日志详情输入 DTO。""" model_config = ConfigDict(frozen=True) run_id: str = Field(..., min_length=1, max_length=64) class RunLogOutput(BaseModel): """执行日志输出 DTO。字段与 ``ScheduledTaskRunLog`` dataclass 对齐。 DateTime 字段(``started_at`` / ``finished_at`` / ``created_at`` / ``updated_at`` / ``deleted_at``)以 ISO 格式字符串表达。 """ model_config = ConfigDict(frozen=True) task_id: str run_id: str triggered_by: str = "auto" status: str = "running" error_message: str | None = None output: dict[str, Any] | None = None started_at: str | None = None finished_at: str | None = None created_by: str = "system" updated_by: str | None = None id: int | None = None created_at: str | None = None updated_at: str | None = None is_deleted: int = 0 deleted_at: str | None = None class ListRunLogsOutput(BaseModel): """列出执行日志输出 DTO。""" model_config = ConfigDict(frozen=True) items: list[RunLogOutput] = Field(default_factory=list) total: int = 0 page: int = 1 page_size: int = 20 class ListAllRunLogsInput(BaseModel): """跨任务执行日志查询输入 DTO。 ``task_id`` 与 ``status`` 均可选,但不可同时为空(由用例层校验), 避免无过滤的全表扫描。``start_date`` / ``end_date`` 为 ISO 格式字符串, 由用例层解析为 ``datetime`` 后传入仓储层。 """ model_config = ConfigDict(frozen=True) page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100) status: str | None = Field( default=None, pattern=r"^(running|success|failure|timeout|skipped)$", description="执行状态过滤", ) start_date: str | None = Field( default=None, description="起始时间(ISO 格式字符串),按 started_at 过滤", ) end_date: str | None = Field( default=None, description="截止时间(ISO 格式字符串),按 started_at 过滤", ) task_id: str | None = Field(default=None, min_length=1, max_length=64) # --------------------------------------------------------------------------- # 可观测性 DTO # --------------------------------------------------------------------------- class HandlerSummaryOutput(BaseModel): """handler 聚合摘要输出 DTO。 对齐 PRD §FR-ST-05 ``GET /handlers`` 端点响应字段,走 DB 聚合 (``task_repo.list_handler_summary``),不查内存注册表。 """ model_config = ConfigDict(frozen=True) handler_name: str description: str = "" task_count: int = 0 active_task_count: int = 0 last_active_at: str | None = None class ListHandlerSummaryOutput(BaseModel): """handler 聚合摘要列表输出 DTO。""" model_config = ConfigDict(frozen=True) items: list[HandlerSummaryOutput] = Field(default_factory=list) class CountByStatusOutput(BaseModel): """任务状态计数输出 DTO。 对齐 PRD §FR-ST-07 健康检查与指标需求,按 ``status`` 维度统计任务数。 """ model_config = ConfigDict(frozen=True) active: int = 0 paused: int = 0 dead_letter: int = 0 class ListUpcomingInput(BaseModel): """即将执行任务查询输入 DTO。 对齐 PRD §FR-ST-09 统计查询,按 ``next_run_at`` 升序返回即将执行的任务列表。 """ model_config = ConfigDict(frozen=True) limit: int = Field(default=100, ge=1, le=1000, description="返回条数上限") handler_name: str | None = Field(default=None, max_length=128) class ListUpcomingOutput(BaseModel): """即将执行任务列表输出 DTO。""" model_config = ConfigDict(frozen=True) items: list[TaskOutput] = Field(default_factory=list) class GetHealthOutput(BaseModel): """调度器健康检查输出 DTO。 对齐 PRD §FR-ST-07 健康检查端点响应:``status`` 表示 worker 是否在最近 5 分钟内执行 tick;``last_tick_at`` 为最近 tick 时间;``active_task_count`` / ``dead_letter_count`` / ``running_count`` 为关键状态计数。 """ model_config = ConfigDict(frozen=True) status: str = Field( default="healthy", pattern=r"^(healthy|unhealthy)$", description="健康状态:healthy(最近 5 分钟内有 tick)/ unhealthy", ) last_tick_at: str | None = Field(default=None, description="最近 tick 时间(ISO 格式字符串)") active_task_count: int = 0 dead_letter_count: int = 0 running_count: int = 0 # --------------------------------------------------------------------------- # 日聚合统计 DTO # --------------------------------------------------------------------------- class ListDailyStatsInput(BaseModel): """日聚合统计查询输入 DTO。 ``start_date`` / ``end_date`` 为 ISO 日期字符串(``YYYY-MM-DD``),由用例层 解析为 ``date`` 后传入仓储层。日期范围上限 90 天。 """ model_config = ConfigDict(frozen=True) start_date: str = Field(..., description="ISO 日期 YYYY-MM-DD") end_date: str = Field(..., description="ISO 日期 YYYY-MM-DD") handler_name: str | None = Field(default=None, max_length=128) class DailyStatOutput(BaseModel): """日聚合统计输出 DTO。字段对齐 ``DailyStat`` dataclass,补充派生字段。 ``total_count`` / ``success_rate`` 为派生字段,由 mapper 计算: ``total_count = success_count + failure_count + timeout_count``, ``success_rate = success_count / total_count if total_count > 0 else 0.0``。 """ model_config = ConfigDict(frozen=True) stat_date: str handler_name: str success_count: int = 0 failure_count: int = 0 timeout_count: int = 0 dead_letter_count: int = 0 total_count: int = 0 success_rate: float = 0.0 class ListDailyStatsOutput(BaseModel): """日聚合统计列表输出 DTO。""" model_config = ConfigDict(frozen=True) items: list[DailyStatOutput] = Field(default_factory=list) # --------------------------------------------------------------------------- # 运维恢复 DTO # --------------------------------------------------------------------------- class ReclaimStaleRunsInput(BaseModel): """回收僵尸执行输入 DTO。 用于 scheduler 重启后回收 ``status=running`` 但实际已超时的执行记录, 将其标记为 ``failure`` 并写入回收错误信息。``timeout_seconds`` 为可选参数, 未传入时由用例层从配置取默认值。 """ model_config = ConfigDict(frozen=True) timeout_seconds: int | None = Field( default=None, ge=1, description="执行超时阈值(秒),None 时从配置取默认值", ) class ReclaimStaleRunsOutput(BaseModel): """回收僵尸执行输出 DTO。""" model_config = ConfigDict(frozen=True) reclaimed_count: int = 0 # --------------------------------------------------------------------------- # 任务回收站 DTO # --------------------------------------------------------------------------- class ListDeletedTasksInput(BaseModel): """列出已删除任务(回收站)输入 DTO。 ``start_date`` / ``end_date`` 为 ISO 格式字符串,由用例层解析为 ``datetime`` 后按 ``deleted_at`` 过滤。 """ model_config = ConfigDict(frozen=True) page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100) start_date: str | None = Field( default=None, description="起始时间(ISO 格式字符串),按 deleted_at 过滤", ) end_date: str | None = Field( default=None, description="截止时间(ISO 格式字符串),按 deleted_at 过滤", ) class RestoreTaskInput(BaseModel): """恢复已删除任务输入 DTO。""" model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) updated_by: str | None = Field(default=None, max_length=128) class HardDeleteTaskInput(BaseModel): """硬删除任务输入 DTO。""" model_config = ConfigDict(frozen=True) task_id: str = Field(..., min_length=1, max_length=64) updated_by: str | None = Field(default=None, max_length=128)