b4e3bb0fe7
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 47s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 50s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m27s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m19s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m21s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m25s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
195 lines
6.4 KiB
Python
Executable File
195 lines
6.4 KiB
Python
Executable File
"""草稿管理路由.
|
|
|
|
端点:
|
|
- GET / 获取草稿详情
|
|
- PUT / 更新草稿
|
|
- POST /publish 发布草稿到模板
|
|
- GET /versions 模板版本历史
|
|
- POST /rollback 回滚到指定版本
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.services.edit_plan_service import EditPlanService
|
|
from app.services.edit_template_service import EditTemplateService
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
from .dependencies import get_draft_plan_id, get_editor_services
|
|
from .schemas import (
|
|
EditorClipBatchUpdateRequest,
|
|
EditorClipBatchUpdateResponse,
|
|
EditorDraftResponse,
|
|
EditorPublishResponse,
|
|
EditorRollbackRequest,
|
|
EditorRollbackResponse,
|
|
EditorTemplateVersionItem,
|
|
EditorUpdateRequest,
|
|
EditorVersionListResponse,
|
|
)
|
|
|
|
router = APIRouter(tags=["Template Editor"])
|
|
|
|
|
|
@router.get("", response_model=EditorDraftResponse)
|
|
def get_editor_draft(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""获取模板编辑器草稿详情
|
|
|
|
首次访问时自动创建草稿。
|
|
"""
|
|
_, plan_svc = services
|
|
plan = plan_svc.get_plan_or_raise(plan_id)
|
|
clips = plan_svc.list_clips(plan_id)
|
|
return EditorDraftResponse(
|
|
plan_id=plan.id,
|
|
template_id=plan.template_id,
|
|
name=plan.name,
|
|
status=plan.status.value if hasattr(plan.status, "value") else str(plan.status),
|
|
config=plan.config or {},
|
|
total_duration=plan.total_duration,
|
|
clip_count=len(clips),
|
|
)
|
|
|
|
|
|
@router.put("", response_model=EditorDraftResponse)
|
|
def update_editor_draft(
|
|
template_id: str,
|
|
req: EditorUpdateRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""更新模板编辑器草稿"""
|
|
_, plan_svc = services
|
|
plan = plan_svc.update_plan(
|
|
plan_id,
|
|
name=req.name,
|
|
config=req.config,
|
|
total_duration=req.total_duration,
|
|
)
|
|
clips = plan_svc.list_clips(plan_id)
|
|
return EditorDraftResponse(
|
|
plan_id=plan.id,
|
|
template_id=plan.template_id,
|
|
name=plan.name,
|
|
status=plan.status.value if hasattr(plan.status, "value") else str(plan.status),
|
|
config=plan.config or {},
|
|
total_duration=plan.total_duration,
|
|
clip_count=len(clips),
|
|
)
|
|
|
|
|
|
@router.post("/publish", response_model=EditorPublishResponse, status_code=status.HTTP_200_OK)
|
|
def publish_draft_to_template(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""将草稿发布(同步)到正式模板
|
|
|
|
草稿的 config 和 clips 会同步覆盖到模板,事务保证一致性。
|
|
"""
|
|
tpl_svc, plan_svc = services
|
|
try:
|
|
tpl = tpl_svc.publish_template_from_draft(template_id, plan_id)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
clips = plan_svc.list_clips(plan_id)
|
|
return EditorPublishResponse(
|
|
template_id=tpl.id,
|
|
status="published",
|
|
clip_count=len(clips),
|
|
version=tpl.version,
|
|
)
|
|
|
|
|
|
@router.get("/versions", response_model=EditorVersionListResponse)
|
|
def list_template_versions(
|
|
template_id: str,
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
):
|
|
"""查询模板发布版本历史"""
|
|
tpl_svc, _ = services
|
|
versions = tpl_svc.list_template_versions(template_id, limit=limit)
|
|
items = [
|
|
EditorTemplateVersionItem(
|
|
version=v.version,
|
|
name=v.name,
|
|
editing_mode=v.editing_mode,
|
|
clip_count=len(v.clip_configs),
|
|
change_note=v.change_note,
|
|
published_by=v.published_by,
|
|
created_at=(v.created_at.isoformat() if hasattr(v.created_at, "isoformat") else str(v.created_at)),
|
|
)
|
|
for v in versions
|
|
]
|
|
return EditorVersionListResponse(versions=items, total=len(items))
|
|
|
|
|
|
@router.post("/rollback", response_model=EditorRollbackResponse, status_code=status.HTTP_200_OK)
|
|
def rollback_template(
|
|
template_id: str,
|
|
request: EditorRollbackRequest,
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""回滚模板到指定历史版本
|
|
|
|
回滚本身也是一次发布,版本号会 +1,可以再次回滚。
|
|
"""
|
|
tpl_svc, _ = services
|
|
try:
|
|
tpl = tpl_svc.rollback_to_version(template_id, request.version)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
clip_configs = tpl_svc.list_clip_configs(template_id)
|
|
return EditorRollbackResponse(
|
|
template_id=tpl.id,
|
|
status="rolled_back",
|
|
rollback_to_version=request.version,
|
|
new_version=tpl.version,
|
|
clip_count=len(clip_configs),
|
|
)
|
|
|
|
|
|
@router.put("/clips", response_model=EditorClipBatchUpdateResponse)
|
|
def batch_update_clips(
|
|
template_id: str,
|
|
req: EditorClipBatchUpdateRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""批量替换草稿clips(全量覆盖,用于前端选择素材后同步片段)
|
|
|
|
事务保证:清空→创建→标记ready 在同一数据库事务内完成,
|
|
任何步骤失败时自动回滚,避免数据不一致。
|
|
"""
|
|
_, plan_svc = services
|
|
plan_svc.get_plan_or_raise(plan_id)
|
|
|
|
clips_data = []
|
|
for clip_item in req.clips:
|
|
item = {
|
|
"asset_id": clip_item.asset_id,
|
|
"start_time": clip_item.start_time,
|
|
"duration": clip_item.duration,
|
|
}
|
|
if clip_item.order is not None:
|
|
item["order"] = clip_item.order
|
|
clips_data.append(item)
|
|
|
|
plan_svc.replace_all_clips_transactional(plan_id, clips_data)
|
|
|
|
return EditorClipBatchUpdateResponse(plan_id=plan_id, clip_count=len(req.clips))
|