Files
xiaoxia-saas/tests/integration/fixtures/duplication_routes_fixed.py
T
xiaoxia c9b9fbc397
Deploy / Staging E2E Tests (push) Has been skipped
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 133h46m2s
CI/CD Pipeline / Frontend Lint (push) Failing after 133h46m39s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 133h46m39s
fix: fixtures文件isort排序+flake8修复
2026-07-03 23:07:47 +08:00

283 lines
9.7 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""查重 API 路由。"""
from __future__ import annotations
import logging
from typing import Any
from uuid import uuid4
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import OSSStorageService, get_storage_service
from app.dependencies import get_duplication_repository
from app.schemas.duplication import (
DuplicateSegmentResponse,
DuplicationDetailResponse,
DuplicationRecordResponse,
DuplicationUploadResponse,
)
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
from packages.application import (
DeleteDuplicationRecordUseCase,
GetDuplicationDetailUseCase,
ListDuplicationRecordsUseCase,
RetryDuplicationUseCase,
UploadForDuplicationCommand,
UploadForDuplicationUseCase,
)
from packages.domain.duplication import DuplicationRecord
logger = logging.getLogger(__name__)
router = APIRouter()
# 查重功能只接受视频文件
ALLOWED_VIDEO_MIME_TYPES = frozenset(
{
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-msvideo",
"video/webm",
"video/x-matroska",
"video/3gpp",
}
)
def _validate_video_mime_type(content_type: str | None) -> str:
"""验证视频文件的 MIME 类型,如果无效则抛出异常。"""
if not content_type:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Content-Type header is required",
)
# 处理带参数的类型,如 "video/mp4; charset=utf-8"
base_type = content_type.split(";")[0].strip().lower()
if base_type not in ALLOWED_VIDEO_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail="只支持视频文件。支持的类型: mp4, mpeg, mov, avi, webm, mkv, 3gp",
)
return base_type
def _to_record_response(record: DuplicationRecord) -> DuplicationRecordResponse:
return DuplicationRecordResponse(
id=record.id,
filename=record.filename,
file_size=record.file_size,
duration_seconds=record.duration_seconds,
status=record.status,
duplicate_rate=record.duplicate_rate,
duplicate_count=record.duplicate_count,
created_at=record.created_at.isoformat(),
updated_at=record.updated_at.isoformat(),
)
def _to_detail_response(record: DuplicationRecord) -> DuplicationDetailResponse:
return DuplicationDetailResponse(
id=record.id,
filename=record.filename,
file_size=record.file_size,
duration_seconds=record.duration_seconds,
status=record.status,
duplicate_rate=record.duplicate_rate,
duplicate_count=record.duplicate_count,
created_at=record.created_at.isoformat(),
updated_at=record.updated_at.isoformat(),
segments=[
DuplicateSegmentResponse(
id=seg.id,
source_start=seg.source_start,
source_end=seg.source_end,
matched_video_id=seg.matched_video_id,
matched_video_name=seg.matched_video_name,
matched_start=seg.matched_start,
matched_end=seg.matched_end,
similarity=seg.similarity,
)
for seg in record.segments
],
)
@router.post("/upload", response_model=DuplicationUploadResponse)
async def upload_for_duplication(
file: UploadFile = File(..., description="要查重的视频文件"),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
duplication_repository: Any = Depends(get_duplication_repository),
storage_service: OSSStorageService = Depends(get_storage_service),
) -> DuplicationUploadResponse:
"""上传视频进行查重。"""
if file.filename is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件名不能为空",
)
# P0-1: 验证 MIME 类型(只接受视频文件)
validated_content_type = _validate_video_mime_type(file.content_type)
# P0-2: 验证文件大小(参考 OSS_DIRECT_UPLOAD_MAX_MB
from app.config import get_settings
settings = get_settings()
max_size_bytes = settings.OSS_DIRECT_UPLOAD_MAX_MB * 1024 * 1024
# 先检查 Content-Length header(如果可用)
if file.size is not None and file.size > max_size_bytes:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"文件超过上传限制 ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
)
# 读取文件内容并上传到 OSS
file_id = uuid4().hex[:8]
safe_filename = file.filename.replace("/", "_").replace("\\", "_")
storage_key = f"duplication/{file_id}/{safe_filename}"
try:
content = await file.read()
file_size = len(content)
# 再次检查实际文件大小
if file_size > max_size_bytes:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"文件超过上传限制 ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
)
except HTTPException:
raise
except Exception as exc:
logger.error("读取查重文件失败: %s", exc, exc_info=True)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件读取失败,请稍后重试",
) from exc
try:
storage_service.upload_file(
content,
storage_key,
content_type=validated_content_type,
)
except Exception as exc:
logger.error("查重文件上传 OSS 失败: %s", exc, exc_info=True)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="文件上传失败,请稍后重试",
) from exc
use_case = UploadForDuplicationUseCase(duplication_repository)
record = use_case.execute(
UploadForDuplicationCommand(
user_id=authenticated_user.user.id,
filename=file.filename,
file_size=file_size,
storage_key=storage_key,
)
)
logger.info(
"Duplication upload: record=%s file=%s user=%s",
record.id,
file.filename,
authenticated_user.user.id,
)
return DuplicationUploadResponse(
id=record.id,
status=record.status,
message=f'文件 "{file.filename}" 已上传,正在查重中...',
)
@router.get("/records", response_model=list[DuplicationRecordResponse])
def list_duplication_records(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
duplication_repository: Any = Depends(get_duplication_repository),
) -> list[DuplicationRecordResponse]:
"""获取当前用户的查重记录列表。"""
use_case = ListDuplicationRecordsUseCase(duplication_repository)
records = use_case.execute(authenticated_user.user.id)
return [_to_record_response(r) for r in records]
@router.get("/records/{record_id}", response_model=DuplicationDetailResponse)
def get_duplication_detail(
record_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
duplication_repository: Any = Depends(get_duplication_repository),
) -> DuplicationDetailResponse:
"""获取查重记录详情(含重复片段)。"""
use_case = GetDuplicationDetailUseCase(duplication_repository)
record = use_case.execute(record_id)
if record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"查重记录 {record_id} 不存在",
)
if record.user_id != authenticated_user.user.id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"查重记录 {record_id} 不存在",
)
return _to_detail_response(record)
@router.delete("/records/{record_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
def delete_duplication_record(
record_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
duplication_repository: Any = Depends(get_duplication_repository),
) -> Response:
"""删除查重记录。"""
# 检查记录是否存在且属于当前用户
detail_uc = GetDuplicationDetailUseCase(duplication_repository)
record = detail_uc.execute(record_id)
if record is None or record.user_id != authenticated_user.user.id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"查重记录 {record_id} 不存在",
)
use_case = DeleteDuplicationRecordUseCase(duplication_repository)
use_case.execute(record_id)
return Response(status_code=204)
@router.post("/records/{record_id}/retry", response_model=DuplicationUploadResponse)
def retry_duplication(
record_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
duplication_repository: Any = Depends(get_duplication_repository),
) -> DuplicationUploadResponse:
"""重新提交查重。"""
# 检查记录存在且属于当前用户
detail_uc = GetDuplicationDetailUseCase(duplication_repository)
record = detail_uc.execute(record_id)
if record is None or record.user_id != authenticated_user.user.id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"查重记录 {record_id} 不存在",
)
use_case = RetryDuplicationUseCase(duplication_repository)
updated = use_case.execute(record_id)
if updated is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"查重记录 {record_id} 不存在",
)
return DuplicationUploadResponse(
id=updated.id,
status=updated.status,
message="已重新提交查重",
)