Files
xiaoxia-saas/apps/api/app/api/routes/_helpers.py
T
xiaoxia 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
feat(phase3): 下线/edit-plans/*旧路由 + 清理废弃代码 + 数据库冗余字段清理 (#657)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-20 23:48:36 +08:00

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")