ForcePilot/backend/package/yuxi/scheduler/use_cases/mappers.py
Kris e9d2486e20 feat(scheduler): 新增活跃任务数、手动触发返回run_id等多项功能增强
本次提交对调度器进行了多维度优化:
1. 扩展HandlerSummary模型,新增active_task_count字段并完善聚合逻辑
2. 重构手动触发任务接口,新增TriggerTaskOutput返回run_id与任务快照
3. 为幂等仓储添加operation参数隔离不同操作的缓存,修复幂等校验冲突问题
4. 为任务统计与列表接口添加分页总页数计算,完善回收站分页支持
5. 新增任务按handler、owner过滤的统计能力,优化更新任务的字段校验与重算逻辑
6. 为删除、恢复、硬删除操作添加幂等保护,完善操作审计与错误处理
2026-07-11 07:38:59 +08:00

220 lines
7.8 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.

"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换。
规范 §6.1/§7.3(对齐 ``external_systems/use_cases/mappers.py``
- persistence 返回领域模型 dataclassservice 返回 Output DTO
- scheduler 域无敏感字段mapper 仅做字段映射与 DateTime → ISO 字符串格式化
- use_cases 层不调用 ORM 的 ``to_dict`` 方法
DateTime 字段(``run_at`` / ``last_run_at`` / ``next_run_at`` / ``started_at`` /
``finished_at`` / ``created_at`` / ``updated_at`` / ``deleted_at``)在 dataclass
中以 naive UTC ``datetime`` 表达mapper 通过 ``format_utc_datetime`` 统一格式化
为 ISO 8601 字符串写入 Output DTO。
"""
from __future__ import annotations
from typing import Any
from yuxi.scheduler.core.models import (
DailyStat,
HandlerSummary,
ScheduledTask,
ScheduledTaskRunLog,
)
from yuxi.scheduler.use_cases.dto.scheduler import (
CountByStatusOutput,
DailyStatOutput,
GetHealthOutput,
HandlerSummaryOutput,
ListDailyStatsOutput,
ListHandlerSummaryOutput,
ListUpcomingOutput,
RunLogOutput,
TaskOutput,
TriggerTaskOutput,
)
from yuxi.utils.datetime_utils import format_utc_datetime
__all__ = [
"daily_stat_to_dict",
"to_count_by_status_output",
"to_daily_stat_output",
"to_get_health_output",
"to_handler_summary_output",
"to_list_daily_stats_output",
"to_run_log_output",
"to_task_output",
"to_trigger_task_output",
]
def to_task_output(task: ScheduledTask) -> TaskOutput:
"""``ScheduledTask`` dataclass → ``TaskOutput`` DTO。
DateTime 字段通过 ``format_utc_datetime`` 格式化为 ISO 8601 字符串。
"""
return TaskOutput(
task_id=task.task_id,
handler_name=task.handler_name,
owner_scope=task.owner_scope,
owner_id=task.owner_id,
schedule_kind=task.schedule_kind,
cron_expression=task.cron_expression,
run_at=format_utc_datetime(task.run_at),
tz=task.tz,
payload=task.payload or {},
enabled=task.enabled,
delete_after_run=task.delete_after_run,
block_strategy=task.block_strategy,
stagger_seconds=task.stagger_seconds,
consecutive_errors=task.consecutive_errors,
status=task.status,
last_run_at=format_utc_datetime(task.last_run_at),
next_run_at=format_utc_datetime(task.next_run_at),
last_error=task.last_error,
created_by=task.created_by,
updated_by=task.updated_by,
id=task.id,
created_at=format_utc_datetime(task.created_at),
updated_at=format_utc_datetime(task.updated_at),
is_deleted=task.is_deleted,
deleted_at=format_utc_datetime(task.deleted_at),
)
def to_trigger_task_output(task: ScheduledTask, run_id: str) -> TriggerTaskOutput:
"""``ScheduledTask`` dataclass + run_id → ``TriggerTaskOutput`` DTO。"""
return TriggerTaskOutput(run_id=run_id, task=to_task_output(task))
def to_run_log_output(log: ScheduledTaskRunLog) -> RunLogOutput:
"""``ScheduledTaskRunLog`` dataclass → ``RunLogOutput`` DTO。
计算派生字段 ``duration_seconds``:当 ``started_at`` 与 ``finished_at``
均非 None 时返回秒级执行时长,否则返回 Nonerunning 状态)。
"""
duration_seconds: float | None = None
if log.started_at is not None and log.finished_at is not None:
delta = log.finished_at - log.started_at
duration_seconds = round(delta.total_seconds(), 3)
return RunLogOutput(
task_id=log.task_id,
run_id=log.run_id,
triggered_by=log.triggered_by,
status=log.status,
error_message=log.error_message,
output=log.output,
started_at=format_utc_datetime(log.started_at),
finished_at=format_utc_datetime(log.finished_at),
duration_seconds=duration_seconds,
created_by=log.created_by,
updated_by=log.updated_by,
id=log.id,
created_at=format_utc_datetime(log.created_at),
updated_at=format_utc_datetime(log.updated_at),
is_deleted=log.is_deleted,
deleted_at=format_utc_datetime(log.deleted_at),
)
def to_handler_summary_output(summary: HandlerSummary) -> HandlerSummaryOutput:
"""``HandlerSummary`` dataclass → ``HandlerSummaryOutput`` DTO。
``description`` 字段仓储层不聚合DB 无此列),由调用方(如 worker 进程
持有 ``HandlerRegistry`` 时按需补充API 进程无注册表时保持空字符串。
``active_task_count`` 已由仓储层聚合,此处直接透传。
"""
return HandlerSummaryOutput(
handler_name=summary.name,
task_count=summary.task_count,
active_task_count=summary.active_task_count,
last_active_at=format_utc_datetime(summary.last_active_at),
)
def to_count_by_status_output(dist: dict[str, int]) -> CountByStatusOutput:
"""状态分布字典 → ``CountByStatusOutput`` DTO。
仓储层 ``count_by_status`` 返回 ``{status: count}``,未出现的状态不在字典中,
这里统一补 0。
"""
return CountByStatusOutput(
active=dist.get("active", 0),
paused=dist.get("paused", 0),
dead_letter=dist.get("dead_letter", 0),
)
def to_get_health_output(
*,
last_tick_at: Any,
active_task_count: int,
dead_letter_count: int,
running_count: int,
healthy: bool,
) -> GetHealthOutput:
"""构造 ``GetHealthOutput`` DTO。
``healthy`` 由调用方按"最近 tick 是否在 5 分钟内"判断后传入。
"""
return GetHealthOutput(
status="healthy" if healthy else "unhealthy",
last_tick_at=format_utc_datetime(last_tick_at),
active_task_count=active_task_count,
dead_letter_count=dead_letter_count,
running_count=running_count,
)
def daily_stat_to_dict(stat: DailyStat) -> dict[str, Any]:
"""``DailyStat`` dataclass → dict供指标导出使用
指标服务(``SchedulerMetricService``)从 ``DailyStat`` 提取字段构造
Prometheus 文本格式,此处提供统一的 dict 转换避免重复字段读取。
"""
return {
"stat_date": stat.stat_date,
"handler_name": stat.handler_name,
"success_count": stat.success_count,
"failure_count": stat.failure_count,
"timeout_count": stat.timeout_count,
"dead_letter_count": stat.dead_letter_count,
}
def to_daily_stat_output(stat: DailyStat) -> DailyStatOutput:
"""``DailyStat`` dataclass → ``DailyStatOutput`` DTO。
计算派生字段 ``total_count`` / ``success_rate````stat_date`` 格式化为
ISO 日期字符串(``YYYY-MM-DD``)。
"""
total_count = stat.success_count + stat.failure_count + stat.timeout_count
success_rate = stat.success_count / total_count if total_count > 0 else 0.0
return DailyStatOutput(
stat_date=stat.stat_date.isoformat(),
handler_name=stat.handler_name,
success_count=stat.success_count,
failure_count=stat.failure_count,
timeout_count=stat.timeout_count,
dead_letter_count=stat.dead_letter_count,
total_count=total_count,
success_rate=success_rate,
)
def to_list_daily_stats_output(stats: list[DailyStat]) -> ListDailyStatsOutput:
"""``DailyStat`` 列表 → ``ListDailyStatsOutput`` DTO。"""
return ListDailyStatsOutput(items=[to_daily_stat_output(s) for s in stats])
def to_list_upcoming_output(tasks: list[ScheduledTask]) -> ListUpcomingOutput:
"""``ScheduledTask`` 列表 → ``ListUpcomingOutput`` DTO。"""
return ListUpcomingOutput(items=[to_task_output(t) for t in tasks])
def to_list_handler_summary_output(
summaries: list[HandlerSummary],
) -> ListHandlerSummaryOutput:
"""``HandlerSummary`` 列表 → ``ListHandlerSummaryOutput`` DTO。"""
return ListHandlerSummaryOutput(items=[to_handler_summary_output(s) for s in summaries])