356df4663e
Deploy / Staging E2E Tests (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 111h7m48s
CI/CD Pipeline / Frontend Lint (push) Failing after 111h7m57s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 111h7m57s
Task #105: 修复剪辑计划/模板页面报服务器繁忙 - templates.py: list_templates/get_template/list_categories 加 try/except - templates.py: 新增 POST /{template_id}/toggle-favorite 兼容端点 - edit_plans.py: list_plans 加 try/except,ai_tasks 加 ImportError 守卫 Task #106: 修复素材库诊断按钮 + 缩略图/视频URL - asset_diagnosis.py: get_project_asset_diagnosis 加 try/except - assets.py: 注入 storage_service,生成签名 file_url - asset.py schema: 新增 file_url 字段 - 视频素材 thumbnail_url 为空时复用 file_url 作为封面 其他: - edit_plans/generation_tasks 支持 source_edit_plan_id - Alembic 迁移 022: 两表加 source_edit_plan_id 字段
322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""Template CRUD + generate + category routes."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from app.auth import AuthenticatedUser, get_current_user
|
||
from app.dependencies import get_db_session
|
||
from app.schemas.template import (
|
||
CategoryResponse,
|
||
CreateCategoryRequest,
|
||
CreateTemplateRequest,
|
||
GenerateWarningResponse,
|
||
ListCategoriesResponse,
|
||
ListTemplatesResponse,
|
||
SegmentResponse,
|
||
TemplateResponse,
|
||
ToggleFavoriteResponse,
|
||
UpdateTemplateRequest,
|
||
ValidateTemplateRequest,
|
||
ValidateTemplateResponse,
|
||
)
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
from packages.adapters.sqlalchemy_impl.template_repository import SQLAlchemyTemplateRepository
|
||
from packages.application.template.commands import (
|
||
CreateCategoryCommand,
|
||
CreateTemplateCommand,
|
||
SegmentCommand,
|
||
UpdateTemplateCommand,
|
||
ValidateTemplateCommand,
|
||
)
|
||
from packages.application.template.use_cases import (
|
||
CreateCategoryUseCase,
|
||
CreateTemplateUseCase,
|
||
DeleteCategoryUseCase,
|
||
DeleteTemplateUseCase,
|
||
GetTemplateUseCase,
|
||
ListCategoriesUseCase,
|
||
ListTemplatesUseCase,
|
||
NotFoundError,
|
||
UpdateTemplateUseCase,
|
||
ValidateTemplateUseCase,
|
||
ValidationError,
|
||
)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
def _get_template_repository(session: Session = Depends(get_db_session)) -> SQLAlchemyTemplateRepository:
|
||
return SQLAlchemyTemplateRepository(session)
|
||
|
||
|
||
def _segment_to_response(seg) -> SegmentResponse:
|
||
return SegmentResponse(
|
||
id=seg.id,
|
||
template_id=seg.template_id,
|
||
segment_order=seg.segment_order,
|
||
duration_min=seg.duration_min,
|
||
duration_max=seg.duration_max,
|
||
material_type=seg.material_type,
|
||
created_at=seg.created_at,
|
||
updated_at=seg.updated_at,
|
||
)
|
||
|
||
|
||
def _to_response(template) -> TemplateResponse:
|
||
return TemplateResponse(
|
||
id=template.id,
|
||
user_id=template.user_id,
|
||
name=template.name,
|
||
mode=template.mode,
|
||
category=template.category,
|
||
tags=template.tags,
|
||
title_config=template.title_config,
|
||
subtitle_config=template.subtitle_config,
|
||
bgm_config=template.bgm_config,
|
||
estimated_duration=template.estimated_duration,
|
||
segments=[_segment_to_response(s) for s in getattr(template, "segments", [])],
|
||
is_active=template.is_active,
|
||
created_at=template.created_at,
|
||
updated_at=template.updated_at,
|
||
)
|
||
|
||
|
||
# ── Template CRUD ──
|
||
|
||
|
||
@router.get("", response_model=ListTemplatesResponse)
|
||
def list_templates(
|
||
skip: int = Query(0, ge=0),
|
||
limit: int = Query(50, ge=1, le=200),
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ListTemplatesResponse:
|
||
user_id = authenticated_user.user.id
|
||
try:
|
||
use_case = ListTemplatesUseCase(template_repository)
|
||
templates = use_case.execute(user_id, skip=skip, limit=limit)
|
||
total = template_repository.count_by_user(user_id)
|
||
except Exception:
|
||
logger.exception("list_templates 查询失败: user_id=%s", user_id)
|
||
return ListTemplatesResponse(items=[], total=0)
|
||
return ListTemplatesResponse(
|
||
items=[_to_response(t) for t in templates],
|
||
total=total,
|
||
)
|
||
|
||
|
||
@router.get("/{template_id}", response_model=TemplateResponse)
|
||
def get_template(
|
||
template_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> TemplateResponse:
|
||
user_id = authenticated_user.user.id
|
||
try:
|
||
use_case = GetTemplateUseCase(template_repository)
|
||
template = use_case.execute(template_id, user_id)
|
||
except Exception:
|
||
logger.exception("get_template 查询失败: template_id=%s", template_id)
|
||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="模板查询失败")
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
return _to_response(template)
|
||
|
||
|
||
@router.post("", response_model=TemplateResponse, status_code=status.HTTP_201_CREATED)
|
||
def create_template(
|
||
request: CreateTemplateRequest,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> TemplateResponse:
|
||
user_id = authenticated_user.user.id
|
||
command = CreateTemplateCommand(
|
||
user_id=user_id,
|
||
name=request.name,
|
||
mode=request.mode,
|
||
category=request.category,
|
||
tags=request.tags,
|
||
title_config=request.title_config,
|
||
subtitle_config=request.subtitle_config,
|
||
bgm_config=request.bgm_config,
|
||
estimated_duration=request.estimated_duration,
|
||
segments=[
|
||
SegmentCommand(
|
||
segment_order=s.segment_order,
|
||
duration_min=s.duration_min,
|
||
duration_max=s.duration_max,
|
||
material_type=s.material_type,
|
||
)
|
||
for s in request.segments
|
||
],
|
||
)
|
||
use_case = CreateTemplateUseCase(template_repository)
|
||
try:
|
||
template = use_case.execute(command)
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
|
||
return _to_response(template)
|
||
|
||
|
||
@router.patch("/{template_id}", response_model=TemplateResponse)
|
||
def update_template(
|
||
template_id: str,
|
||
request: UpdateTemplateRequest,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> TemplateResponse:
|
||
user_id = authenticated_user.user.id
|
||
command = UpdateTemplateCommand(
|
||
template_id=template_id,
|
||
user_id=user_id,
|
||
name=request.name,
|
||
mode=request.mode,
|
||
category=request.category,
|
||
tags=request.tags,
|
||
title_config=request.title_config,
|
||
subtitle_config=request.subtitle_config,
|
||
bgm_config=request.bgm_config,
|
||
estimated_duration=request.estimated_duration,
|
||
segments=(
|
||
[
|
||
SegmentCommand(
|
||
segment_order=s.segment_order,
|
||
duration_min=s.duration_min,
|
||
duration_max=s.duration_max,
|
||
material_type=s.material_type,
|
||
)
|
||
for s in request.segments
|
||
]
|
||
if request.segments is not None
|
||
else None
|
||
),
|
||
)
|
||
use_case = UpdateTemplateUseCase(template_repository)
|
||
try:
|
||
template = use_case.execute(command)
|
||
except NotFoundError:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
|
||
return _to_response(template)
|
||
|
||
|
||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
|
||
def delete_template(
|
||
template_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> Response:
|
||
user_id = authenticated_user.user.id
|
||
use_case = DeleteTemplateUseCase(template_repository)
|
||
deleted = use_case.execute(template_id, user_id)
|
||
if not deleted:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
return Response(status_code=204)
|
||
|
||
|
||
@router.post("/{template_id}/toggle-favorite", response_model=ToggleFavoriteResponse)
|
||
def toggle_favorite(
|
||
template_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ToggleFavoriteResponse:
|
||
"""切换模板收藏状态(当前为兼容端点,始终返回 false)"""
|
||
user_id = authenticated_user.user.id
|
||
use_case = GetTemplateUseCase(template_repository)
|
||
try:
|
||
template = use_case.execute(template_id, user_id)
|
||
except Exception:
|
||
logger.exception("toggle_favorite 查询失败: template_id=%s", template_id)
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
return ToggleFavoriteResponse(id=template_id, is_favorite=False)
|
||
|
||
|
||
# ── Validate template ──
|
||
|
||
|
||
@router.post("/{template_id}/validate", response_model=ValidateTemplateResponse)
|
||
def validate_template(
|
||
template_id: str,
|
||
request: ValidateTemplateRequest = ValidateTemplateRequest(),
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ValidateTemplateResponse:
|
||
user_id = authenticated_user.user.id
|
||
command = ValidateTemplateCommand(
|
||
template_id=template_id,
|
||
user_id=user_id,
|
||
voiceover_duration=request.voiceover_duration,
|
||
)
|
||
use_case = ValidateTemplateUseCase(template_repository)
|
||
try:
|
||
result = use_case.execute(command)
|
||
except NotFoundError:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
|
||
|
||
return ValidateTemplateResponse(
|
||
template=_to_response(result.template),
|
||
warnings=[GenerateWarningResponse(code=w.code, message=w.message, details=w.details) for w in result.warnings],
|
||
)
|
||
|
||
|
||
# ── Category CRUD ──
|
||
|
||
|
||
@router.get("/categories/list", response_model=ListCategoriesResponse)
|
||
def list_categories(
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ListCategoriesResponse:
|
||
user_id = authenticated_user.user.id
|
||
try:
|
||
use_case = ListCategoriesUseCase(template_repository)
|
||
categories = use_case.execute(user_id)
|
||
except Exception:
|
||
logger.exception("list_categories 查询失败: user_id=%s", user_id)
|
||
return ListCategoriesResponse(items=[])
|
||
return ListCategoriesResponse(
|
||
items=[CategoryResponse(id=c.id, user_id=c.user_id, name=c.name, created_at=c.created_at) for c in categories],
|
||
)
|
||
|
||
|
||
@router.post("/categories", response_model=CategoryResponse, status_code=status.HTTP_201_CREATED)
|
||
def create_category(
|
||
request: CreateCategoryRequest,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> CategoryResponse:
|
||
user_id = authenticated_user.user.id
|
||
command = CreateCategoryCommand(user_id=user_id, name=request.name)
|
||
use_case = CreateCategoryUseCase(template_repository)
|
||
category = use_case.execute(command)
|
||
return CategoryResponse(
|
||
id=category.id,
|
||
user_id=category.user_id,
|
||
name=category.name,
|
||
created_at=category.created_at,
|
||
)
|
||
|
||
|
||
@router.delete("/categories/{category_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
|
||
def delete_category(
|
||
category_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> Response:
|
||
user_id = authenticated_user.user.id
|
||
use_case = DeleteCategoryUseCase(template_repository)
|
||
deleted = use_case.execute(category_id, user_id)
|
||
if not deleted:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
|
||
return Response(status_code=204)
|