"""JobService 服务层 — Phase 8 任务 2.10. 将 JobService 与 VideoComposeService 集成,提供视频合成的完整异步工作流: 1. 创建 Job(记录任务元数据) 2. 提交执行(dispatch Celery 任务) 3. Celery 任务中更新进度、处理完成/失败 同时也提供通用的 Job 管理能力,供 ClipPlanService、RenderOrchestrator 等使用。 """ from __future__ import annotations import logging from typing import Any from packages.application.jobs import ( CancelJobUseCase, CompleteJobCommand, CompleteJobUseCase, CreateJobCommand, CreateJobUseCase, FailJobCommand, FailJobUseCase, GetJobStatisticsUseCase, GetJobUseCase, ListJobsUseCase, RetryJobUseCase, SubmitJobUseCase, UpdateJobProgressCommand, UpdateJobProgressUseCase, ) from packages.domain.job import Job, JobStatus, JobType from packages.ports.job_repository import JobRepository logger = logging.getLogger(__name__) class JobService: """统一异步任务管理服务。 职责: - 为视频合成等耗时操作提供统一的异步任务管理 - 封装 Use Case 的调用,提供简洁的服务接口 - 与 VideoComposeService 集成,支持视频合成工作流 用法:: job_service = JobService(db) job = job_service.create_compose_job( project_id="xxx", plan_id="yyy", user_id="zzz", ) job_service.submit_job(job.id, celery_task_id="celery-xxx") """ def __init__(self, job_repo: JobRepository): self._job_repo = job_repo # ── 创建任务 ────────────────────────────────────────────────────────── def create_compose_job( self, project_id: str, plan_id: str, user_id: str, *, max_retries: int = 3, ) -> Job: """创建视频合成任务。 Args: project_id: 项目 ID plan_id: EditPlan ID user_id: 创建人 ID max_retries: 最大重试次数 Returns: 创建的 Job 实例 """ use_case = CreateJobUseCase(self._job_repo) return use_case.execute( CreateJobCommand( project_id=project_id, job_type=JobType.VIDEO_COMPOSE, payload={"plan_id": plan_id}, source_id=plan_id, created_by_user_id=user_id, max_retries=max_retries, ) ) def create_render_job( self, project_id: str, plan_id: str, user_id: str, *, max_retries: int = 3, ) -> Job: """创建剪辑计划渲染任务。""" use_case = CreateJobUseCase(self._job_repo) return use_case.execute( CreateJobCommand( project_id=project_id, job_type=JobType.RENDER_EDIT_PLAN, payload={"plan_id": plan_id}, source_id=plan_id, created_by_user_id=user_id, max_retries=max_retries, ) ) def create_job( self, project_id: str, job_type: JobType | str, *, payload: dict | None = None, source_id: str = "", user_id: str = "", max_retries: int = 3, ) -> Job: """创建通用任务。""" use_case = CreateJobUseCase(self._job_repo) return use_case.execute( CreateJobCommand( project_id=project_id, job_type=job_type, payload=payload or {}, source_id=source_id, created_by_user_id=user_id, max_retries=max_retries, ) ) # ── 提交执行 ────────────────────────────────────────────────────────── def submit_job(self, job_id: str, celery_task_id: str = "") -> Job: """提交任务执行。""" use_case = SubmitJobUseCase(self._job_repo) return use_case.execute(job_id, celery_task_id) # ── 进度更新 ────────────────────────────────────────────────────────── def update_progress(self, job_id: str, progress: float, stage: str = "") -> Job: """更新任务进度。""" use_case = UpdateJobProgressUseCase(self._job_repo) return use_case.execute( UpdateJobProgressCommand( job_id=job_id, progress=progress, current_stage=stage, ) ) # ── 完成 / 失败 ──────────────────────────────────────────────────────── def complete_job(self, job_id: str, result: dict | None = None) -> Job: """标记任务完成。""" use_case = CompleteJobUseCase(self._job_repo) return use_case.execute(CompleteJobCommand(job_id=job_id, result=result or {})) def fail_job(self, job_id: str, error_message: str) -> Job: """标记任务失败。""" use_case = FailJobUseCase(self._job_repo) return use_case.execute(FailJobCommand(job_id=job_id, error_message=error_message)) # ── 重试 / 取消 ──────────────────────────────────────────────────────── def retry_job(self, job_id: str) -> Job: """重试失败任务。""" use_case = RetryJobUseCase(self._job_repo) return use_case.execute(job_id) def cancel_job(self, job_id: str) -> Job: """取消任务。""" use_case = CancelJobUseCase(self._job_repo) return use_case.execute(job_id) # ── 查询 ───────────────────────────────────────────────────────────── def get_job(self, job_id: str) -> Job | None: """获取任务详情。""" use_case = GetJobUseCase(self._job_repo) return use_case.execute(job_id) def list_project_jobs( self, project_id: str, *, job_type: JobType | str | None = None, status: JobStatus | str | None = None, limit: int = 50, offset: int = 0, ) -> list[Job]: """获取项目下的任务列表。""" use_case = ListJobsUseCase(self._job_repo) return use_case.execute( project_id=project_id, job_type=job_type, status=status, limit=limit, offset=offset, ) def list_user_jobs( self, user_id: str, *, job_type: JobType | str | None = None, status: JobStatus | str | None = None, limit: int = 50, offset: int = 0, ) -> list[Job]: """获取用户的任务列表。""" use_case = ListJobsUseCase(self._job_repo) return use_case.execute( user_id=user_id, job_type=job_type, status=status, limit=limit, offset=offset, ) def get_statistics(self, project_id: str) -> dict[str, Any]: """获取项目任务统计。""" use_case = GetJobStatisticsUseCase(self._job_repo) return use_case.execute(project_id) # ── 防重复检查 ──────────────────────────────────────────────────────── def has_active_job_for_source(self, source_id: str, job_type: JobType | str) -> bool: """检查是否已有活跃任务(防止重复提交)。 Args: source_id: 关联的业务实体 ID job_type: 任务类型 Returns: True 如果存在活跃任务 """ return self._job_repo.find_active_by_source(source_id, job_type) is not None # ── 便捷方法:带防重的视频合成提交 ────────────────────────────────────── def submit_compose_if_not_exists( self, project_id: str, plan_id: str, user_id: str, celery_task_id: str = "", ) -> tuple[Job, bool]: """创建并提交视频合成任务(防重复)。 Returns: (job, created): job 实例和是否新创建的标志 """ if self.has_active_job_for_source(plan_id, JobType.VIDEO_COMPOSE): existing = self._job_repo.find_active_by_source(plan_id, JobType.VIDEO_COMPOSE) logger.info("已存在活跃的视频合成任务: job_id=%s plan_id=%s", existing.id, plan_id) return existing, False job = self.create_compose_job(project_id, plan_id, user_id) job = self.submit_job(job.id, celery_task_id) return job, True