531aacb57e
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
411 lines
15 KiB
Python
411 lines
15 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,
|
||
CopyTemplateRequest,
|
||
CreateCategoryRequest,
|
||
CreateTemplateRequest,
|
||
GenerateWarningResponse,
|
||
ListCategoriesResponse,
|
||
ListTagsResponse,
|
||
ListTemplatesResponse,
|
||
SegmentResponse,
|
||
TemplateResponse,
|
||
TemplateUsageResponse,
|
||
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 (
|
||
CopyTemplateCommand,
|
||
CreateCategoryCommand,
|
||
CreateTemplateCommand,
|
||
ListTemplatesFilter,
|
||
SegmentCommand,
|
||
UpdateTemplateCommand,
|
||
ValidateTemplateCommand,
|
||
)
|
||
from packages.application.template.use_cases import (
|
||
CopyTemplateUseCase,
|
||
CountTemplatesUseCase,
|
||
CreateCategoryUseCase,
|
||
CreateTemplateUseCase,
|
||
DeleteCategoryUseCase,
|
||
DeleteTemplateUseCase,
|
||
GetTemplateUseCase,
|
||
ListCategoriesUseCase,
|
||
ListTagsUseCase,
|
||
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, usage_count: int = 0) -> 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,
|
||
usage_count=usage_count,
|
||
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),
|
||
category: str | None = Query(None, description="按分类筛选"),
|
||
tag: str | None = Query(None, description="按标签筛选"),
|
||
keyword: str | None = Query(None, description="按名称关键词搜索"),
|
||
mode: str | None = Query(None, description="按剪辑模式筛选"),
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ListTemplatesResponse:
|
||
user_id = authenticated_user.user.id
|
||
try:
|
||
tpl_filter = ListTemplatesFilter(
|
||
category=category,
|
||
tag=tag,
|
||
keyword=keyword,
|
||
mode=mode,
|
||
)
|
||
use_case = ListTemplatesUseCase(template_repository)
|
||
templates = use_case.execute(user_id, skip=skip, limit=limit, filter=tpl_filter)
|
||
count_use_case = CountTemplatesUseCase(template_repository)
|
||
total = count_use_case.execute(user_id, filter=tpl_filter)
|
||
|
||
# 批量查询使用次数
|
||
items = []
|
||
for t in templates:
|
||
usage = template_repository.get_usage_count(t.id)
|
||
items.append(_to_response(t, usage_count=usage))
|
||
except Exception:
|
||
logger.exception("list_templates 查询失败: user_id=%s", user_id)
|
||
return ListTemplatesResponse(items=[], total=0)
|
||
return ListTemplatesResponse(
|
||
items=items,
|
||
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)
|
||
usage = template_repository.get_usage_count(template_id)
|
||
except Exception as _e:
|
||
logger.exception("get_template 查询失败: template_id=%s", template_id)
|
||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="模板查询失败") from _e
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
return _to_response(template, usage_count=usage)
|
||
|
||
|
||
@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)) from 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 as _e:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found") from _e
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||
return _to_response(template)
|
||
|
||
|
||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
|
||
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
|
||
|
||
|
||
@router.post("/{template_id}/copy", response_model=TemplateResponse, status_code=status.HTTP_201_CREATED)
|
||
def copy_template(
|
||
template_id: str,
|
||
request: CopyTemplateRequest,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> TemplateResponse:
|
||
"""复制模板(含所有片段配置)"""
|
||
user_id = authenticated_user.user.id
|
||
command = CopyTemplateCommand(
|
||
template_id=template_id,
|
||
user_id=user_id,
|
||
new_name=request.new_name,
|
||
)
|
||
use_case = CopyTemplateUseCase(template_repository)
|
||
try:
|
||
template = use_case.execute(command)
|
||
except NotFoundError as _e:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found") from _e
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||
return _to_response(template)
|
||
|
||
|
||
@router.get("/{template_id}/usage", response_model=TemplateUsageResponse)
|
||
def get_template_usage(
|
||
template_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> TemplateUsageResponse:
|
||
"""获取模板使用次数(关联的剪辑计划数量)"""
|
||
user_id = authenticated_user.user.id
|
||
# 鉴权:确保模板存在且属于当前用户
|
||
use_case = GetTemplateUseCase(template_repository)
|
||
template = use_case.execute(template_id, user_id)
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||
usage = template_repository.get_usage_count(template_id)
|
||
return TemplateUsageResponse(template_id=template_id, usage_count=usage)
|
||
|
||
|
||
@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 as _e:
|
||
logger.exception("toggle_favorite 查询失败: template_id=%s", template_id)
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found") from _e
|
||
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 as _e:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found") from _e
|
||
except ValidationError as exc:
|
||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from 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, response_class=Response
|
||
)
|
||
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)
|
||
|
||
|
||
# ── Tags ──
|
||
|
||
|
||
@router.get("/tags/list", response_model=ListTagsResponse)
|
||
def list_tags(
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
|
||
) -> ListTagsResponse:
|
||
"""获取用户所有模板标签(去重排序)"""
|
||
user_id = authenticated_user.user.id
|
||
try:
|
||
use_case = ListTagsUseCase(template_repository)
|
||
tags = use_case.execute(user_id)
|
||
except Exception:
|
||
logger.exception("list_tags 查询失败: user_id=%s", user_id)
|
||
return ListTagsResponse(items=[])
|
||
return ListTagsResponse(items=tags)
|