- Brief 支持代理商附件上传 (迁移 007) - 项目新增 platform 字段 (迁移 008),前端创建/展示平台信息 - 修复 AI 规则解析:处理中文引号导致 JSON 解析失败的问题 - 修复消息中心崩溃:补全后端消息类型映射 + fallback 保护 - 项目创建时自动发送消息通知 - .gitignore 排除 backend/data/ 数据库文件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
6.7 KiB
Python
149 lines
6.7 KiB
Python
"""
|
|
任务模型
|
|
"""
|
|
from typing import TYPE_CHECKING, Optional
|
|
from datetime import datetime
|
|
from sqlalchemy import String, Integer, Text, ForeignKey, DateTime, Enum as SQLEnum, Boolean
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
import enum
|
|
|
|
from app.models.base import Base, TimestampMixin
|
|
from app.models.types import JSONType
|
|
|
|
if TYPE_CHECKING:
|
|
from app.models.project import Project
|
|
from app.models.organization import Agency, Creator
|
|
|
|
|
|
class TaskStage(str, enum.Enum):
|
|
"""任务阶段"""
|
|
SCRIPT_UPLOAD = "script_upload" # 待上传脚本
|
|
SCRIPT_AI_REVIEW = "script_ai_review" # 脚本 AI 审核中
|
|
SCRIPT_AGENCY_REVIEW = "script_agency_review" # 脚本代理商审核中
|
|
SCRIPT_BRAND_REVIEW = "script_brand_review" # 脚本品牌方终审中
|
|
VIDEO_UPLOAD = "video_upload" # 待上传视频
|
|
VIDEO_AI_REVIEW = "video_ai_review" # 视频 AI 审核中
|
|
VIDEO_AGENCY_REVIEW = "video_agency_review" # 视频代理商审核中
|
|
VIDEO_BRAND_REVIEW = "video_brand_review" # 视频品牌方终审中
|
|
COMPLETED = "completed" # 已完成
|
|
REJECTED = "rejected" # 已驳回
|
|
|
|
|
|
class TaskStatus(str, enum.Enum):
|
|
"""任务状态"""
|
|
PENDING = "pending" # 待处理
|
|
PROCESSING = "processing" # 处理中
|
|
PASSED = "passed" # 通过
|
|
REJECTED = "rejected" # 驳回
|
|
FORCE_PASSED = "force_passed" # 强制通过
|
|
|
|
|
|
class Task(Base, TimestampMixin):
|
|
"""任务表"""
|
|
__tablename__ = "tasks"
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
|
|
# 关联
|
|
project_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
agency_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
ForeignKey("agencies.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
creator_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
ForeignKey("creators.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# 任务信息
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False) # 如 "宣传任务(1)"
|
|
sequence: Mapped[int] = mapped_column(Integer, default=1, nullable=False) # 序号
|
|
|
|
# 当前阶段
|
|
stage: Mapped[TaskStage] = mapped_column(
|
|
SQLEnum(TaskStage, name="task_stage_enum", values_callable=lambda x: [e.value for e in x]),
|
|
default=TaskStage.SCRIPT_UPLOAD,
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
# ===== 脚本相关 =====
|
|
script_file_url: Mapped[Optional[str]] = mapped_column(String(2048), nullable=True)
|
|
script_file_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
script_uploaded_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 脚本 AI 审核结果
|
|
script_ai_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
script_ai_result: Mapped[Optional[dict]] = mapped_column(JSONType, nullable=True)
|
|
script_ai_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 脚本代理商审核
|
|
script_agency_status: Mapped[Optional[TaskStatus]] = mapped_column(
|
|
SQLEnum(TaskStatus, name="task_status_enum", values_callable=lambda x: [e.value for e in x]),
|
|
nullable=True,
|
|
)
|
|
script_agency_comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
script_agency_reviewer_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
|
script_agency_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 脚本品牌方终审
|
|
script_brand_status: Mapped[Optional[TaskStatus]] = mapped_column(
|
|
SQLEnum(TaskStatus, name="task_status_enum", create_type=False, values_callable=lambda x: [e.value for e in x]),
|
|
nullable=True,
|
|
)
|
|
script_brand_comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
script_brand_reviewer_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
|
script_brand_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# ===== 视频相关 =====
|
|
video_file_url: Mapped[Optional[str]] = mapped_column(String(2048), nullable=True)
|
|
video_file_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
video_duration: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 秒
|
|
video_thumbnail_url: Mapped[Optional[str]] = mapped_column(String(2048), nullable=True)
|
|
video_uploaded_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 视频 AI 审核结果
|
|
video_ai_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
video_ai_result: Mapped[Optional[dict]] = mapped_column(JSONType, nullable=True)
|
|
video_ai_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 视频代理商审核
|
|
video_agency_status: Mapped[Optional[TaskStatus]] = mapped_column(
|
|
SQLEnum(TaskStatus, name="task_status_enum", create_type=False, values_callable=lambda x: [e.value for e in x]),
|
|
nullable=True,
|
|
)
|
|
video_agency_comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
video_agency_reviewer_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
|
video_agency_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# 视频品牌方终审
|
|
video_brand_status: Mapped[Optional[TaskStatus]] = mapped_column(
|
|
SQLEnum(TaskStatus, name="task_status_enum", create_type=False, values_callable=lambda x: [e.value for e in x]),
|
|
nullable=True,
|
|
)
|
|
video_brand_comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
video_brand_reviewer_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
|
video_brand_reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# ===== 申诉相关 =====
|
|
appeal_count: Mapped[int] = mapped_column(Integer, default=1, nullable=False) # 剩余申诉次数
|
|
is_appeal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # 是否为申诉
|
|
appeal_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # 申诉理由
|
|
|
|
# 关联
|
|
project: Mapped["Project"] = relationship("Project", back_populates="tasks")
|
|
agency: Mapped["Agency"] = relationship("Agency", foreign_keys=[agency_id])
|
|
creator: Mapped["Creator"] = relationship("Creator", foreign_keys=[creator_id])
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Task(id={self.id}, name={self.name}, stage={self.stage})>"
|