""" app/models/task.py — generation_tasks / text_candidates / image_candidates / delivery_packages Alembic 003 业务主体(2/3) 任务主键:自增 BIGINT + mongo_trace_id VARCHAR(24) eval_score 留 NULL,不接 banana 假评分(基石) """ from datetime import datetime from typing import Optional from sqlalchemy import ( BigInteger, Boolean, DateTime, Enum, Float, ForeignKey, Index, Integer, String, Text, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.constants.enums import ( BannedWordStatus, CandidateSource, ImageRole, PackageStatus, ReviewStatus, TaskStatus, ) from app.core.database import Base class GenerationTask(Base): """生产任务,审核字段平铺(不建独立审核表)。""" __tablename__ = "generation_tasks" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) workspace_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False ) product_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("products.id"), nullable=False ) operator_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("users.id"), nullable=False ) theme: Mapped[str | None] = mapped_column(String(256)) text_count: Mapped[int] = mapped_column(Integer, default=5, nullable=False) image_count: Mapped[int] = mapped_column(Integer, default=3, nullable=False) track: Mapped[str] = mapped_column(String(16), default="ai", nullable=False) # 本次产品是否入镜:True=必须用产品参考图(无图禁生成,不降级纯文生图);False=允许纯文生图 need_product_image: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) status: Mapped[str] = mapped_column( Enum(TaskStatus, values_callable=lambda x: [e.value for e in x]), default=TaskStatus.PENDING, nullable=False, ) mongo_trace_id: Mapped[str | None] = mapped_column(String(24)) # MongoDB trace # ── 审核字段(平铺)────────────────────────────── review_status: Mapped[str | None] = mapped_column( Enum(ReviewStatus, values_callable=lambda x: [e.value for e in x]) ) reviewer_id: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("users.id")) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime) reject_reason: Mapped[str | None] = mapped_column(Text) approved_at: Mapped[datetime | None] = mapped_column(DateTime) archived_at: Mapped[datetime | None] = mapped_column(DateTime) # 011: 第2环S12+第11环裂变 benchmark_ids: Mapped[str | None] = mapped_column(Text, comment="关联标杆笔记ID列表(JSON list),第2环引用") source_fission_id: Mapped[int | None] = mapped_column(Integer, comment="裂变来源fission_task ID;NULL=普通任务") created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now(), nullable=False ) __table_args__ = ( Index("idx_generation_tasks_workspace_status", "workspace_id", "status"), ) class TextCandidate(Base): """文案候选(source=ai/import 区分双轨)。eval_score 留 NULL。""" __tablename__ = "text_candidates" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) workspace_id: Mapped[int] = mapped_column(BigInteger, nullable=False) task_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("generation_tasks.id", ondelete="CASCADE"), nullable=False ) source: Mapped[str] = mapped_column( Enum(CandidateSource, values_callable=lambda x: [e.value for e in x]), default=CandidateSource.AI, nullable=False, ) angle_label: Mapped[str | None] = mapped_column(String(64)) content: Mapped[str | None] = mapped_column(Text) score_json: Mapped[str | None] = mapped_column(Text) # 五维分 JSON banned_word_status: Mapped[str] = mapped_column( Enum(BannedWordStatus, values_callable=lambda x: [e.value for e in x]), default=BannedWordStatus.PASS, nullable=False, ) eval_score: Mapped[float | None] = mapped_column(Float) # 一期留 NULL is_selected: Mapped[bool] = mapped_column(default=False, nullable=False) edited: Mapped[bool] = mapped_column(default=False, nullable=False) # 改稿标记:用户改过=最强飞轮信号 created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), nullable=False ) __table_args__ = ( Index("idx_text_candidates_task_id", "task_id"), ) class ImageCandidate(Base): """图片候选。eval_score 留 NULL。""" __tablename__ = "image_candidates" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) workspace_id: Mapped[int] = mapped_column(BigInteger, nullable=False) task_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("generation_tasks.id", ondelete="CASCADE"), nullable=False ) # role 用 varchar 不用 enum:storyboard 角色名仍在演进,约束放应用层 ImageRole role: Mapped[str] = mapped_column( String(32), default=ImageRole.HOOK.value, nullable=False, ) url: Mapped[str | None] = mapped_column(String(512)) strategy: Mapped[str | None] = mapped_column(String(4)) # A/B/C(二期) seq: Mapped[int] = mapped_column(Integer, default=1) # 分镜序号 is_selected: Mapped[bool] = mapped_column(default=False, nullable=False) # R2 重生标记:True=用户手动重生产出(新增不删旧,前端同strategy+role默认只展示最新) is_regen: Mapped[bool] = mapped_column(default=False, nullable=False) eval_score: Mapped[float | None] = mapped_column(Float) # 一期留 NULL # 标题文字校验结果JSON(NULL=通过;有值=曾不过/重生,前端提示运营人工筛) text_review: Mapped[str | None] = mapped_column(String(512)) # AI评图分0-100(展示+生成时筛选+排序用,绝不进飞轮权重;eval_score 另留 NULL) ai_visual_score: Mapped[float | None] = mapped_column(Float) ai_visual_note: Mapped[str | None] = mapped_column(String(256)) # AI评图一句话评语(前端展示) created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), nullable=False ) __table_args__ = ( Index("idx_image_candidates_task_id", "task_id"), ) class DeliveryPackage(Base): """达人素材交付包。""" __tablename__ = "delivery_packages" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) workspace_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False ) task_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("generation_tasks.id"), nullable=False ) status: Mapped[str] = mapped_column( Enum(PackageStatus, values_callable=lambda x: [e.value for e in x]), default=PackageStatus.PENDING, nullable=False, ) package_path: Mapped[str | None] = mapped_column(String(512)) download_url: Mapped[str | None] = mapped_column(String(512)) expires_at: Mapped[datetime | None] = mapped_column(DateTime) created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), nullable=False )