后端: - 实现登出 API(清除 refresh token) - 清除 videos.py 中已被 Celery 任务取代的死代码 - 添加速率限制中间件(60次/分钟,登录10次/分钟) - 添加 SECRET_KEY/ENCRYPTION_KEY 默认值警告 - OSS STS 方法回退到 Policy 签名(不再抛异常) 前端: - 添加全局 404/error/loading 页面 - 添加三端 error.tsx + loading.tsx 错误边界 - 修复 useId 条件调用违反 Hooks 规则 - 修复未转义引号和 Image 命名冲突 - 添加 ESLint 配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
5.6 KiB
Python
206 lines
5.6 KiB
Python
"""
|
||
视频审核 API
|
||
"""
|
||
import uuid
|
||
from typing import Optional
|
||
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||
from fastapi.responses import JSONResponse
|
||
from sqlalchemy import select, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database import get_db
|
||
from app.models.tenant import Tenant
|
||
from app.models.review import ReviewTask, TaskStatus as DBTaskStatus, Platform as DBPlatform
|
||
from app.schemas.review import (
|
||
VideoReviewRequest,
|
||
VideoReviewSubmitResponse,
|
||
VideoReviewProgressResponse,
|
||
VideoReviewResultResponse,
|
||
TaskStatus,
|
||
Violation,
|
||
ViolationType,
|
||
RiskLevel,
|
||
ViolationSource,
|
||
SoftRiskWarning,
|
||
)
|
||
|
||
router = APIRouter(prefix="/videos", tags=["videos"])
|
||
|
||
|
||
async def _ensure_tenant_exists(tenant_id: str, db: AsyncSession) -> Tenant:
|
||
"""确保租户存在,不存在则自动创建"""
|
||
result = await db.execute(
|
||
select(Tenant).where(Tenant.id == tenant_id)
|
||
)
|
||
tenant = result.scalar_one_or_none()
|
||
|
||
if not tenant:
|
||
tenant = Tenant(id=tenant_id, name=f"租户-{tenant_id}")
|
||
db.add(tenant)
|
||
await db.flush()
|
||
|
||
return tenant
|
||
|
||
|
||
@router.post(
|
||
"/review",
|
||
response_model=VideoReviewSubmitResponse,
|
||
status_code=status.HTTP_202_ACCEPTED,
|
||
)
|
||
async def submit_video_review(
|
||
request: VideoReviewRequest,
|
||
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> VideoReviewSubmitResponse:
|
||
"""
|
||
提交视频审核
|
||
|
||
返回 202 Accepted,异步处理
|
||
"""
|
||
# 确保租户存在
|
||
await _ensure_tenant_exists(x_tenant_id, db)
|
||
|
||
review_id = f"review-{uuid.uuid4().hex[:12]}"
|
||
|
||
# 创建审核任务
|
||
task = ReviewTask(
|
||
id=review_id,
|
||
tenant_id=x_tenant_id,
|
||
video_url=str(request.video_url),
|
||
platform=DBPlatform(request.platform.value),
|
||
brand_id=request.brand_id,
|
||
creator_id=request.creator_id,
|
||
status=DBTaskStatus.PENDING,
|
||
progress=0,
|
||
current_step="等待处理",
|
||
competitors=request.competitors,
|
||
requirements=request.requirements,
|
||
)
|
||
db.add(task)
|
||
await db.commit()
|
||
|
||
# 触发 Celery 异步任务
|
||
try:
|
||
from app.tasks.review import process_video_review_task
|
||
process_video_review_task.delay(
|
||
review_id=review_id,
|
||
tenant_id=x_tenant_id,
|
||
video_url=str(request.video_url),
|
||
brand_id=request.brand_id,
|
||
platform=request.platform.value,
|
||
)
|
||
except Exception:
|
||
# Celery 不可用时,任务保持 PENDING 状态
|
||
# 后续可通过定时任务或手动触发处理
|
||
pass
|
||
|
||
return VideoReviewSubmitResponse(
|
||
review_id=review_id,
|
||
status=TaskStatus.PENDING,
|
||
)
|
||
|
||
|
||
@router.get(
|
||
"/review/{review_id}/progress",
|
||
response_model=VideoReviewProgressResponse,
|
||
)
|
||
async def get_review_progress(
|
||
review_id: str,
|
||
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> VideoReviewProgressResponse:
|
||
"""
|
||
查询审核进度
|
||
"""
|
||
result = await db.execute(
|
||
select(ReviewTask).where(
|
||
and_(
|
||
ReviewTask.id == review_id,
|
||
ReviewTask.tenant_id == x_tenant_id,
|
||
)
|
||
)
|
||
)
|
||
task = result.scalar_one_or_none()
|
||
|
||
if not task:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"审核任务不存在: {review_id}",
|
||
)
|
||
|
||
return VideoReviewProgressResponse(
|
||
review_id=review_id,
|
||
status=TaskStatus(task.status.value),
|
||
progress=task.progress,
|
||
current_step=task.current_step,
|
||
)
|
||
|
||
|
||
@router.get("/review/{review_id}/result")
|
||
async def get_review_result(
|
||
review_id: str,
|
||
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
查询审核结果
|
||
|
||
- 未完成:返回 202 + 进度结构
|
||
- 已完成:返回 200 + 结果结构
|
||
"""
|
||
result = await db.execute(
|
||
select(ReviewTask).where(
|
||
and_(
|
||
ReviewTask.id == review_id,
|
||
ReviewTask.tenant_id == x_tenant_id,
|
||
)
|
||
)
|
||
)
|
||
task = result.scalar_one_or_none()
|
||
|
||
if not task:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"审核任务不存在: {review_id}",
|
||
)
|
||
|
||
# 未完成:返回 202 + 进度
|
||
if task.status in [DBTaskStatus.PENDING, DBTaskStatus.PROCESSING]:
|
||
progress_response = VideoReviewProgressResponse(
|
||
review_id=review_id,
|
||
status=TaskStatus(task.status.value),
|
||
progress=task.progress,
|
||
current_step=task.current_step,
|
||
)
|
||
return JSONResponse(
|
||
status_code=status.HTTP_202_ACCEPTED,
|
||
content=progress_response.model_dump(),
|
||
)
|
||
|
||
# 失败:返回错误信息
|
||
if task.status == DBTaskStatus.FAILED:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=task.error_message or "审核任务失败",
|
||
)
|
||
|
||
# 已完成:返回 200 + 结果
|
||
violations = []
|
||
if task.violations:
|
||
for v in task.violations:
|
||
violations.append(Violation(**v))
|
||
|
||
soft_warnings = []
|
||
if task.soft_warnings:
|
||
for w in task.soft_warnings:
|
||
soft_warnings.append(SoftRiskWarning(**w))
|
||
|
||
return VideoReviewResultResponse(
|
||
review_id=review_id,
|
||
status=TaskStatus.COMPLETED,
|
||
score=task.score or 100,
|
||
summary=task.summary or "审核完成",
|
||
violations=violations,
|
||
soft_warnings=soft_warnings,
|
||
)
|