9c6c477f55
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
P0 关键修复: - P0-1: 注册接口添加 RateLimitMiddleware 限流保护 - P0-3: /metrics 端点添加 JWT 认证(移除匿名访问) - P0-4: 修复 Celery 任务名冲突(generation_task vs generate_video) - P1-5: JWT logout token 黑名单机制 P1 修复: - P1-1: forgot_password 硬编码 localhost → 使用 settings.APP_BASE_URL - P1-2: generation.py 直接创建 DB 连接 → 使用依赖注入 - P1-6: Image.open() 未关闭 → 统一使用 with 语句 - P1-7: 订阅续费事务修复 P2 代码质量: - P2-1: 修复 EditingMode 枚举重复定义 → 统一引用 shared 包 - P2-2: 修复 SMTP_FRON_NAME → SMTP_FROM_NAME 拼写 - P2-3: UserModel subscription_quota 类型统一为 float - P2-4: .env.production DATABASE_MAX_OVERFLOW 30 → 10 - 清理 15 处 except:pass(保留 2 处有注释说明的) - 禁用 SVG 上传(XSS 风险) - 删除 decode_token_unsafe() 不安全函数 - 简化 /ready 端点 - 删除 8 处死代码、10 个空文件/模块 - 合并 3 对 100% 重复函数 - 对齐 6 个废弃环境变量 v2 修复(代码审查后): - 修复密码重置路由路径: /password/forgot → /forgot-password, /password/reset → /reset-password(与前端 API 对齐) - 合并 _check_project_access: asset_libraries.py 和 edit_plans.py 中的重复函数统一到 _helpers.py(含空字符串守卫 + 中文错误信息) - 顺手修复: HTTPException 统一从 fastapi 导入(替换 starlette 导入) - OSS_ENDPOINT 拼写修复拆分为单独 PR,本 PR 不包含
264 lines
8.1 KiB
Python
Executable File
264 lines
8.1 KiB
Python
Executable File
"""JobService 应用层用例 — Phase 8 任务 2.10.
|
||
|
||
提供任务创建、提交、状态管理、进度追踪、失败重试、查询等核心能力。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from typing import Any
|
||
|
||
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,
|
||
}
|