Files
xiaoxia-saas/packages/adapters/sqlalchemy_impl/models.py
T
API文档维护Agent 01f5541e14
Tests / test (pull_request) Failing after 0s
Tests / lint (pull_request) Failing after 0s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
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
2026-06-28 08:45:44 +08:00

352 lines
19 KiB
Python
Executable File

from datetime import datetime, timezone
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()
class UserModel(Base):
__tablename__ = "users"
id = Column(String(36), primary_key=True)
email = Column(String(255), nullable=False, unique=True, index=True)
username = Column(String(100), nullable=True, unique=True, index=True)
display_name = Column(String(255), nullable=False)
password_hash = Column(String(255), nullable=False)
email_verified = Column(Boolean, nullable=False, default=False)
email_verification_token = Column(String(255), nullable=True)
password_reset_token = Column(String(255), nullable=True)
password_reset_expires_at = Column(DateTime, nullable=True)
last_login_at = Column(DateTime, nullable=True)
last_login_ip = Column(String(50), nullable=True)
# 订阅相关字段 (移到 User 级别)
subscription_plan = Column(String(20), nullable=False, default="free")
subscription_status = Column(String(20), nullable=False, default="active")
subscription_expires_at = Column(DateTime, nullable=True)
# 配额限制 (移到 User 级别)
max_projects = Column(Float, nullable=False, default=3)
max_storage_gb = Column(Float, nullable=False, default=10)
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), 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), 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)
total_size = Column(Float, nullable=False, default=0)
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), 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 对象键(相对路径),用于内部存储和操作
storage_key = Column(String(255), nullable=False)
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
file_size = Column(Integer, nullable=False)
# file_url: 完整可访问的 URL,用于客户端直接访问文件
file_url = Column(String(1000), nullable=False)
thumbnail_url = Column(String(1000), nullable=True)
duration = Column(Float, nullable=True)
width = Column(Float, nullable=True)
height = Column(Float, nullable=True)
fps = Column(Float, nullable=True)
codec = Column(String(50), nullable=True)
status = Column(String(20), nullable=False, default="uploading", index=True)
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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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)
error_message = Column(Text, nullable=False, default="")
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), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
strategy_id = Column(String(32), nullable=False, default="")
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), 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)
result_count = Column(Float, nullable=False, default=0)
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), 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), 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)
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
file_size = Column(Integer, nullable=False)
duration = Column(Float, nullable=False)
thumbnail_url = Column(String(1000), nullable=True)
width = Column(Float, nullable=False)
height = Column(Float, nullable=False)
fps = Column(Float, nullable=False)
status = Column(String(20), nullable=False, default="completed", index=True)
review_status = Column(String(20), nullable=False, default="pending_review", index=True)
generation_params = Column(Text, nullable=False, default="{}")
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
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), 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), 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), 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)
actual_start_date = Column(DateTime, nullable=True)
actual_end_date = Column(DateTime, nullable=True)
tags_json = Column(Text, nullable=False, default="[]")
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), 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)
completed = Column(Boolean, nullable=False, default=False)
completed_at = Column(DateTime, 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="milestones")
class TaskIssueModel(Base):
__tablename__ = "task_issues"
id = Column(String(32), primary_key=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), 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")