881eea9195
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 36s
CI/CD Pipeline / Frontend Lint (push) Successful in 51s
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Successful in 56s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 9s
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 1m50s
fix(api+ci): DELETE 204响应体全修复 + isort/black整理 + CI环境兼容 + 测试修复
267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
"""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
|