981d965d89
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web 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 / Build Staging Web Image (push) Successful in 48s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m18s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 5m42s
CI/CD Pipeline / Unit Tests (push) Successful in 6m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m13s
CI/CD Pipeline / Integration Tests (push) Successful in 2m14s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m6s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 17m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
142 lines
5.0 KiB
Python
Executable File
142 lines
5.0 KiB
Python
Executable File
"""路由层共享辅助函数 — 消除跨文件重复定义。"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException, status
|
|
|
|
from packages.application import GetProjectUseCase
|
|
from packages.ports.user_repository import UserRepository
|
|
|
|
|
|
def check_project_access(project_id: str, user_id: str, project_repository) -> None:
|
|
"""检查用户是否有项目访问权限。
|
|
|
|
合并自 asset_libraries.py / edit_plans.py 的同名函数。
|
|
- 空 project_id 直接放行(兼容 edit_plans 中 project_id 可选的场景)
|
|
- 错误信息使用中文,与项目其他路由保持一致
|
|
"""
|
|
if not project_id or not project_id.strip():
|
|
return
|
|
project = project_repository.find_by_id(project_id)
|
|
if project is None:
|
|
raise HTTPException(status_code=404, detail="项目不存在")
|
|
if not project.can_access(user_id):
|
|
raise HTTPException(status_code=403, detail="无权访问该项目")
|
|
|
|
|
|
def get_user_plan(user_id: str, user_repository: UserRepository) -> str:
|
|
"""获取用户的订阅计划名称。"""
|
|
user = user_repository.find_by_id(user_id)
|
|
if user is None:
|
|
return "free"
|
|
return getattr(user, "subscription_plan", "free") or "free"
|
|
|
|
|
|
def require_project_and_library(
|
|
project_id: str,
|
|
library_id: str,
|
|
project_repository: Any,
|
|
asset_library_repository: Any,
|
|
) -> None:
|
|
"""Verify project and asset library exist."""
|
|
project = GetProjectUseCase(project_repository).execute(project_id)
|
|
if project is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
|
|
|
libraries = asset_library_repository.find_by_project(project_id)
|
|
if not any(item.id == library_id for item in libraries):
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found")
|
|
|
|
|
|
def auto_select_video_assets(
|
|
*,
|
|
project_id: str,
|
|
asset_library_repo: Any,
|
|
asset_repo: Any,
|
|
logger=None,
|
|
) -> list[str]:
|
|
"""从项目视频素材库自动选取 ready 状态的视频素材。
|
|
|
|
Args:
|
|
project_id: 项目 ID
|
|
asset_library_repo: 素材库仓储
|
|
asset_repo: 素材仓储
|
|
logger: 可选的 logger 实例,用于记录警告
|
|
|
|
Returns:
|
|
选中的素材 ID 列表,无可用素材时返回空列表
|
|
"""
|
|
if not project_id:
|
|
return []
|
|
|
|
# 找到项目的视频素材库
|
|
libs = asset_library_repo.find_by_project(project_id)
|
|
video_lib = None
|
|
for lib in libs:
|
|
lib_kind = lib.kind.value if hasattr(lib.kind, "value") else lib.kind
|
|
if lib_kind == "video":
|
|
video_lib = lib
|
|
break
|
|
|
|
if not video_lib:
|
|
if logger:
|
|
logger.warning("自动选素材: 项目 %s 无视频素材库", project_id)
|
|
return []
|
|
|
|
# 从素材库中选取 ready 状态的视频素材
|
|
assets = asset_repo.find_by_library(video_lib.id)
|
|
ready_videos = [
|
|
a
|
|
for a in assets
|
|
if (a.status.value if hasattr(a.status, "value") else a.status) == "ready"
|
|
and a.mime_type
|
|
and a.mime_type.startswith("video")
|
|
]
|
|
|
|
# 过滤横屏素材(只保留竖屏/正方形)
|
|
# 移动端短视频场景默认竖屏,横屏素材裁剪后画面不可用
|
|
# 注意:这只是选素材优化,渲染引擎本身支持任何分辨率的素材
|
|
filtered_videos = []
|
|
skipped_landscape = 0
|
|
for a in ready_videos:
|
|
width = a.width if hasattr(a, "width") and a.width else 0
|
|
height = a.height if hasattr(a, "height") and a.height else 0
|
|
if not width or not height:
|
|
# 从 metadata 兜底
|
|
if a.metadata and isinstance(a.metadata, dict):
|
|
width = int(a.metadata.get("width", 0) or 0)
|
|
height = int(a.metadata.get("height", 0) or 0)
|
|
if width and height and width > height:
|
|
skipped_landscape += 1
|
|
continue
|
|
filtered_videos.append(a)
|
|
|
|
if skipped_landscape and logger:
|
|
logger.warning("自动选素材: 跳过 %d 个横屏素材", skipped_landscape)
|
|
|
|
if not filtered_videos:
|
|
if logger:
|
|
logger.warning("自动选素材: 素材库 %s 无可用视频素材", video_lib.name)
|
|
return []
|
|
|
|
# 按创建时间降序(新素材在前)
|
|
filtered_videos.sort(key=lambda a: a.created_at, reverse=True)
|
|
return [a.id for a in filtered_videos]
|
|
|
|
|
|
def format_utc_datetime(dt: datetime | None) -> str:
|
|
"""将数据库读出的 UTC naive datetime 格式化为带时区的 ISO 8601 字符串。
|
|
|
|
数据库 DateTime 列不带时区信息,但存的是 UTC 时间。
|
|
直接 .isoformat() 输出无时区标识,前端会按本地时间解析,导致差 8 小时。
|
|
输出带 Z 后缀,前端 new Date() 自动转本地时间。
|
|
"""
|
|
if dt is None:
|
|
return ""
|
|
if isinstance(dt, str):
|
|
return dt
|
|
if dt.tzinfo is None:
|
|
return dt.isoformat() + "Z"
|
|
return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|