"""JobService 应用层用例 — Phase 8 任务 2.10. 提供任务创建、提交、状态管理、进度追踪、失败重试、查询等核心能力。 """ from __future__ import annotations import logging from dataclasses import dataclass, field from typing import Any from packages.domain.job import Job, JobStatus, JobType from packages.ports.job_repository import JobRepository logger = logging.getLogger(__name__) # ── 命令对象 ────────────────────────────────────────────────────────────────── @dataclass(slots=True) class CreateJobCommand: """创建任务的命令。""" project_id: str job_type: JobType | str payload: dict = field(default_factory=dict) source_id: str = "" created_by_user_id: str = "" max_retries: int = 3 @dataclass(slots=True) class UpdateJobProgressCommand: """更新任务进度的命令。""" job_id: str progress: float current_stage: str = "" @dataclass(slots=True) class CompleteJobCommand: """完成任务的命令。""" job_id: str result: dict = field(default_factory=dict) @dataclass(slots=True) class FailJobCommand: """标记任务失败的命令。""" job_id: str error_message: str # ── 用例 ───────────────────────────────────────────────────────────────────── class CreateJobUseCase: """创建异步任务。 创建后状态为 pending,不自动提交到 Celery(由调用方决定何时 dispatch)。 """ def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, command: CreateJobCommand) -> Job: job = Job.create( project_id=command.project_id, job_type=command.job_type, payload=command.payload, source_id=command.source_id, created_by_user_id=command.created_by_user_id, max_retries=command.max_retries, ) logger.info( "创建任务: job_id=%s type=%s project=%s", job.id, job.job_type.value, job.project_id, ) return self._job_repo.create(job) class SubmitJobUseCase: """提交任务执行:将任务状态切换为 running 并返回 Celery 任务 ID 占位。 实际的 Celery dispatch 由上层(路由层 / 服务层)完成,本用例仅做状态流转。 """ def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, job_id: str, celery_task_id: str = "") -> Job: job = self._job_repo.get(job_id) if job is None: raise ValueError(f"任务不存在: {job_id}") if job.status != JobStatus.PENDING: raise ValueError(f"只有 pending 状态的任务可以提交,当前状态: {job.status.value}") job.mark_running(stage="已提交,等待执行") job.celery_task_id = celery_task_id return self._job_repo.update(job) class UpdateJobProgressUseCase: """更新任务进度。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, command: UpdateJobProgressCommand) -> Job: job = self._job_repo.get(command.job_id) if job is None: raise ValueError(f"任务不存在: {command.job_id}") if job.status != JobStatus.RUNNING: raise ValueError(f"只有 running 状态的任务可以更新进度,当前状态: {job.status.value}") job.update_progress(command.progress, command.current_stage) return self._job_repo.update(job) class CompleteJobUseCase: """标记任务成功完成。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, command: CompleteJobCommand) -> Job: job = self._job_repo.get(command.job_id) if job is None: raise ValueError(f"任务不存在: {command.job_id}") if job.status not in (JobStatus.RUNNING, JobStatus.PENDING): raise ValueError(f"只有 running/pending 状态的任务可以标记完成,当前状态: {job.status.value}") job.mark_success(result=command.result) logger.info("任务完成: job_id=%s", job.id) return self._job_repo.update(job) class FailJobUseCase: """标记任务失败。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, command: FailJobCommand) -> Job: job = self._job_repo.get(command.job_id) if job is None: raise ValueError(f"任务不存在: {command.job_id}") job.mark_failed(command.error_message) logger.warning("任务失败: job_id=%s error=%s", job.id, command.error_message) return self._job_repo.update(job) class RetryJobUseCase: """重试失败任务。 将 failed 任务重置为 pending,retry_count + 1。 实际的 Celery 重新 dispatch 由调用方完成。 """ def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, job_id: str) -> Job: job = self._job_repo.get(job_id) if job is None: raise ValueError(f"任务不存在: {job_id}") job.prepare_retry() logger.info("任务重试: job_id=%s retry_count=%d", job.id, job.retry_count) return self._job_repo.update(job) class CancelJobUseCase: """取消任务。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, job_id: str) -> Job: job = self._job_repo.get(job_id) if job is None: raise ValueError(f"任务不存在: {job_id}") if job.is_terminal: raise ValueError(f"任务已处于终态,无法取消: {job.status.value}") job.mark_cancelled() logger.info("任务取消: job_id=%s", job.id) return self._job_repo.update(job) class GetJobUseCase: """获取单个任务详情。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, job_id: str) -> Job | None: return self._job_repo.get(job_id) class ListJobsUseCase: """列出任务(支持按项目 / 用户 / 类型 / 状态过滤)。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute( self, *, project_id: str | None = None, user_id: str | None = None, job_type: JobType | str | None = None, status: JobStatus | str | None = None, limit: int = 50, offset: int = 0, ) -> list[Job]: if project_id: return self._job_repo.list_by_project( project_id, job_type=job_type, status=status, limit=limit, offset=offset, ) if user_id: return self._job_repo.list_by_user( user_id, job_type=job_type, status=status, limit=limit, offset=offset, ) raise ValueError("必须指定 project_id 或 user_id") class GetJobStatisticsUseCase: """获取项目的任务统计摘要。""" def __init__(self, job_repo: JobRepository): self._job_repo = job_repo def execute(self, project_id: str) -> dict[str, Any]: total = self._job_repo.count_by_project(project_id) pending = self._job_repo.count_by_project(project_id, status=JobStatus.PENDING) running = self._job_repo.count_by_project(project_id, status=JobStatus.RUNNING) success = self._job_repo.count_by_project(project_id, status=JobStatus.SUCCESS) failed = self._job_repo.count_by_project(project_id, status=JobStatus.FAILED) return { "project_id": project_id, "total": total, "pending": pending, "running": running, "success": success, "failed": failed, }