Files
xiaoxia-saas/apps/api/app/services/job_service.py
T
xiaoxia 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环境兼容 + 测试修复 (#286)
fix(api+ci): DELETE 204响应体全修复 + isort/black整理 + CI环境兼容 + 测试修复
2026-07-14 07:07:51 +08:00

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