""" 审核决策 API 端点 """ from fastapi import APIRouter, HTTPException, status, Header 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, get_user_by_id, update_user_tokens router = APIRouter() # 模拟视频数据引用(实际使用时应该通过服务层访问) VIDEOS: dict[str, dict] = { "video_001": { "video_id": "video_001", "status": "pending_review", "owner_id": "user_creator_001", "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", }, ], }, "video_002": { "video_id": "video_002", "status": "pending_review", "owner_id": "user_creator_002", "violations": [], }, "video_003": { "video_id": "video_003", "status": "pending_review", "owner_id": "user_creator_003", "violations": [], }, "video_own": { "video_id": "video_own", "status": "pending_review", "owner_id": "user_creator_001", "violations": [], }, "video_assigned": { "video_id": "video_assigned", "status": "pending_review", "owner_id": "user_creator_001", "assigned_agency": "user_agency_001", "violations": [], }, } # 模拟审核历史 REVIEW_HISTORY: dict[str, list[dict]] = {} # 模拟申诉存储 APPEALS: dict[str, dict] = { "appeal_001": { "appeal_id": "appeal_001", "video_id": "video_001", "user_id": "user_creator_001", "violation_ids": ["vio_001"], "reason": "这个词语在此语境下是正常使用", "status": "pending", "created_at": datetime.now().isoformat(), }, } class ReviewDecisionRequest(BaseModel): decision: str # passed, rejected, force_passed selected_violations: list[str] = [] comment: str = "" force_pass_reason: str = "" class ReviewDecisionResponse(BaseModel): review_id: str status: str selected_violations: list[str] = [] force_pass_reason: Optional[str] = None class AddViolationRequest(BaseModel): type: str content: str timestamp_start: float timestamp_end: float severity: str = "medium" class AddViolationResponse(BaseModel): violation_id: str source: str = "manual" type: str content: str severity: str class DeleteViolationRequest(BaseModel): delete_reason: str = "" class DeleteViolationResponse(BaseModel): status: str class ModifyViolationRequest(BaseModel): severity: str modify_reason: str = "" class ModifyViolationResponse(BaseModel): violation_id: str severity: str class AppealRequest(BaseModel): violation_ids: list[str] reason: str class AppealResponse(BaseModel): appeal_id: str status: str class ProcessAppealRequest(BaseModel): decision: str # approved, rejected comment: str = "" class ProcessAppealResponse(BaseModel): appeal_id: str status: str class ReviewHistoryResponse(BaseModel): history: list[dict[str, Any]] class BatchDecisionRequest(BaseModel): video_ids: list[str] decision: str comment: str = "" class BatchDecisionResponse(BaseModel): processed_count: int success_count: int failure_count: int = 0 failures: list[dict[str, str]] = [] def check_review_permission(user: dict, video: dict) -> bool: """检查用户是否有审核权限""" role = user.get("role") user_id = user.get("user_id") # 达人不能审核自己的视频 if role == "creator" and video.get("owner_id") == user_id: return False # 品牌方不能做决策 if role == "brand": return False # Agency 只能审核分配给自己的视频 if role == "agency": assigned_agency = video.get("assigned_agency") if assigned_agency and assigned_agency == user_id: return True return False # 审核员可以审核所有视频 if role == "reviewer": return True return False def add_history_entry(video_id: str, action: str, actor: str, details: dict = None): """添加审核历史记录""" if video_id not in REVIEW_HISTORY: REVIEW_HISTORY[video_id] = [] entry = { "timestamp": datetime.now().isoformat(), "action": action, "actor": actor, "details": details or {}, } REVIEW_HISTORY[video_id].append(entry) # ==================== 静态路由必须放在动态路由之前 ==================== @router.post("/batch/decision", response_model=BatchDecisionResponse) async def batch_review_decision( request: BatchDecisionRequest, 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) processed_count = len(request.video_ids) success_count = 0 failures = [] for video_id in request.video_ids: video = VIDEOS.get(video_id) if not video: failures.append({"video_id": video_id, "error": "Video not found"}) continue if not check_review_permission(user, video): failures.append({"video_id": video_id, "error": "Permission denied"}) continue # 更新视频状态 video["status"] = request.decision success_count += 1 # 添加历史记录 add_history_entry( video_id, f"batch_review_{request.decision}", user["user_id"], {"comment": request.comment}, ) failure_count = len(failures) return BatchDecisionResponse( processed_count=processed_count, success_count=success_count, failure_count=failure_count, failures=failures, ) @router.post("/appeals/{appeal_id}/process", response_model=ProcessAppealResponse) async def process_appeal( appeal_id: str, request: ProcessAppealRequest, 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) appeal = APPEALS.get(appeal_id) if not appeal: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Appeal not found: {appeal_id}", ) if request.decision not in ["approved", "rejected"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid decision type", ) # 更新申诉状态 appeal["status"] = request.decision appeal["processed_by"] = user["user_id"] appeal["processed_at"] = datetime.now().isoformat() appeal["process_comment"] = request.comment # 如果申诉成功,返还令牌 if request.decision == "approved": update_user_tokens(appeal["user_id"], 1) # 添加历史记录 video_id = appeal["video_id"] add_history_entry( video_id, f"appeal_{request.decision}", user["user_id"], {"appeal_id": appeal_id, "comment": request.comment}, ) return ProcessAppealResponse( appeal_id=appeal_id, status=request.decision, ) # ==================== 动态路由 ==================== @router.post("/{video_id}/decision", response_model=ReviewDecisionResponse) async def submit_review_decision( video_id: str, request: ReviewDecisionRequest, 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}", ) # 检查权限 if not check_review_permission(user, video): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to review this video", ) # 验证决策类型 if request.decision not in ["passed", "rejected", "force_passed"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid decision type", ) # 驳回必须选择违规项 if request.decision == "rejected": if not request.selected_violations: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "驳回必须选择至少一个违规项"}, ) # 强制通过必须填写原因 if request.decision == "force_passed": if not request.force_pass_reason: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "强制通过必须填写原因"}, ) # 更新视频状态 video["status"] = request.decision # 创建审核记录 review_id = f"review_{uuid.uuid4().hex[:8]}" # 添加历史记录 add_history_entry( video_id, f"review_{request.decision}", user["user_id"], {"comment": request.comment}, ) return ReviewDecisionResponse( review_id=review_id, status=request.decision, selected_violations=request.selected_violations, force_pass_reason=request.force_pass_reason if request.decision == "force_passed" else None, ) @router.post("/{video_id}/violations", response_model=AddViolationResponse, status_code=status.HTTP_201_CREATED) async def add_manual_violation( video_id: str, request: AddViolationRequest, 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}", ) violation_id = f"vio_{uuid.uuid4().hex[:8]}" violation = { "violation_id": violation_id, "type": request.type, "content": request.content, "severity": request.severity, "timestamp_start": request.timestamp_start, "timestamp_end": request.timestamp_end, "source": "manual", } if "violations" not in video: video["violations"] = [] video["violations"].append(violation) # 添加历史记录 add_history_entry( video_id, "add_violation", user["user_id"], {"violation_id": violation_id}, ) return AddViolationResponse( violation_id=violation_id, source="manual", type=request.type, content=request.content, severity=request.severity, ) @router.delete("/{video_id}/violations/{violation_id}", response_model=DeleteViolationResponse) async def delete_violation( video_id: str, violation_id: str, request: DeleteViolationRequest = DeleteViolationRequest(), 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}", ) violations = video.get("violations", []) violation = next((v for v in 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}", ) video["violations"] = [v for v in violations if v["violation_id"] != violation_id] # 添加历史记录 add_history_entry( video_id, "delete_violation", user["user_id"], {"violation_id": violation_id, "reason": request.delete_reason}, ) return DeleteViolationResponse(status="deleted") @router.patch("/{video_id}/violations/{violation_id}", response_model=ModifyViolationResponse) async def modify_violation( video_id: str, violation_id: str, request: ModifyViolationRequest, 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}", ) violations = video.get("violations", []) violation = next((v for v in 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}", ) violation["severity"] = request.severity # 添加历史记录 add_history_entry( video_id, "modify_violation", user["user_id"], {"violation_id": violation_id, "new_severity": request.severity, "reason": request.modify_reason}, ) return ModifyViolationResponse( violation_id=violation_id, severity=request.severity, ) @router.post("/{video_id}/appeal", response_model=AppealResponse, status_code=status.HTTP_201_CREATED) async def submit_appeal( video_id: str, request: AppealRequest, 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) user_data = get_user_by_id(user["user_id"]) # 检查申诉理由长度 if len(request.reason) < 10: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "申诉理由必须至少 10 个字符"}, ) # 检查申诉令牌 if not user_data or user_data.get("appeal_tokens", 0) <= 0: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"error": "申诉令牌不足"}, ) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) # 扣除令牌 update_user_tokens(user["user_id"], -1) # 创建申诉 appeal_id = f"appeal_{uuid.uuid4().hex[:8]}" APPEALS[appeal_id] = { "appeal_id": appeal_id, "video_id": video_id, "user_id": user["user_id"], "violation_ids": request.violation_ids, "reason": request.reason, "status": "pending", "created_at": datetime.now().isoformat(), } # 添加历史记录 add_history_entry( video_id, "submit_appeal", user["user_id"], {"appeal_id": appeal_id}, ) return AppealResponse( appeal_id=appeal_id, status="pending", ) @router.get("/{video_id}/history", response_model=ReviewHistoryResponse) async def get_review_history( 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}", ) history = REVIEW_HISTORY.get(video_id, []) return ReviewHistoryResponse(history=history) @router.get("/{video_id}") async def get_review( 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 { "video_id": video_id, "status": video.get("status"), "violations": video.get("violations", []), }