Your Name f87ae48ad5 feat: 实现 FastAPI REST API 端点和集成测试
- 添加认证 API (登录/token验证)
- 添加 Brief API (上传/解析/导入/冲突检测)
- 添加视频 API (上传/断点续传/审核/违规/预览/重提交)
- 添加审核 API (决策/批量审核/申诉/历史)
- 实现基于角色的权限控制
- 更新集成测试,49 个测试全部通过
- 总体测试覆盖率 89.63%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 18:08:12 +08:00

478 lines
13 KiB
Python

"""
视频 API 端点
"""
from fastapi import APIRouter, HTTPException, status, Header, UploadFile, File, Form, Query
from pydantic import BaseModel
from typing import Optional, Any
from datetime import datetime
import uuid
from app.api.v1.endpoints.auth import get_current_user
from app.services.video_auditor import VideoFileValidator, VideoAuditor
router = APIRouter()
# 最大文件大小 100MB
MAX_FILE_SIZE = 100 * 1024 * 1024
# 模拟视频存储
VIDEOS: dict[str, dict] = {
"video_001": {
"video_id": "video_001",
"task_id": "task_001",
"brief_id": "brief_001",
"title": "测试视频",
"status": "completed",
"owner_id": "user_creator_001",
"processing_time_ms": 12000,
"violations": [
{
"violation_id": "vio_001",
"type": "forbidden_word",
"content": "最好的",
"severity": "high",
"timestamp_start": 5.0,
"timestamp_end": 5.5,
"source": "ai",
},
{
"violation_id": "vio_002",
"type": "competitor_logo",
"content": "检测到竞品 Logo",
"severity": "medium",
"timestamp_start": 10.0,
"timestamp_end": 12.0,
"source": "ai",
},
],
"brief_compliance": {
"selling_point_coverage": {"coverage_rate": 0.8},
"duration_check": {"product_visible": {"status": "passed"}},
},
"created_at": datetime.now().isoformat(),
},
"video_processing": {
"video_id": "video_processing",
"task_id": "task_001",
"status": "processing",
"progress": 45,
"owner_id": "user_creator_001",
"created_at": datetime.now().isoformat(),
},
"video_own": {
"video_id": "video_own",
"task_id": "task_001",
"status": "pending_review",
"owner_id": "user_creator_001",
"violations": [],
"created_at": datetime.now().isoformat(),
},
"video_assigned": {
"video_id": "video_assigned",
"task_id": "task_001",
"status": "pending_review",
"owner_id": "user_creator_001",
"assigned_agency": "user_agency_001",
"violations": [],
"created_at": datetime.now().isoformat(),
},
}
# 模拟违规证据
EVIDENCES: dict[str, dict] = {
"vio_001": {
"violation_id": "vio_001",
"evidence_type": "text",
"screenshot_url": "/static/screenshots/vio_001.jpg",
"timestamp_start": 5.0,
"timestamp_end": 5.5,
"content": "最好的",
},
}
# 模拟上传会话
UPLOAD_SESSIONS: dict[str, dict] = {}
class VideoUploadResponse(BaseModel):
video_id: str
status: str
message: str = ""
class UploadInitRequest(BaseModel):
filename: str
file_size: int
task_id: str
class UploadInitResponse(BaseModel):
upload_id: str
chunk_size: int = 1024 * 1024 # 1MB
class ChunkUploadResponse(BaseModel):
received_chunks: int
total_chunks: int
status: str
class VideoListResponse(BaseModel):
items: list[dict[str, Any]]
total: int
page: int
page_size: int
class ResubmitRequest(BaseModel):
modification_note: str = ""
modified_sections: list[str] = []
class ResubmitResponse(BaseModel):
status: str
new_video_id: str
class PreviewResponse(BaseModel):
preview_url: str
start_ms: int
end_ms: int
@router.post("/upload", response_model=VideoUploadResponse, status_code=status.HTTP_202_ACCEPTED)
async def upload_video(
file: UploadFile = File(...),
task_id: str = Form(...),
title: str = Form(""),
authorization: Optional[str] = Header(None),
):
"""上传视频文件"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
user = get_current_user(authorization)
# 验证文件格式
content_type = file.content_type or ""
file_ext = file.filename.split(".")[-1].lower() if file.filename else ""
validator = VideoFileValidator()
# 检查格式
if file_ext not in ["mp4", "mov"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported video format: {file_ext}. Only MP4 and MOV are supported.",
)
# 读取文件内容检查大小
content = await file.read()
file_size = len(content)
if file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size is 100MB, got {file_size / (1024*1024):.1f}MB",
)
# 创建视频记录
video_id = f"video_{uuid.uuid4().hex[:8]}"
VIDEOS[video_id] = {
"video_id": video_id,
"task_id": task_id,
"title": title or file.filename,
"status": "processing",
"owner_id": user["user_id"],
"created_at": datetime.now().isoformat(),
}
return VideoUploadResponse(
video_id=video_id,
status="processing",
message="Video is being processed",
)
@router.post("/upload/init", response_model=UploadInitResponse)
async def init_resumable_upload(
request: UploadInitRequest,
authorization: Optional[str] = Header(None),
):
"""初始化断点续传"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
user = get_current_user(authorization)
if request.file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size is 100MB",
)
upload_id = f"upload_{uuid.uuid4().hex[:8]}"
chunk_size = 1024 * 1024 # 1MB
UPLOAD_SESSIONS[upload_id] = {
"upload_id": upload_id,
"filename": request.filename,
"file_size": request.file_size,
"task_id": request.task_id,
"user_id": user["user_id"],
"received_chunks": [],
"total_chunks": (request.file_size + chunk_size - 1) // chunk_size,
"created_at": datetime.now().isoformat(),
}
return UploadInitResponse(
upload_id=upload_id,
chunk_size=chunk_size,
)
@router.post("/upload/{upload_id}/chunk", response_model=ChunkUploadResponse)
async def upload_chunk(
upload_id: str,
chunk: UploadFile = File(...),
chunk_index: int = Form(...),
authorization: Optional[str] = Header(None),
):
"""上传分片"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
session = UPLOAD_SESSIONS.get(upload_id)
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Upload session not found",
)
# 记录已接收的分片
if chunk_index not in session["received_chunks"]:
session["received_chunks"].append(chunk_index)
return ChunkUploadResponse(
received_chunks=len(session["received_chunks"]),
total_chunks=session["total_chunks"],
status="uploading" if len(session["received_chunks"]) < session["total_chunks"] else "completed",
)
@router.get("/{video_id}/audit")
async def get_audit_result(
video_id: str,
authorization: Optional[str] = Header(None),
):
"""获取审核结果"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
user = get_current_user(authorization)
video = VIDEOS.get(video_id)
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Video not found: {video_id}",
)
return {
"report_id": f"report_{video_id}",
"video_id": video_id,
"status": video.get("status"),
"progress": video.get("progress"),
"violations": video.get("violations", []),
"brief_compliance": video.get("brief_compliance"),
"processing_time_ms": video.get("processing_time_ms"),
}
@router.get("/{video_id}/violations")
async def get_video_violations(
video_id: str,
authorization: Optional[str] = Header(None),
):
"""获取视频违规列表"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
video = VIDEOS.get(video_id)
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Video not found: {video_id}",
)
return {"violations": video.get("violations", [])}
@router.get("/{video_id}/violations/{violation_id}/evidence")
async def get_violation_evidence(
video_id: str,
violation_id: str,
authorization: Optional[str] = Header(None),
):
"""获取违规证据"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
video = VIDEOS.get(video_id)
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Video not found: {video_id}",
)
# 查找违规项
violation = next(
(v for v in video.get("violations", []) if v["violation_id"] == violation_id),
None,
)
if not violation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Violation not found: {violation_id}",
)
evidence = EVIDENCES.get(violation_id, {
"violation_id": violation_id,
"evidence_type": violation.get("type", "unknown"),
"screenshot_url": f"/static/screenshots/{violation_id}.jpg",
"timestamp_start": violation.get("timestamp_start", 0),
"timestamp_end": violation.get("timestamp_end", 0),
"content": violation.get("content", ""),
})
return evidence
@router.get("/{video_id}/preview", response_model=PreviewResponse)
async def get_video_preview(
video_id: str,
start_ms: int = Query(0),
end_ms: int = Query(10000),
authorization: Optional[str] = Header(None),
):
"""获取视频预览"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
video = VIDEOS.get(video_id)
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Video not found: {video_id}",
)
return PreviewResponse(
preview_url=f"/static/videos/{video_id}/preview.mp4?start={start_ms}&end={end_ms}",
start_ms=start_ms,
end_ms=end_ms,
)
@router.post("/{video_id}/resubmit", response_model=ResubmitResponse, status_code=status.HTTP_202_ACCEPTED)
async def resubmit_video(
video_id: str,
request: ResubmitRequest,
authorization: Optional[str] = Header(None),
):
"""重新提交视频"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
user = get_current_user(authorization)
video = VIDEOS.get(video_id)
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Video not found: {video_id}",
)
# 创建新视频记录
new_video_id = f"video_{uuid.uuid4().hex[:8]}"
VIDEOS[new_video_id] = {
"video_id": new_video_id,
"task_id": video.get("task_id"),
"title": video.get("title"),
"status": "processing",
"owner_id": user["user_id"],
"previous_version": video_id,
"modification_note": request.modification_note,
"modified_sections": request.modified_sections,
"created_at": datetime.now().isoformat(),
}
return ResubmitResponse(
status="processing",
new_video_id=new_video_id,
)
@router.get("", response_model=VideoListResponse)
async def list_videos(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
status: Optional[str] = Query(None),
task_id: Optional[str] = Query(None),
authorization: Optional[str] = Header(None),
):
"""获取视频列表"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required",
)
user = get_current_user(authorization)
# 过滤视频
filtered = list(VIDEOS.values())
if status:
filtered = [v for v in filtered if v.get("status") == status]
if task_id:
filtered = [v for v in filtered if v.get("task_id") == task_id]
# 分页
total = len(filtered)
start = (page - 1) * page_size
end = start + page_size
items = filtered[start:end]
return VideoListResponse(
items=items,
total=total,
page=page,
page_size=page_size,
)