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:
@@ -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