Your Name 0b3dfa3c52 feat: AI 审核自动驳回 + 功效词可配置 + UI 修复
- AI 自动驳回:法规/品牌安全 HIGH 违规或总分<40 自动打回上传阶段
- 功效词可配置:从硬编码改为品牌方在规则页面自行管理
- 驳回通知:AI 驳回时只通知达人,含具体原因
- 达人端:脚本/视频页面展示 AI 驳回原因 + 重新上传入口
- 规则页面:新增"功效词"分类
- 种子数据:新增 6 条默认功效词
- 其他:代理商管理下拉修复、AI 配置模型列表扩展、视觉模型标签修正、规则编辑放开限制

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:24:32 +08:00

1448 lines
51 KiB
Python
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
实现完整的审核任务流程
"""
import asyncio
import logging
from typing import Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db, AsyncSessionLocal
from app.models.user import User, UserRole
from app.models.task import Task, TaskStage, TaskStatus
from app.models.project import Project
from app.models.organization import Brand, Agency, Creator
from app.api.deps import (
get_current_user,
get_current_agency,
get_current_creator,
get_current_brand,
)
from app.schemas.task import (
TaskCreateRequest,
TaskResponse,
TaskListResponse,
TaskSummary,
ReviewTaskListResponse,
TaskScriptUploadRequest,
TaskVideoUploadRequest,
TaskReviewRequest,
AppealRequest,
AppealCountRequest,
AppealCountActionRequest,
CreatorInfo,
AgencyInfo,
ProjectInfo,
)
from app.services.task_service import (
create_task,
get_task_by_id,
check_task_permission,
upload_script,
upload_video,
complete_ai_review,
agency_review,
brand_review,
submit_appeal,
increase_appeal_count,
list_tasks_for_creator,
list_tasks_for_agency,
list_tasks_for_brand,
list_pending_reviews_for_agency,
list_pending_reviews_for_brand,
)
from app.api.sse import notify_new_task, notify_task_updated, notify_review_decision
from app.services.message_service import create_message
from app.models.brief import Brief
from app.schemas.review import ScriptReviewRequest, Platform
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tasks", tags=["任务"])
async def _run_script_ai_review(task_id: str, tenant_id: str):
"""
后台执行脚本 AI 审核
- 获取 Brief 信息(卖点、黑名单词)
- 调用 review_script 进行审核
- 保存审核结果并推进任务阶段
- 发送 SSE 通知
"""
from app.api.scripts import review_script
async with AsyncSessionLocal() as db:
try:
task = await get_task_by_id(db, task_id)
if not task or task.stage.value != "script_ai_review":
logger.warning(f"任务 {task_id} 不在 AI 审核阶段,跳过")
return
# 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == task.project_id)
)
project = project_result.scalar_one_or_none()
if not project:
logger.error(f"任务 {task_id} 对应的项目不存在")
return
# 获取 Brief
brief_result = await db.execute(
select(Brief).where(Brief.project_id == project.id)
)
brief = brief_result.scalar_one_or_none()
# 构建审核请求
platform = project.platform or "douyin"
selling_points = brief.selling_points if brief else None
blacklist_words = brief.blacklist_words if brief else None
min_selling_points = brief.min_selling_points if brief else None
request = ScriptReviewRequest(
content=" ", # 占位,实际内容从 file_url 解析
platform=Platform(platform),
brand_id=project.brand_id,
selling_points=selling_points,
min_selling_points=min_selling_points,
blacklist_words=blacklist_words,
file_url=task.script_file_url,
file_name=task.script_file_name,
)
# 调用审核逻辑
result = await review_script(
request=request,
x_tenant_id=tenant_id,
db=db,
)
# 保存审核结果
task = await get_task_by_id(db, task_id)
task = await complete_ai_review(
db=db,
task=task,
review_type="script",
score=result.score,
result=result.model_dump(),
)
await db.commit()
ai_auto_rejected = task.script_ai_result and task.script_ai_result.get("ai_auto_rejected")
logger.info(f"任务 {task_id} AI 审核完成,得分: {result.score},自动驳回: {ai_auto_rejected}")
if ai_auto_rejected:
# AI 自动驳回:只通知达人
try:
creator_result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = creator_result.scalar_one_or_none()
if creator_obj:
reject_reason = task.script_ai_result.get("ai_reject_reason", "")
await notify_task_updated(
task_id=task.id,
user_ids=[creator_obj.user_id],
data={"action": "ai_auto_rejected", "stage": task.stage.value, "score": result.score},
)
await create_message(
db=db,
user_id=creator_obj.user_id,
type="task",
title="脚本未通过 AI 审核",
content=f"任务「{task.name}」未通过 AI 审核({result.score} 分),原因:{reject_reason}。请修改后重新上传。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
else:
# 正常通过SSE 通知达人和代理商 + 消息通知代理商
try:
user_ids = []
creator_result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = creator_result.scalar_one_or_none()
if creator_obj:
user_ids.append(creator_obj.user_id)
agency_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
agency_obj = agency_result.scalar_one_or_none()
if agency_obj:
user_ids.append(agency_obj.user_id)
if user_ids:
await notify_task_updated(
task_id=task.id,
user_ids=user_ids,
data={"action": "ai_review_completed", "stage": task.stage.value, "score": result.score},
)
except Exception:
pass
try:
ag_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
ag_obj = ag_result.scalar_one_or_none()
if ag_obj:
await create_message(
db=db,
user_id=ag_obj.user_id,
type="task",
title="脚本 AI 审核完成",
content=f"任务「{task.name}」AI 审核完成,综合得分 {result.score} 分,请审核。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
# AI 未配置时通知品牌方
if not result.ai_available:
try:
brand_result = await db.execute(
select(Brand).where(Brand.id == project.brand_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj and brand_obj.user_id:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="AI 审核降级运行",
content=f"任务「{task.name}」的 AI 审核已降级运行仅关键词检测请前往「AI 配置」完成设置以获得更精准的审核结果。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
except Exception as e:
logger.error(f"任务 {task_id} AI 审核失败: {e}", exc_info=True)
await db.rollback()
# AI 审核异常时通知品牌方rollback 后重新开始事务)
try:
brand_result = await db.execute(
select(Brand).where(Brand.id == tenant_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj and brand_obj.user_id:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="AI 审核异常",
content=f"任务 AI 审核过程中出错,审核结果可能不完整,请检查 AI 服务配置。错误信息:{str(e)[:100]}",
related_task_id=task_id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
async def _run_video_ai_review(task_id: str, tenant_id: str):
"""
后台执行视频 AI 审核
复用脚本审核的完整规则检测链(违禁词/竞品/平台规则/白名单/AI深度分析
审核内容来源:已通过审核的脚本文本 + 视频文件(如可解析)。
"""
from app.api.scripts import review_script
async with AsyncSessionLocal() as db:
try:
await asyncio.sleep(2) # 模拟处理延迟
task = await get_task_by_id(db, task_id)
if not task or task.stage.value != "video_ai_review":
logger.warning(f"任务 {task_id} 不在视频 AI 审核阶段,跳过")
return
# 获取项目信息
project_result = await db.execute(
select(Project).where(Project.id == task.project_id)
)
project = project_result.scalar_one_or_none()
if not project:
logger.error(f"任务 {task_id} 对应的项目不存在")
return
# 获取 Brief
brief_result = await db.execute(
select(Brief).where(Brief.project_id == project.id)
)
brief = brief_result.scalar_one_or_none()
platform = project.platform or "douyin"
selling_points = brief.selling_points if brief else None
blacklist_words = brief.blacklist_words if brief else None
min_selling_points = brief.min_selling_points if brief else None
# 使用脚本内容作为审核基础(视频 ASR 尚未实现,先复用脚本文本)
script_content = ""
if task.script_file_url and task.script_file_name:
# 脚本文件可用,复用
pass # review_script 会自动解析 file_url
request = ScriptReviewRequest(
content=script_content or " ",
platform=Platform(platform),
brand_id=project.brand_id,
selling_points=selling_points,
min_selling_points=min_selling_points,
blacklist_words=blacklist_words,
file_url=task.script_file_url,
file_name=task.script_file_name,
)
# 调用完整审核逻辑(竞品/违禁词/平台规则/白名单/AI深度分析全部参与
result = await review_script(
request=request,
x_tenant_id=tenant_id,
db=db,
)
video_score = result.score
video_result = {
"score": video_score,
"summary": result.summary,
"violations": [v.model_dump() for v in result.violations],
"soft_warnings": [w.model_dump() for w in result.soft_warnings],
"dimensions": result.dimensions.model_dump(),
"selling_point_matches": [sp.model_dump() for sp in result.selling_point_matches],
}
task = await get_task_by_id(db, task_id)
task = await complete_ai_review(
db=db,
task=task,
review_type="video",
score=video_score,
result=video_result,
)
await db.commit()
ai_auto_rejected = task.video_ai_result and task.video_ai_result.get("ai_auto_rejected")
logger.info(f"任务 {task_id} 视频 AI 审核完成,得分: {video_score},自动驳回: {ai_auto_rejected}")
if ai_auto_rejected:
# AI 自动驳回:只通知达人
try:
creator_result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = creator_result.scalar_one_or_none()
if creator_obj:
reject_reason = task.video_ai_result.get("ai_reject_reason", "")
await notify_task_updated(
task_id=task.id,
user_ids=[creator_obj.user_id],
data={"action": "ai_auto_rejected", "stage": task.stage.value, "score": video_score},
)
await create_message(
db=db,
user_id=creator_obj.user_id,
type="task",
title="视频未通过 AI 审核",
content=f"任务「{task.name}」视频未通过 AI 审核({video_score} 分),原因:{reject_reason}。请修改后重新上传。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
else:
# 正常通过SSE 通知达人和代理商 + 消息通知代理商
try:
user_ids = []
creator_result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = creator_result.scalar_one_or_none()
if creator_obj:
user_ids.append(creator_obj.user_id)
agency_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
agency_obj = agency_result.scalar_one_or_none()
if agency_obj:
user_ids.append(agency_obj.user_id)
if user_ids:
await notify_task_updated(
task_id=task.id,
user_ids=user_ids,
data={"action": "ai_review_completed", "stage": task.stage.value, "score": video_score},
)
except Exception:
pass
try:
ag_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
ag_obj = ag_result.scalar_one_or_none()
if ag_obj:
await create_message(
db=db,
user_id=ag_obj.user_id,
type="task",
title="视频 AI 审核完成",
content=f"任务「{task.name}」视频 AI 审核完成,得分 {video_score} 分,请审核。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
# AI 未配置时通知品牌方
if not result.ai_available:
try:
brand_result = await db.execute(
select(Brand).where(Brand.id == project.brand_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj and brand_obj.user_id:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="视频 AI 审核降级运行",
content=f"任务「{task.name}」的视频 AI 审核已降级运行仅关键词检测请前往「AI 配置」完成设置以获得更精准的审核结果。",
related_task_id=task.id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
except Exception as e:
logger.error(f"任务 {task_id} 视频 AI 审核失败: {e}", exc_info=True)
await db.rollback()
# AI 审核异常时通知品牌方rollback 后重新开始事务)
try:
brand_result = await db.execute(
select(Brand).where(Brand.id == tenant_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj and brand_obj.user_id:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="视频 AI 审核异常",
content=f"任务视频 AI 审核过程中出错,审核结果可能不完整,请检查 AI 服务配置。错误信息:{str(e)[:100]}",
related_task_id=task_id,
sender_name="系统",
)
await db.commit()
except Exception:
pass
def _task_to_response(task: Task) -> TaskResponse:
"""将数据库模型转换为响应模型"""
return TaskResponse(
id=task.id,
name=task.name,
sequence=task.sequence,
stage=task.stage,
project=ProjectInfo(
id=task.project.id,
name=task.project.name,
brand_name=task.project.brand.name if task.project.brand else None,
platform=task.project.platform,
),
agency=AgencyInfo(
id=task.agency.id,
name=task.agency.name,
),
creator=CreatorInfo(
id=task.creator.id,
name=task.creator.name,
avatar=task.creator.avatar,
),
script_file_url=task.script_file_url,
script_file_name=task.script_file_name,
script_uploaded_at=task.script_uploaded_at,
script_ai_score=task.script_ai_score,
script_ai_result=task.script_ai_result,
script_agency_status=task.script_agency_status,
script_agency_comment=task.script_agency_comment,
script_brand_status=task.script_brand_status,
script_brand_comment=task.script_brand_comment,
video_file_url=task.video_file_url,
video_file_name=task.video_file_name,
video_duration=task.video_duration,
video_thumbnail_url=task.video_thumbnail_url,
video_uploaded_at=task.video_uploaded_at,
video_ai_score=task.video_ai_score,
video_ai_result=task.video_ai_result,
video_agency_status=task.video_agency_status,
video_agency_comment=task.video_agency_comment,
video_brand_status=task.video_brand_status,
video_brand_comment=task.video_brand_comment,
appeal_count=task.appeal_count,
is_appeal=task.is_appeal,
appeal_reason=task.appeal_reason,
created_at=task.created_at,
updated_at=task.updated_at,
)
def _task_to_summary(task: Task) -> TaskSummary:
"""将任务转换为摘要"""
return TaskSummary(
id=task.id,
name=task.name,
stage=task.stage,
creator_name=task.creator.name,
creator_avatar=task.creator.avatar,
project_name=task.project.name,
is_appeal=task.is_appeal,
appeal_reason=task.appeal_reason,
created_at=task.created_at,
updated_at=task.updated_at,
)
# ===== 任务创建 =====
@router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
async def create_new_task(
request: TaskCreateRequest,
agency: Agency = Depends(get_current_agency),
db: AsyncSession = Depends(get_db),
):
"""
创建任务(代理商操作)
- 代理商为指定达人创建任务
- 同一项目同一达人可以创建多个任务
- 任务名称自动生成为 "宣传任务(N)"
"""
# 验证项目是否存在
result = await db.execute(
select(Project).where(Project.id == request.project_id)
)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="项目不存在",
)
# 验证达人是否存在
result = await db.execute(
select(Creator).where(Creator.id == request.creator_id)
)
creator = result.scalar_one_or_none()
if not creator:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="达人不存在",
)
# 创建任务
task = await create_task(
db=db,
project_id=request.project_id,
agency_id=agency.id,
creator_id=request.creator_id,
name=request.name,
)
await db.commit()
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 提取通知所需的值commit 后 ORM 对象会过期,提前缓存)
_task_id = task.id
_task_name = task.name
_project_id = task.project.id
_project_name = task.project.name
_project_brand_id = task.project.brand_id
_agency_name = agency.name
_creator_user_id = creator.user_id
_creator_name = creator.name or creator.id
# 创建消息 + SSE 通知达人有新任务
try:
await create_message(
db=db,
user_id=_creator_user_id,
type="new_task",
title="新任务分配",
content=f"您有新的任务「{_task_name}」,来自项目「{_project_name}",
related_task_id=_task_id,
related_project_id=_project_id,
sender_name=_agency_name,
)
await db.commit()
except Exception as e:
logger.warning(f"创建达人通知消息失败: {e}")
# 通知品牌方:代理商给项目添加了达人
try:
brand_result = await db.execute(
select(Brand).where(Brand.id == _project_brand_id)
)
brand = brand_result.scalar_one_or_none()
if brand and brand.user_id:
await create_message(
db=db,
user_id=brand.user_id,
type="new_task",
title="达人加入项目",
content=f"代理商「{_agency_name}」将达人「{_creator_name}」加入项目「{_project_name}」,任务:{_task_name}",
related_task_id=_task_id,
related_project_id=_project_id,
sender_name=_agency_name,
)
await db.commit()
else:
logger.warning(f"品牌方不存在或无 user_id: brand_id={_project_brand_id}")
except Exception as e:
logger.warning(f"创建品牌方通知消息失败: {e}")
try:
await notify_new_task(
task_id=_task_id,
creator_user_id=_creator_user_id,
task_name=_task_name,
project_name=_project_name,
)
except Exception as e:
logger.warning(f"SSE 通知失败: {e}")
return _task_to_response(task)
# ===== 任务查询 =====
@router.get("", response_model=TaskListResponse)
async def list_tasks(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
stage: Optional[TaskStage] = Query(None),
project_id: Optional[str] = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
查询任务列表
- 达人: 查看分配给自己的任务
- 代理商: 查看自己创建的任务
- 品牌方: 查看自己项目下的所有任务
"""
if current_user.role == UserRole.CREATOR:
result = await db.execute(
select(Creator).where(Creator.user_id == current_user.id)
)
creator = result.scalar_one_or_none()
if not creator:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="达人信息不存在",
)
tasks, total = await list_tasks_for_creator(db, creator.id, page, page_size, stage)
elif current_user.role == UserRole.AGENCY:
result = await db.execute(
select(Agency).where(Agency.user_id == current_user.id)
)
agency = result.scalar_one_or_none()
if not agency:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="代理商信息不存在",
)
tasks, total = await list_tasks_for_agency(db, agency.id, page, page_size, stage, project_id)
elif current_user.role == UserRole.BRAND:
result = await db.execute(
select(Brand).where(Brand.user_id == current_user.id)
)
brand = result.scalar_one_or_none()
if not brand:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="品牌方信息不存在",
)
tasks, total = await list_tasks_for_brand(db, brand.id, page, page_size, stage, project_id)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权限访问",
)
return TaskListResponse(
items=[_task_to_response(t) for t in tasks],
total=total,
page=page,
page_size=page_size,
)
@router.get("/pending", response_model=ReviewTaskListResponse)
async def list_pending_reviews(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
获取待审核任务列表
- 代理商: 获取待代理商审核的任务
- 品牌方: 获取待品牌方终审的任务
"""
if current_user.role == UserRole.AGENCY:
result = await db.execute(
select(Agency).where(Agency.user_id == current_user.id)
)
agency = result.scalar_one_or_none()
if not agency:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="代理商信息不存在",
)
tasks, total = await list_pending_reviews_for_agency(db, agency.id, page, page_size)
elif current_user.role == UserRole.BRAND:
result = await db.execute(
select(Brand).where(Brand.user_id == current_user.id)
)
brand = result.scalar_one_or_none()
if not brand:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="品牌方信息不存在",
)
tasks, total = await list_pending_reviews_for_brand(db, brand.id, page, page_size)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅代理商和品牌方可查看待审核任务",
)
return ReviewTaskListResponse(
items=[_task_to_summary(t) for t in tasks],
total=total,
page=page,
page_size=page_size,
)
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
task_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
查询任务详情
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
# 权限检查
has_permission = await check_task_permission(task, current_user, db)
if not has_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务",
)
return _task_to_response(task)
# ===== 文件上传 =====
@router.post("/{task_id}/script", response_model=TaskResponse)
async def upload_task_script(
task_id: str,
request: TaskScriptUploadRequest,
creator: Creator = Depends(get_current_creator),
db: AsyncSession = Depends(get_db),
):
"""
上传/更新脚本(达人操作)
- 只能在 script_upload 阶段上传
- 上传后自动进入 AI 审核
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
if task.creator_id != creator.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能上传自己任务的脚本",
)
try:
task = await upload_script(
db=db,
task=task,
file_url=request.file_url,
file_name=request.file_name,
)
await db.commit()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 通知代理商脚本已上传(消息 + SSE
try:
result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
agency_obj = result.scalar_one_or_none()
if agency_obj:
await create_message(
db=db,
user_id=agency_obj.user_id,
type="task",
title="达人已上传脚本",
content=f"任务「{task.name}」的脚本已上传,等待 AI 审核。",
related_task_id=task.id,
sender_name=creator.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[agency_obj.user_id],
data={"action": "script_uploaded", "stage": task.stage.value},
)
except Exception:
pass
# 获取 tenant_id (品牌方 ID) 并在后台触发 AI 审核
try:
project_result = await db.execute(
select(Project).where(Project.id == task.project_id)
)
project = project_result.scalar_one_or_none()
if project:
asyncio.create_task(_run_script_ai_review(task.id, project.brand_id))
logger.info(f"已触发任务 {task.id} 的后台 AI 审核")
except Exception as e:
logger.error(f"触发 AI 审核失败: {e}")
return _task_to_response(task)
@router.post("/{task_id}/video", response_model=TaskResponse)
async def upload_task_video(
task_id: str,
request: TaskVideoUploadRequest,
creator: Creator = Depends(get_current_creator),
db: AsyncSession = Depends(get_db),
):
"""
上传/更新视频(达人操作)
- 只能在 video_upload 阶段上传
- 上传后自动进入 AI 审核
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
if task.creator_id != creator.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能上传自己任务的视频",
)
try:
task = await upload_video(
db=db,
task=task,
file_url=request.file_url,
file_name=request.file_name,
duration=request.duration,
thumbnail_url=request.thumbnail_url,
)
await db.commit()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 通知代理商视频已上传(消息 + SSE
try:
result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
agency_obj = result.scalar_one_or_none()
if agency_obj:
await create_message(
db=db,
user_id=agency_obj.user_id,
type="task",
title="达人已上传视频",
content=f"任务「{task.name}」的视频已上传,等待 AI 审核。",
related_task_id=task.id,
sender_name=creator.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[agency_obj.user_id],
data={"action": "video_uploaded", "stage": task.stage.value},
)
except Exception:
pass
# 获取 tenant_id 并在后台触发视频 AI 审核
try:
project_result = await db.execute(
select(Project).where(Project.id == task.project_id)
)
project = project_result.scalar_one_or_none()
if project:
asyncio.create_task(_run_video_ai_review(task.id, project.brand_id))
logger.info(f"已触发任务 {task.id} 的后台视频 AI 审核")
except Exception as e:
logger.error(f"触发视频 AI 审核失败: {e}")
return _task_to_response(task)
# ===== 审核操作 =====
@router.post("/{task_id}/script/review", response_model=TaskResponse)
async def review_script(
task_id: str,
request: TaskReviewRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
审核脚本
- 代理商: 在 script_agency_review 阶段审核
- 品牌方: 在 script_brand_review 阶段审核
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
try:
if current_user.role == UserRole.AGENCY:
if task.stage != TaskStage.SCRIPT_AGENCY_REVIEW:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前阶段不在代理商审核中",
)
result = await db.execute(
select(Agency).where(Agency.user_id == current_user.id)
)
agency = result.scalar_one_or_none()
if not agency or task.agency_id != agency.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
task = await agency_review(
db=db,
task=task,
reviewer_id=current_user.id,
action=request.action,
comment=request.comment,
)
elif current_user.role == UserRole.BRAND:
if task.stage != TaskStage.SCRIPT_BRAND_REVIEW:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前阶段不在品牌方审核中",
)
result = await db.execute(
select(Brand).where(Brand.user_id == current_user.id)
)
brand = result.scalar_one_or_none()
if not brand:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
# 验证任务属于该品牌
if task.project.brand_id != brand.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
# 品牌方不能使用 force_pass
if request.action == "force_pass":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="品牌方不能使用强制通过",
)
task = await brand_review(
db=db,
task=task,
reviewer_id=current_user.id,
action=request.action,
comment=request.comment,
)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅代理商和品牌方可审核",
)
await db.commit()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 创建消息 + SSE 通知达人脚本审核结果
try:
result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = result.scalar_one_or_none()
if creator_obj:
reviewer_type = "agency" if current_user.role == UserRole.AGENCY else "brand"
action_text = {"pass": "通过", "reject": "驳回", "force_pass": "强制通过"}.get(request.action, request.action)
await create_message(
db=db,
user_id=creator_obj.user_id,
type=request.action,
title=f"脚本审核{action_text}",
content=f"您的任务「{task.name}」脚本已被{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_review_decision(
task_id=task.id,
creator_user_id=creator_obj.user_id,
review_type="script",
reviewer_type=reviewer_type,
action=request.action,
comment=request.comment,
)
await notify_task_updated(
task_id=task.id,
user_ids=[creator_obj.user_id],
data={"action": f"script_{request.action}", "stage": task.stage.value},
)
except Exception:
pass
# 代理商通过 → 通知品牌方有新内容待审核
try:
if current_user.role == UserRole.AGENCY and request.action in ("pass", "force_pass"):
brand_result = await db.execute(
select(Brand).where(Brand.id == task.project.brand_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="新脚本待审核",
content=f"任务「{task.name}」脚本已通过代理商审核,请进行品牌终审。",
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[brand_obj.user_id],
data={"action": "script_pending_brand_review", "stage": task.stage.value},
)
except Exception:
pass
# 品牌方审核 → 通知代理商结果
try:
if current_user.role == UserRole.BRAND:
ag_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
ag_obj = ag_result.scalar_one_or_none()
if ag_obj:
action_text = {"pass": "通过", "reject": "驳回"}.get(request.action, request.action)
await create_message(
db=db,
user_id=ag_obj.user_id,
type="task",
title=f"脚本品牌终审{action_text}",
content=f"任务「{task.name}」脚本品牌终审已{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[ag_obj.user_id],
data={"action": f"script_brand_{request.action}", "stage": task.stage.value},
)
except Exception:
pass
return _task_to_response(task)
@router.post("/{task_id}/video/review", response_model=TaskResponse)
async def review_video(
task_id: str,
request: TaskReviewRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
审核视频
- 代理商: 在 video_agency_review 阶段审核
- 品牌方: 在 video_brand_review 阶段审核
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
try:
if current_user.role == UserRole.AGENCY:
if task.stage != TaskStage.VIDEO_AGENCY_REVIEW:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前阶段不在代理商审核中",
)
result = await db.execute(
select(Agency).where(Agency.user_id == current_user.id)
)
agency = result.scalar_one_or_none()
if not agency or task.agency_id != agency.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
task = await agency_review(
db=db,
task=task,
reviewer_id=current_user.id,
action=request.action,
comment=request.comment,
)
elif current_user.role == UserRole.BRAND:
if task.stage != TaskStage.VIDEO_BRAND_REVIEW:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="当前阶段不在品牌方审核中",
)
result = await db.execute(
select(Brand).where(Brand.user_id == current_user.id)
)
brand = result.scalar_one_or_none()
if not brand:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
# 验证任务属于该品牌
if task.project.brand_id != brand.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权审核此任务",
)
# 品牌方不能使用 force_pass
if request.action == "force_pass":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="品牌方不能使用强制通过",
)
task = await brand_review(
db=db,
task=task,
reviewer_id=current_user.id,
action=request.action,
comment=request.comment,
)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅代理商和品牌方可审核",
)
await db.commit()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 创建消息 + SSE 通知达人视频审核结果
try:
result = await db.execute(
select(Creator).where(Creator.id == task.creator_id)
)
creator_obj = result.scalar_one_or_none()
if creator_obj:
reviewer_type = "agency" if current_user.role == UserRole.AGENCY else "brand"
action_text = {"pass": "通过", "reject": "驳回", "force_pass": "强制通过"}.get(request.action, request.action)
await create_message(
db=db,
user_id=creator_obj.user_id,
type=request.action,
title=f"视频审核{action_text}",
content=f"您的任务「{task.name}」视频已被{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_review_decision(
task_id=task.id,
creator_user_id=creator_obj.user_id,
review_type="video",
reviewer_type=reviewer_type,
action=request.action,
comment=request.comment,
)
await notify_task_updated(
task_id=task.id,
user_ids=[creator_obj.user_id],
data={"action": f"video_{request.action}", "stage": task.stage.value},
)
except Exception:
pass
# 代理商通过 → 通知品牌方有视频待审核
try:
if current_user.role == UserRole.AGENCY and request.action in ("pass", "force_pass"):
brand_result = await db.execute(
select(Brand).where(Brand.id == task.project.brand_id)
)
brand_obj = brand_result.scalar_one_or_none()
if brand_obj:
await create_message(
db=db,
user_id=brand_obj.user_id,
type="task",
title="新视频待审核",
content=f"任务「{task.name}」视频已通过代理商审核,请进行品牌终审。",
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[brand_obj.user_id],
data={"action": "video_pending_brand_review", "stage": task.stage.value},
)
except Exception:
pass
# 品牌方审核 → 通知代理商结果
try:
if current_user.role == UserRole.BRAND:
ag_result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
ag_obj = ag_result.scalar_one_or_none()
if ag_obj:
action_text = {"pass": "通过", "reject": "驳回"}.get(request.action, request.action)
await create_message(
db=db,
user_id=ag_obj.user_id,
type="task",
title=f"视频品牌终审{action_text}",
content=f"任务「{task.name}」视频品牌终审已{action_text}" + (f",评语:{request.comment}" if request.comment else ""),
related_task_id=task.id,
sender_name=current_user.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[ag_obj.user_id],
data={"action": f"video_brand_{request.action}", "stage": task.stage.value},
)
except Exception:
pass
return _task_to_response(task)
# ===== 申诉操作 =====
@router.post("/{task_id}/appeal", response_model=TaskResponse)
async def submit_task_appeal(
task_id: str,
request: AppealRequest,
creator: Creator = Depends(get_current_creator),
db: AsyncSession = Depends(get_db),
):
"""
提交申诉(达人操作)
- 只能在 rejected 阶段申诉
- 需要有剩余申诉次数
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
if task.creator_id != creator.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能申诉自己的任务",
)
try:
task = await submit_appeal(
db=db,
task=task,
reason=request.reason,
)
await db.commit()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
# 重新加载关联
task = await get_task_by_id(db, task.id)
# 通知代理商有新申诉(消息 + SSE
try:
result = await db.execute(
select(Agency).where(Agency.id == task.agency_id)
)
agency_obj = result.scalar_one_or_none()
if agency_obj:
await create_message(
db=db,
user_id=agency_obj.user_id,
type="task",
title="达人提交申诉",
content=f"任务「{task.name}」的达人提交了申诉:{request.reason}",
related_task_id=task.id,
sender_name=creator.name,
)
await db.commit()
await notify_task_updated(
task_id=task.id,
user_ids=[agency_obj.user_id],
data={"action": "appeal_submitted", "stage": task.stage.value},
)
except Exception:
pass
return _task_to_response(task)
@router.post("/{task_id}/appeal-count", response_model=TaskResponse)
async def increase_task_appeal_count(
task_id: str,
agency: Agency = Depends(get_current_agency),
db: AsyncSession = Depends(get_db),
):
"""
增加申诉次数(代理商操作)
- 每次调用增加 1 次申诉次数
"""
task = await get_task_by_id(db, task_id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
if task.agency_id != agency.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只能操作自己的任务",
)
task = await increase_appeal_count(db, task)
await db.commit()
# 重新加载关联
task = await get_task_by_id(db, task.id)
return _task_to_response(task)