- AI 自动驳回:法规/品牌安全 HIGH 违规或总分<40 自动打回上传阶段 - 功效词可配置:从硬编码改为品牌方在规则页面自行管理 - 驳回通知:AI 驳回时只通知达人,含具体原因 - 达人端:脚本/视频页面展示 AI 驳回原因 + 重新上传入口 - 规则页面:新增"功效词"分类 - 种子数据:新增 6 条默认功效词 - 其他:代理商管理下拉修复、AI 配置模型列表扩展、视觉模型标签修正、规则编辑放开限制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1448 lines
51 KiB
Python
1448 lines
51 KiB
Python
"""
|
||
任务 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)
|