feat: 片段批量操作API - 重排序 + 批量删除
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 1m6s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 32s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m13s
CI/CD Pipeline / Unit Tests (push) Successful in 3m14s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 1m6s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 32s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m13s
CI/CD Pipeline / Unit Tests (push) Successful in 3m14s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
新增 2 个批量操作接口:
- POST /{plan_id}/clips/reorder 批量重排序(前端拖拽后一次性提交)
- POST /{plan_id}/clips/batch-delete 批量删除片段
自动触发编辑状态回退(completed/failed → editing)
自动校验片段归属(不属于该plan的片段会被拒绝/跳过)
This commit is contained in:
@@ -461,6 +461,7 @@ def delete_plan(
|
||||
|
||||
from .edit_plans_ai import router as ai_router
|
||||
from .edit_plans_clips import router as clips_router
|
||||
from .edit_plans_clips_batch import router as clips_batch_router
|
||||
from .edit_plans_generation import router as generation_router
|
||||
from .edit_plans_timeline import router as timeline_router
|
||||
|
||||
@@ -468,3 +469,4 @@ router.include_router(generation_router)
|
||||
router.include_router(ai_router)
|
||||
router.include_router(timeline_router)
|
||||
router.include_router(clips_router, prefix="/{plan_id}/clips", tags=["EditPlan Clips"])
|
||||
router.include_router(clips_batch_router, prefix="/{plan_id}/clips", tags=["EditPlan Clips"])
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
"""剪辑计划片段批量操作 API。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import get_db_session, get_project_repository
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ── Schemas ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ClipReorderItem(BaseModel):
|
||||
"""重排序条目"""
|
||||
|
||||
clip_id: str
|
||||
new_order: int = Field(..., ge=0, description="新的排序序号")
|
||||
|
||||
|
||||
class ClipReorderRequest(BaseModel):
|
||||
"""片段重排序请求"""
|
||||
|
||||
items: List[ClipReorderItem] = Field(..., min_length=1, max_length=500, description="重排序条目列表")
|
||||
|
||||
|
||||
class ClipReorderResponse(BaseModel):
|
||||
"""片段重排序响应"""
|
||||
|
||||
success: bool
|
||||
updated_count: int
|
||||
message: str = ""
|
||||
|
||||
|
||||
class ClipBatchDeleteRequest(BaseModel):
|
||||
"""批量删除片段请求"""
|
||||
|
||||
clip_ids: List[str] = Field(..., min_length=1, max_length=500, description="要删除的片段ID列表")
|
||||
|
||||
|
||||
class ClipBatchDeleteResponse(BaseModel):
|
||||
"""批量删除片段响应"""
|
||||
|
||||
success: bool
|
||||
deleted_count: int
|
||||
message: str = ""
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _check_plan_access(plan_id: str, user_id: str, project_repository: Any, db: Session) -> Any:
|
||||
"""验证用户是否有权限访问该剪辑计划,返回 plan 对象。"""
|
||||
from ._helpers import check_project_access
|
||||
from app.services.edit_plan_service import EditPlanService
|
||||
|
||||
svc = EditPlanService(db)
|
||||
plan = svc.get_plan(plan_id)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=404, detail=f"剪辑计划不存在: {plan_id}")
|
||||
if plan.project_id:
|
||||
check_project_access(plan.project_id, user_id, project_repository)
|
||||
return plan
|
||||
|
||||
|
||||
def _get_svc(db: Session):
|
||||
"""获取 EditPlanService 实例"""
|
||||
from app.services.edit_plan_service import EditPlanService
|
||||
|
||||
return EditPlanService(db)
|
||||
|
||||
|
||||
# ── Routes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("/reorder", response_model=ClipReorderResponse)
|
||||
def reorder_clips(
|
||||
plan_id: str,
|
||||
body: ClipReorderRequest,
|
||||
db: Session = Depends(get_db_session),
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
) -> ClipReorderResponse:
|
||||
"""批量重排序片段
|
||||
|
||||
前端拖拽调整顺序后,一次性提交所有变更的 order。
|
||||
自动触发编辑状态回退(从 completed/failed 切回 editing)。
|
||||
"""
|
||||
_check_plan_access(plan_id, current_user.user.id, project_repository, db)
|
||||
|
||||
svc = _get_svc(db)
|
||||
|
||||
# 验证所有 clip 都属于该 plan
|
||||
clip_ids = [item.clip_id for item in body.items]
|
||||
existing_clips = svc.list_clips(plan_id, skip=0, limit=10000)
|
||||
existing_ids = {c.id for c in existing_clips}
|
||||
|
||||
invalid_ids = [cid for cid in clip_ids if cid not in existing_ids]
|
||||
if invalid_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"以下片段不属于该计划: {', '.join(invalid_ids[:5])}",
|
||||
)
|
||||
|
||||
# 执行重排序
|
||||
updated_count = 0
|
||||
for item in body.items:
|
||||
try:
|
||||
svc.update_clip(item.clip_id, order=item.new_order)
|
||||
updated_count += 1
|
||||
except ValueError as e:
|
||||
logger.warning("重排序片段失败: clip_id=%s error=%s", item.clip_id, e)
|
||||
|
||||
logger.info(
|
||||
"批量重排序片段: plan_id=%s count=%d by user=%s",
|
||||
plan_id,
|
||||
updated_count,
|
||||
current_user.user.id,
|
||||
)
|
||||
|
||||
return ClipReorderResponse(
|
||||
success=True,
|
||||
updated_count=updated_count,
|
||||
message=f"成功更新 {updated_count} 个片段的顺序",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/batch-delete", response_model=ClipBatchDeleteResponse)
|
||||
def batch_delete_clips(
|
||||
plan_id: str,
|
||||
body: ClipBatchDeleteRequest,
|
||||
db: Session = Depends(get_db_session),
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
) -> ClipBatchDeleteResponse:
|
||||
"""批量删除片段
|
||||
|
||||
自动触发编辑状态回退(从 completed/failed 切回 editing)。
|
||||
"""
|
||||
_check_plan_access(plan_id, current_user.user.id, project_repository, db)
|
||||
|
||||
svc = _get_svc(db)
|
||||
|
||||
# 验证所有 clip 都属于该 plan
|
||||
existing_clips = svc.list_clips(plan_id, skip=0, limit=10000)
|
||||
existing_ids = {c.id for c in existing_clips}
|
||||
|
||||
valid_ids = [cid for cid in body.clip_ids if cid in existing_ids]
|
||||
skipped = len(body.clip_ids) - len(valid_ids)
|
||||
|
||||
# 执行删除
|
||||
deleted_count = 0
|
||||
for clip_id in valid_ids:
|
||||
if svc.delete_clip(clip_id):
|
||||
deleted_count += 1
|
||||
|
||||
message = f"成功删除 {deleted_count} 个片段"
|
||||
if skipped > 0:
|
||||
message += f",跳过 {skipped} 个不存在的片段"
|
||||
|
||||
logger.info(
|
||||
"批量删除片段: plan_id=%s deleted=%d skipped=%d by user=%s",
|
||||
plan_id,
|
||||
deleted_count,
|
||||
skipped,
|
||||
current_user.user.id,
|
||||
)
|
||||
|
||||
return ClipBatchDeleteResponse(
|
||||
success=True,
|
||||
deleted_count=deleted_count,
|
||||
message=message,
|
||||
)
|
||||
Reference in New Issue
Block a user