diff --git a/apps/api/app/api/routes/templates_editor/clips.py b/apps/api/app/api/routes/templates_editor/clips.py index 06f0242a7..3cbe15b6b 100755 --- a/apps/api/app/api/routes/templates_editor/clips.py +++ b/apps/api/app/api/routes/templates_editor/clips.py @@ -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) diff --git a/apps/api/app/api/routes/templates_editor/schemas.py b/apps/api/app/api/routes/templates_editor/schemas.py index af09bbb26..fd14011b5 100755 --- a/apps/api/app/api/routes/templates_editor/schemas.py +++ b/apps/api/app/api/routes/templates_editor/schemas.py @@ -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):