fix: add foreign key constraints to all ORM models
- Add 35 ForeignKey constraints across 15 tables (was zero) - ON DELETE CASCADE for parent-child relationships (project → children) - ON DELETE SET NULL for optional user references (created_by, assignee, etc.) - Add relationship() with back_populates and cascade on all models - Alter 11 columns from nullable=False to nullable=True for SET NULL - Add 2 missing indexes on FK columns (assets.uploaded_by_user_id, project_titles.created_by_user_id) - Update Pydantic response models to accept str | None for nullable fields - Add Alembic migration 010 with complete upgrade/downgrade Closes #17
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""add foreign key constraints and missing indexes
|
||||
|
||||
Revision ID: 010
|
||||
Revises: 009
|
||||
Create Date: 2026-06-28
|
||||
|
||||
Summary:
|
||||
- Add ForeignKey constraints to all 35 logical FK columns across 16 tables
|
||||
- ON DELETE CASCADE for parent-child relationships (project → children)
|
||||
- ON DELETE SET NULL for optional user references (created_by, assignee, etc.)
|
||||
- Add missing indexes on 10 FK columns
|
||||
- Alter columns from NOT NULL to nullable where SET NULL is needed
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "010"
|
||||
down_revision = "009"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── projects ──────────────────────────────────────────────
|
||||
op.create_foreign_key("fk_projects_owner_user_id", "projects", "users", ["owner_user_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── asset_libraries ───────────────────────────────────────
|
||||
op.create_foreign_key("fk_asset_libraries_project_id", "asset_libraries", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── assets ────────────────────────────────────────────────
|
||||
# uploaded_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("assets", "uploaded_by_user_id", existing_type=sa.String(36), nullable=True)
|
||||
op.create_foreign_key("fk_assets_project_id", "assets", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_assets_asset_library_id", "assets", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_assets_uploaded_by_user_id", "assets", "users", ["uploaded_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
# Add missing index
|
||||
op.create_index("ix_assets_uploaded_by_user_id", "assets", ["uploaded_by_user_id"])
|
||||
|
||||
# ── project_titles ────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("project_titles", "created_by_user_id", existing_type=sa.String(36), nullable=True)
|
||||
op.create_foreign_key("fk_project_titles_project_id", "project_titles", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_project_titles_created_by_user_id", "project_titles", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
# Add missing index
|
||||
op.create_index("ix_project_titles_created_by_user_id", "project_titles", ["created_by_user_id"])
|
||||
|
||||
# ── edit_templates ────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("edit_templates", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_edit_templates_project_id", "edit_templates", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_templates_created_by_user_id", "edit_templates", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── edit_plans ────────────────────────────────────────────
|
||||
# title_id: nullable=False → nullable=True for SET NULL
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("edit_plans", "title_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("edit_plans", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_edit_plans_project_id", "edit_plans", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_template_id", "edit_plans", "edit_templates", ["template_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_asset_library_id", "edit_plans", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_title_id", "edit_plans", "project_titles", ["title_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_edit_plans_created_by_user_id", "edit_plans", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── edit_plan_clips ───────────────────────────────────────
|
||||
op.create_foreign_key("fk_edit_plan_clips_edit_plan_id", "edit_plan_clips", "edit_plans", ["edit_plan_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plan_clips_asset_id", "edit_plan_clips", "assets", ["asset_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── ingest_jobs ───────────────────────────────────────────
|
||||
# result_asset_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("ingest_jobs", "result_asset_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_ingest_jobs_project_id", "ingest_jobs", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_ingest_jobs_library_id", "ingest_jobs", "asset_libraries", ["library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_ingest_jobs_result_asset_id", "ingest_jobs", "assets", ["result_asset_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── classification_jobs ───────────────────────────────────
|
||||
op.create_foreign_key("fk_classification_jobs_project_id", "classification_jobs", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_classification_jobs_asset_id", "classification_jobs", "assets", ["asset_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── generation_tasks ──────────────────────────────────────
|
||||
# edit_plan_id: nullable=False → nullable=True for SET NULL
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("generation_tasks", "edit_plan_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("generation_tasks", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_generation_tasks_project_id", "generation_tasks", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generation_tasks_asset_library_id", "generation_tasks", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generation_tasks_edit_plan_id", "generation_tasks", "edit_plans", ["edit_plan_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_generation_tasks_created_by_user_id", "generation_tasks", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── generated_videos ──────────────────────────────────────
|
||||
op.create_foreign_key("fk_generated_videos_project_id", "generated_videos", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generated_videos_generation_task_id", "generated_videos", "generation_tasks", ["generation_task_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generated_videos_duplicate_of", "generated_videos", "generated_videos", ["duplicate_of"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── tasks ─────────────────────────────────────────────────
|
||||
# parent_task_id: nullable=False → nullable=True for SET NULL
|
||||
# assignee_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("tasks", "parent_task_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("tasks", "assignee_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_tasks_project_id", "tasks", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_tasks_parent_task_id", "tasks", "tasks", ["parent_task_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_tasks_assignee_user_id", "tasks", "users", ["assignee_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── milestones ────────────────────────────────────────────
|
||||
op.create_foreign_key("fk_milestones_project_id", "milestones", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── task_issues ───────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("task_issues", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_task_issues_task_id", "task_issues", "tasks", ["task_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_task_issues_project_id", "task_issues", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_task_issues_created_by_user_id", "task_issues", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ── task_issues ───────────────────────────────────────────
|
||||
op.drop_constraint("fk_task_issues_created_by_user_id", "task_issues", type_="foreignkey")
|
||||
op.drop_constraint("fk_task_issues_project_id", "task_issues", type_="foreignkey")
|
||||
op.drop_constraint("fk_task_issues_task_id", "task_issues", type_="foreignkey")
|
||||
op.alter_column("task_issues", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── milestones ────────────────────────────────────────────
|
||||
op.drop_constraint("fk_milestones_project_id", "milestones", type_="foreignkey")
|
||||
|
||||
# ── tasks ─────────────────────────────────────────────────
|
||||
op.drop_constraint("fk_tasks_assignee_user_id", "tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_tasks_parent_task_id", "tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_tasks_project_id", "tasks", type_="foreignkey")
|
||||
op.alter_column("tasks", "assignee_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("tasks", "parent_task_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── generated_videos ──────────────────────────────────────
|
||||
op.drop_constraint("fk_generated_videos_duplicate_of", "generated_videos", type_="foreignkey")
|
||||
op.drop_constraint("fk_generated_videos_generation_task_id", "generated_videos", type_="foreignkey")
|
||||
op.drop_constraint("fk_generated_videos_project_id", "generated_videos", type_="foreignkey")
|
||||
|
||||
# ── generation_tasks ──────────────────────────────────────
|
||||
op.drop_constraint("fk_generation_tasks_created_by_user_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_edit_plan_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_asset_library_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_project_id", "generation_tasks", type_="foreignkey")
|
||||
op.alter_column("generation_tasks", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("generation_tasks", "edit_plan_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── classification_jobs ───────────────────────────────────
|
||||
op.drop_constraint("fk_classification_jobs_asset_id", "classification_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_classification_jobs_project_id", "classification_jobs", type_="foreignkey")
|
||||
|
||||
# ── ingest_jobs ───────────────────────────────────────────
|
||||
op.drop_constraint("fk_ingest_jobs_result_asset_id", "ingest_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_ingest_jobs_library_id", "ingest_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_ingest_jobs_project_id", "ingest_jobs", type_="foreignkey")
|
||||
op.alter_column("ingest_jobs", "result_asset_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── edit_plan_clips ───────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_plan_clips_asset_id", "edit_plan_clips", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plan_clips_edit_plan_id", "edit_plan_clips", type_="foreignkey")
|
||||
|
||||
# ── edit_plans ────────────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_plans_created_by_user_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_title_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_asset_library_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_template_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_project_id", "edit_plans", type_="foreignkey")
|
||||
op.alter_column("edit_plans", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("edit_plans", "title_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── edit_templates ────────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_templates_created_by_user_id", "edit_templates", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_templates_project_id", "edit_templates", type_="foreignkey")
|
||||
op.alter_column("edit_templates", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── project_titles ────────────────────────────────────────
|
||||
op.drop_index("ix_project_titles_created_by_user_id", table_name="project_titles")
|
||||
op.drop_constraint("fk_project_titles_created_by_user_id", "project_titles", type_="foreignkey")
|
||||
op.drop_constraint("fk_project_titles_project_id", "project_titles", type_="foreignkey")
|
||||
op.alter_column("project_titles", "created_by_user_id", existing_type=sa.String(36), nullable=False)
|
||||
|
||||
# ── assets ────────────────────────────────────────────────
|
||||
op.drop_index("ix_assets_uploaded_by_user_id", table_name="assets")
|
||||
op.drop_constraint("fk_assets_uploaded_by_user_id", "assets", type_="foreignkey")
|
||||
op.drop_constraint("fk_assets_asset_library_id", "assets", type_="foreignkey")
|
||||
op.drop_constraint("fk_assets_project_id", "assets", type_="foreignkey")
|
||||
op.alter_column("assets", "uploaded_by_user_id", existing_type=sa.String(36), nullable=False)
|
||||
|
||||
# ── asset_libraries ───────────────────────────────────────
|
||||
op.drop_constraint("fk_asset_libraries_project_id", "asset_libraries", type_="foreignkey")
|
||||
|
||||
# ── projects ──────────────────────────────────────────────
|
||||
op.drop_constraint("fk_projects_owner_user_id", "projects", type_="foreignkey")
|
||||
@@ -65,8 +65,8 @@ class TaskResponse(BaseModel):
|
||||
description: str
|
||||
status: TaskStatus
|
||||
priority: TaskPriority
|
||||
parent_task_id: str
|
||||
assignee_user_id: str
|
||||
parent_task_id: str | None
|
||||
assignee_user_id: str | None
|
||||
progress: float
|
||||
planned_start_date: datetime | None
|
||||
planned_end_date: datetime | None
|
||||
@@ -126,7 +126,7 @@ class TaskIssueResponse(BaseModel):
|
||||
description: str
|
||||
resolved: bool
|
||||
resolved_at: datetime | None
|
||||
created_by_user_id: str
|
||||
created_by_user_id: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class EditPlanResponse(BaseModel):
|
||||
project_id: str
|
||||
template_id: str
|
||||
asset_library_id: str
|
||||
title_id: str = ""
|
||||
title_id: str | None = ""
|
||||
status: str
|
||||
summary: str
|
||||
editing_mode: str | None = None # one_take, pip, voice_over, voice_pip
|
||||
|
||||
@@ -16,7 +16,7 @@ class GenerationTaskResponse(BaseModel):
|
||||
asset_library_id: str
|
||||
strategy_id: str
|
||||
voice_library_id: str
|
||||
edit_plan_id: str
|
||||
edit_plan_id: str | None
|
||||
status: str
|
||||
progress: float
|
||||
result_count: int
|
||||
|
||||
@@ -14,4 +14,4 @@ class IngestJobResponse(BaseModel):
|
||||
storage_key: str
|
||||
status: str
|
||||
error_message: str
|
||||
result_asset_id: str
|
||||
result_asset_id: str | None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, Text, JSON, UniqueConstraint, create_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text, JSON, UniqueConstraint, create_engine
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
@@ -30,23 +30,41 @@ class UserModel(Base):
|
||||
used_storage_gb = Column(Float, nullable=False, default=0.0)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
projects = relationship("ProjectModel", back_populates="owner", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ProjectModel(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
owner_user_id = Column(String(32), nullable=False, index=True)
|
||||
owner_user_id = Column(String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=False, default="")
|
||||
shared_users = Column(JSON, nullable=False, default=list) # 被共享的用户 ID 列表
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
owner = relationship("UserModel", back_populates="projects")
|
||||
asset_libraries = relationship("AssetLibraryModel", back_populates="project", cascade="all, delete-orphan")
|
||||
assets = relationship("AssetModel", back_populates="project", cascade="all, delete-orphan")
|
||||
project_titles = relationship("ProjectTitleModel", back_populates="project", cascade="all, delete-orphan")
|
||||
edit_templates = relationship("EditTemplateModel", back_populates="project", cascade="all, delete-orphan")
|
||||
edit_plans = relationship("EditPlanModel", back_populates="project", cascade="all, delete-orphan")
|
||||
ingest_jobs = relationship("IngestJobModel", back_populates="project", cascade="all, delete-orphan")
|
||||
classification_jobs = relationship("ClassificationJobModel", back_populates="project", cascade="all, delete-orphan")
|
||||
generation_tasks = relationship("GenerationTaskModel", back_populates="project", cascade="all, delete-orphan")
|
||||
generated_videos = relationship("GeneratedVideoModel", back_populates="project", cascade="all, delete-orphan")
|
||||
tasks = relationship("TaskModel", back_populates="project", cascade="all, delete-orphan")
|
||||
milestones = relationship("MilestoneModel", back_populates="project", cascade="all, delete-orphan")
|
||||
task_issues = relationship("TaskIssueModel", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class AssetLibraryModel(Base):
|
||||
__tablename__ = "asset_libraries"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
project_id = Column(String(36), nullable=True, index=True)
|
||||
project_id = Column(String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
kind = Column(String(20), nullable=False, index=True)
|
||||
asset_count = Column(Float, nullable=False, default=0)
|
||||
@@ -54,13 +72,17 @@ class AssetLibraryModel(Base):
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="asset_libraries")
|
||||
assets = relationship("AssetModel", back_populates="asset_library")
|
||||
|
||||
|
||||
class AssetModel(Base):
|
||||
__tablename__ = "assets"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
project_id = Column(String(36), nullable=False, index=True)
|
||||
asset_library_id = Column(String(36), nullable=False, index=True)
|
||||
project_id = Column(String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
asset_library_id = Column(String(36), ForeignKey("asset_libraries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(500), nullable=False)
|
||||
file_type = Column(String(20), nullable=False, index=True)
|
||||
# storage_key: OSS 对象键(相对路径),用于内部存储和操作
|
||||
@@ -79,90 +101,121 @@ class AssetModel(Base):
|
||||
classification_status = Column(String(20), nullable=False, default="pending", index=True)
|
||||
classification_result = Column(Text, nullable=True)
|
||||
quality_score = Column(Float, nullable=True)
|
||||
uploaded_by_user_id = Column(String(36), nullable=False)
|
||||
uploaded_by_user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="assets")
|
||||
asset_library = relationship("AssetLibraryModel", back_populates="assets")
|
||||
uploaded_by = relationship("UserModel")
|
||||
|
||||
|
||||
class ProjectTitleModel(Base):
|
||||
__tablename__ = "project_titles"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
project_id = Column(String(36), nullable=False, index=True)
|
||||
project_id = Column(String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
text = Column(String(200), nullable=False)
|
||||
category = Column(String(50), nullable=False, default="default", index=True)
|
||||
favorite = Column(Boolean, nullable=False, default=False, index=True)
|
||||
usage_count = Column(Integer, nullable=False, default=0)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_by_user_id = Column(String(36), nullable=False)
|
||||
created_by_user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="project_titles")
|
||||
created_by = relationship("UserModel")
|
||||
|
||||
|
||||
class EditTemplateModel(Base):
|
||||
__tablename__ = "edit_templates"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(120), nullable=False)
|
||||
description = Column(Text, nullable=False, default="")
|
||||
target_duration = Column(Float, nullable=False, default=30)
|
||||
clip_count = Column(Integer, nullable=False, default=3)
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_by_user_id = Column(String(32), nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="edit_templates")
|
||||
created_by = relationship("UserModel")
|
||||
edit_plans = relationship("EditPlanModel", back_populates="template")
|
||||
|
||||
|
||||
class EditPlanModel(Base):
|
||||
__tablename__ = "edit_plans"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
template_id = Column(String(32), nullable=False, index=True)
|
||||
asset_library_id = Column(String(32), nullable=False, index=True)
|
||||
title_id = Column(String(32), nullable=False, default="")
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
template_id = Column(String(32), ForeignKey("edit_templates.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
asset_library_id = Column(String(32), ForeignKey("asset_libraries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
title_id = Column(String(32), ForeignKey("project_titles.id", ondelete="SET NULL"), nullable=True)
|
||||
editing_mode = Column(String(20), nullable=True, default=None, index=True)
|
||||
status = Column(String(20), nullable=False, default="draft", index=True)
|
||||
summary = Column(Text, nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="edit_plans")
|
||||
template = relationship("EditTemplateModel", back_populates="edit_plans")
|
||||
asset_library = relationship("AssetLibraryModel")
|
||||
title = relationship("ProjectTitleModel")
|
||||
created_by = relationship("UserModel")
|
||||
clips = relationship("EditPlanClipModel", back_populates="edit_plan", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class EditPlanClipModel(Base):
|
||||
__tablename__ = "edit_plan_clips"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
edit_plan_id = Column(String(32), nullable=False, index=True)
|
||||
asset_id = Column(String(32), nullable=False, index=True)
|
||||
edit_plan_id = Column(String(32), ForeignKey("edit_plans.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
asset_id = Column(String(32), ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
sequence = Column(Integer, nullable=False)
|
||||
start_time = Column(Float, nullable=False, default=0)
|
||||
duration = Column(Float, nullable=False, default=0)
|
||||
reason = Column(Text, nullable=False, default="")
|
||||
layer = Column(String(20), nullable=False, default="main")
|
||||
|
||||
# relationships
|
||||
edit_plan = relationship("EditPlanModel", back_populates="clips")
|
||||
asset = relationship("AssetModel")
|
||||
|
||||
|
||||
class IngestJobModel(Base):
|
||||
__tablename__ = "ingest_jobs"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
library_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
library_id = Column(String(32), ForeignKey("asset_libraries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
storage_key = Column(String(255), nullable=False)
|
||||
status = Column(String(20), nullable=False, default="pending")
|
||||
error_message = Column(Text, nullable=False, default="")
|
||||
result_asset_id = Column(String(32), nullable=False, default="")
|
||||
result_asset_id = Column(String(32), ForeignKey("assets.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="ingest_jobs")
|
||||
library = relationship("AssetLibraryModel")
|
||||
result_asset = relationship("AssetModel")
|
||||
|
||||
|
||||
class ClassificationJobModel(Base):
|
||||
__tablename__ = "classification_jobs"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
asset_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
asset_id = Column(String(32), ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
status = Column(String(20), nullable=False, default="pending")
|
||||
classification = Column(String(50), nullable=False, default="")
|
||||
confidence = Column(Float, nullable=False, default=0.0)
|
||||
@@ -170,16 +223,20 @@ class ClassificationJobModel(Base):
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="classification_jobs")
|
||||
asset = relationship("AssetModel")
|
||||
|
||||
|
||||
class GenerationTaskModel(Base):
|
||||
__tablename__ = "generation_tasks"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
strategy_id = Column(String(32), nullable=False, default="")
|
||||
asset_library_id = Column(String(32), nullable=False, index=True)
|
||||
asset_library_id = Column(String(32), ForeignKey("asset_libraries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
voice_library_id = Column(String(32), nullable=False, default="")
|
||||
edit_plan_id = Column(String(32), nullable=False, default="", index=True)
|
||||
edit_plan_id = Column(String(32), ForeignKey("edit_plans.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
editing_mode = Column(String(20), nullable=False, default="one_take", index=True) # 剪辑模式: one_take, pip, voice_over, voice_pip
|
||||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||||
progress = Column(Float, nullable=False, default=0.0)
|
||||
@@ -187,16 +244,23 @@ class GenerationTaskModel(Base):
|
||||
error_message = Column(Text, nullable=False, default="")
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
created_by_user_id = Column(String(32), nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="generation_tasks")
|
||||
asset_library = relationship("AssetLibraryModel")
|
||||
edit_plan = relationship("EditPlanModel")
|
||||
created_by = relationship("UserModel")
|
||||
generated_videos = relationship("GeneratedVideoModel", back_populates="generation_task", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class GeneratedVideoModel(Base):
|
||||
__tablename__ = "generated_videos"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
generation_task_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
generation_task_id = Column(String(32), ForeignKey("generation_tasks.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
# file_url: 完整可访问的 URL,用于客户端直接访问视频
|
||||
file_url = Column(String(1000), nullable=False)
|
||||
@@ -215,20 +279,25 @@ class GeneratedVideoModel(Base):
|
||||
updated_at = Column(DateTime, nullable=True)
|
||||
video_fingerprint = Column(Text, nullable=True)
|
||||
is_duplicate = Column(Boolean, nullable=False, default=False)
|
||||
duplicate_of = Column(String(32), nullable=True)
|
||||
duplicate_of = Column(String(32), ForeignKey("generated_videos.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="generated_videos")
|
||||
generation_task = relationship("GenerationTaskModel", back_populates="generated_videos")
|
||||
duplicate_target = relationship("GeneratedVideoModel", remote_side=[id])
|
||||
|
||||
|
||||
class TaskModel(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(200), nullable=False)
|
||||
description = Column(Text, nullable=False, default="")
|
||||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||||
priority = Column(String(20), nullable=False, default="medium")
|
||||
parent_task_id = Column(String(32), nullable=False, default="", index=True)
|
||||
assignee_user_id = Column(String(32), nullable=False, default="")
|
||||
parent_task_id = Column(String(32), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
assignee_user_id = Column(String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
progress = Column(Float, nullable=False, default=0.0)
|
||||
planned_start_date = Column(DateTime, nullable=True)
|
||||
planned_end_date = Column(DateTime, nullable=True)
|
||||
@@ -238,12 +307,18 @@ class TaskModel(Base):
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="tasks")
|
||||
parent_task = relationship("TaskModel", remote_side=[id])
|
||||
assignee = relationship("UserModel")
|
||||
issues = relationship("TaskIssueModel", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MilestoneModel(Base):
|
||||
__tablename__ = "milestones"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(200), nullable=False)
|
||||
description = Column(Text, nullable=False, default="")
|
||||
target_date = Column(DateTime, nullable=True)
|
||||
@@ -252,17 +327,25 @@ class MilestoneModel(Base):
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
project = relationship("ProjectModel", back_populates="milestones")
|
||||
|
||||
|
||||
class TaskIssueModel(Base):
|
||||
__tablename__ = "task_issues"
|
||||
|
||||
id = Column(String(32), primary_key=True)
|
||||
task_id = Column(String(32), nullable=False, index=True)
|
||||
project_id = Column(String(32), nullable=False, index=True)
|
||||
task_id = Column(String(32), ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
project_id = Column(String(32), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
title = Column(String(200), nullable=False)
|
||||
description = Column(Text, nullable=False, default="")
|
||||
resolved = Column(Boolean, nullable=False, default=False)
|
||||
resolved_at = Column(DateTime, nullable=True)
|
||||
created_by_user_id = Column(String(32), nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
# relationships
|
||||
task = relationship("TaskModel", back_populates="issues")
|
||||
project = relationship("ProjectModel", back_populates="task_issues")
|
||||
created_by = relationship("UserModel")
|
||||
|
||||
Reference in New Issue
Block a user