Files
xiaoxia-saas/packages/application/jobs.py
T
灵应 8935196fcd
Deploy / Staging E2E Tests (push) Has been skipped
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 138h4m33s
CI/CD Pipeline / Frontend Lint (push) Failing after 138h4m39s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 138h4m39s
style: 后端代码black格式化
2026-07-03 18:49:54 +08:00

266 lines
8.2 KiB
Python
Executable File
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.
"""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 任务重置为 pendingretry_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,
}