"""JobService 应用层用例 — Phase 8 任务 2.10. 提供任务创建、提交、状态管理、进度追踪、失败重试、查询等核心能力。 """ from __future__ import annotations import logging from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any from uuid import uuid4 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, }