feat(clips): EditorClipResponse 增加 asset_url 签名URL字段 #1403

Closed
xiaoxia wants to merge 1 commits from feat/clips-asset-url into develop
2 changed files with 65 additions and 3 deletions
@@ -19,10 +19,14 @@ import logging
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import get_asset_repository
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 packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from .dependencies import get_draft_plan_id, get_editor_services
from .schemas import (
ClipBatchDeleteRequest,
@@ -43,7 +47,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Template Editor"])
def _clip_to_response(clip) -> EditorClipResponse:
def _clip_to_response(clip, asset_url: str | None = None) -> EditorClipResponse:
"""统一构造片段响应"""
return EditorClipResponse(
id=clip.id,
@@ -59,14 +63,55 @@ def _clip_to_response(clip) -> EditorClipResponse:
else str(clip.transition_effect),
playback_speed=clip.playback_speed or 1.0,
config=clip.config or {},
asset_url=asset_url,
)
def _build_asset_url_map(
asset_ids: list[str],
asset_repo: SQLAlchemyAssetRepository,
) -> dict[str, str | None]:
"""批量查询素材并生成签名URL映射.
Returns:
{asset_id: signed_url_or_None}
"""
if not asset_ids:
return {}
result: dict[str, str | None] = {}
try:
storage = get_storage_service()
except Exception:
logger.warning("获取存储服务失败,跳过asset_url生成")
return {aid: None for aid in asset_ids}
for aid in asset_ids:
if not aid:
continue
try:
asset = asset_repo.find_by_id(aid)
if asset is None:
result[aid] = None
continue
storage_key = getattr(asset, "storage_key", None) or ""
if not storage_key:
result[aid] = None
continue
result[aid] = storage.get_download_url(storage_key, expires_seconds=3600)
except Exception:
logger.warning("生成素材签名URL失败: asset_id=%s", aid, exc_info=True)
result[aid] = None
return result
@router.get("/clips", response_model=EditorClipListResponse)
def list_draft_clips(
template_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=500),
_: AuthenticatedUser = Depends(get_current_user),
@@ -75,8 +120,17 @@ def list_draft_clips(
_, plan_svc = services
clips = plan_svc.list_clips(plan_id, skip=skip, limit=limit)
total = plan_svc.count_clips(plan_id)
# 批量解析素材签名URL
asset_ids = [getattr(c, "asset_id", "") or "" for c in clips]
asset_ids = [aid for aid in asset_ids if aid]
url_map = _build_asset_url_map(asset_ids, asset_repo)
return EditorClipListResponse(
items=[_clip_to_response(c) for c in clips],
items=[
_clip_to_response(c, asset_url=url_map.get(getattr(c, "asset_id", "") or ""))
for c in clips
],
total=total,
)
@@ -156,6 +210,7 @@ def get_draft_clip_detail(
clip_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
_: AuthenticatedUser = Depends(get_current_user),
):
"""获取草稿中的片段详情"""
@@ -165,7 +220,10 @@ def get_draft_clip_detail(
raise HTTPException(status_code=404, detail="片段不存在")
if clip.plan_id != plan_id:
raise HTTPException(status_code=404, detail="片段不存在")
return _clip_to_response(clip)
asset_id = getattr(clip, "asset_id", "") or ""
url_map = _build_asset_url_map([asset_id], asset_repo) if asset_id else {}
return _clip_to_response(clip, asset_url=url_map.get(asset_id))
@router.post("/clips/{clip_id}/split", response_model=dict[str, Any], status_code=status.HTTP_200_OK)
@@ -450,6 +450,10 @@ class EditorClipResponse(BaseModel):
transition_effect: str = "cut"
playback_speed: float = 1.0
config: dict[str, Any] = Field(default_factory=dict)
asset_url: str | None = Field(
default=None,
description="素材视频签名URL(1小时有效),用于前端预览播放",
)
class EditorClipListResponse(BaseModel):