Files
xiaoxia-saas/packages/adapters/sqlalchemy_impl/models.py
T
xiaoxia afa7fae921
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Failing after 3m35s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m37s
AI Code Review / AI Code Review (pull_request) Successful in 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m58s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
fix(tts): 修复 AI Review 阻塞问题——克隆音色解析强制生效 + 素材库并发创建防护
1. synthesize() 克隆音色解析修复:原逻辑仅在 voice_id == profile UUID 时才把
   actual_voice_id 替换为克隆 CosyVoice id,导致 voice_id 传预置音色+显式传
   voice_clone_profile_id 时仍用预置音色合成、与记录的 profile 不一致。
   改为命中 profile 并校验通过后一律 actual_voice_id = resolved_profile.voice_id。
   补回归用例(老测试只断言 201,未覆盖解析结果)。
2. 自动创建 voice 素材库并发竞态:新增 migration 058 给 asset_libraries
   加 (project_id, kind) 唯一索引(model 同步 UniqueConstraint,staging
   2137 条数据无重复可安全建索引);创建处捕获 IntegrityError 回滚后重查,
   返回抢先创建成功的库。
3. 明确 download_asset 自动识别 URL/OSS key 的注释(Review 第 1 条为误报,
   SharedStorageService.download_asset 对 http(s):// 前缀走 HTTP 下载)。
2026-08-30 16:32:32 +08:00

620 lines
30 KiB
Python
Executable File

from datetime import datetime, timezone
from typing import Any
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import declarative_base
Base: Any = 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(Integer, nullable=False, default=3)
max_storage_gb = Column(Integer, nullable=False, default=10)
used_storage_gb = Column(Integer, nullable=False, default=0)
# 管理员标识
is_admin = Column(Boolean, nullable=False, default=False)
# 微信登录
wechat_openid = Column(String(128), nullable=True, unique=True, index=True)
wechat_unionid = Column(String(128), nullable=True, unique=True, index=True)
# 手机号绑定
phone = Column(String(32), nullable=True, unique=True, index=True)
phone_verified = Column(Boolean, nullable=False, default=False)
binding_completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class ProjectModel(Base):
__tablename__ = "projects"
id = Column(String(36), primary_key=True)
owner_user_id = Column(String(36), 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 列表
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class AssetLibraryModel(Base):
__tablename__ = "asset_libraries"
__table_args__ = (UniqueConstraint("project_id", "kind", name="uq_asset_libraries_project_kind"),)
id = Column(String(36), primary_key=True)
project_id = Column(String(36), 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)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
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)
name = Column(String(500), nullable=False)
file_type = Column(String(20), nullable=False, index=True)
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
file_size = Column(Integer, nullable=False)
# file_url: 完整可访问的 URL,用于客户端直接访问文件
file_url = Column(String(1000), nullable=False)
# storage_key: OSS 存储键,用于内部下载上传
storage_key = Column(String(500), nullable=False, default="", server_default="")
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), nullable=False)
file_hash = Column(String(64), nullable=True, index=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
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))
class TagModel(Base):
"""标签 ORM 模型。"""
__tablename__ = "tags"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
__table_args__ = (UniqueConstraint("user_id", "name", name="uq_tags_user_name"),)
class AssetTagModel(Base):
"""素材-标签关联表 ORM 模型。"""
__tablename__ = "asset_tags"
asset_id = Column(String(36), primary_key=True)
tag_id = Column(String(36), primary_key=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditTemplateModel(Base):
"""Phase 8 剪辑模板 ORM 模型
全局模板库中的模板,定义剪辑风格、配置参数和预览信息。
"""
__tablename__ = "edit_templates"
id = Column(String(36), primary_key=True)
name = Column(String(120), nullable=False)
description = Column(Text, nullable=False, default="")
template_type = Column(String(50), nullable=False, default="default", index=True)
editing_mode = Column(String(20), nullable=False, default="one_take")
config = Column(JSON, nullable=False, default=dict)
preview_url = Column(String(1000), nullable=False, default="")
sort_weight = Column(Integer, nullable=False, default=0, index=True)
status = Column(String(20), nullable=False, default="active", index=True)
version = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditTemplateVersionModel(Base):
"""模板发布版本快照 ORM 模型
每次发布保存完整快照,支持版本历史查询和回滚。
"""
__tablename__ = "edit_template_versions"
id = Column(String(36), primary_key=True)
template_id = Column(String(32), nullable=False, index=True)
version = Column(Integer, nullable=False)
name = Column(String(200), nullable=False, default="")
editing_mode = Column(String(30), nullable=False, default="one_take")
config = Column(JSON, nullable=False, default=dict)
clip_configs = Column(JSON, nullable=False, default=list)
change_note = Column(String(500), nullable=False, default="")
published_by = Column(String(36), nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditPlanModel(Base):
"""Phase 8 剪辑计划 ORM 模型
基于某个 EditTemplate 创建的剪辑计划,包含具体的配置和状态追踪。
"""
__tablename__ = "edit_plans"
id = Column(String(36), primary_key=True)
template_id = Column(String(36), nullable=False, index=True)
name = Column(String(200), nullable=False)
status = Column(String(20), nullable=False, default="draft", index=True)
total_duration = Column(Float, nullable=False, default=0.0)
config = Column(JSON, nullable=False, default=dict)
source_edit_plan_id = Column(String(36), nullable=True, index=True)
project_id = Column(String(36), nullable=False, default="", index=True)
created_by_user_id = Column(String(36), nullable=False, default="", 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))
class TemplateClipConfigModel(Base):
"""Phase 8 模板片段配置 ORM 模型
定义模板中每个片段的规则:类型、时长范围、文案模板、素材要求、转场效果。
"""
__tablename__ = "template_clip_configs"
id = Column(String(36), primary_key=True)
template_id = Column(String(36), nullable=False, index=True)
clip_type = Column(String(20), nullable=False, index=True)
order = Column(Integer, nullable=False)
min_duration = Column(Float, nullable=False, default=0.0)
max_duration = Column(Float, nullable=False, default=0.0)
text_template = Column(Text, nullable=False, default="")
material_requirements = Column(JSON, nullable=False, default=dict)
transition_effect = Column(String(20), nullable=False, default="cut")
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditPlanClipModel(Base):
"""Phase 8 剪辑计划片段 ORM 模型
剪辑计划中的具体片段实例,关联 EditPlan 和可选的 TemplateClipConfig。
"""
__tablename__ = "edit_plan_clips"
id = Column(String(36), primary_key=True)
plan_id = Column(String(36), nullable=False, index=True)
clip_type = Column(String(20), nullable=False, index=True)
order = Column(Integer, nullable=False)
template_clip_config_id = Column(String(36), nullable=False, default="", index=True)
asset_id = Column(String(36), nullable=False, default="", index=True)
text_content = Column(Text, nullable=False, default="")
start_time = Column(Float, nullable=False, default=0.0)
duration = Column(Float, nullable=False, default=0.0)
transition_effect = Column(String(20), nullable=False, default="cut")
transition_duration = Column(Float, nullable=False, default=0.0)
playback_speed = Column(Float, nullable=False, default=1.0)
status = Column(String(20), nullable=False, default="pending", index=True)
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class IngestJobModel(Base):
__tablename__ = "ingest_jobs"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, index=True)
library_id = Column(String(36), 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(36), nullable=False, default="")
file_hash = Column(String(64), 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))
class ClassificationJobModel(Base):
__tablename__ = "classification_jobs"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, index=True)
asset_id = Column(String(36), 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))
class GenerationTaskModel(Base):
__tablename__ = "generation_tasks"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, default="", index=True)
strategy_id = Column(String(36), nullable=False, default="")
asset_library_id = Column(String(36), nullable=False, default="", index=True)
voice_library_id = Column(String(36), nullable=False, default="")
template_id = Column(String(36), nullable=False, default="", index=True)
asset_ids = Column(JSON, nullable=False, default=list)
title_ids = Column(JSON, nullable=False, default=list)
voice_ids = Column(JSON, nullable=False, default=list)
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="")
error_info = Column(JSON, nullable=True)
retry_count = Column(Integer, nullable=False, default=0)
auto_retry_enabled = Column(Boolean, nullable=False, default=False)
auto_retry_max = Column(Integer, nullable=False, default=0)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
source_edit_plan_id = Column(String(36), nullable=True, index=True)
asset_select_mode = Column(String(20), nullable=False, default="")
batch_id = Column(String(36), nullable=False, default="", index=True)
video_title = Column(String(255), nullable=False, default="")
resolution = Column(String(20), nullable=False, default="")
is_preview = Column(Boolean, nullable=False, default=False, index=True)
source_task_id = Column(String(32), nullable=False, default="", index=True)
output_width = Column(Integer, nullable=False, default=1280)
output_height = Column(Integer, nullable=False, default=720)
cover_url = Column(String(1000), nullable=False, default="")
custom_title = Column(String(500), nullable=False, default="")
title_config = Column(JSON, nullable=False, default=dict)
bgm_config = Column(JSON, nullable=False, default=dict)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
logs = Column(Text, nullable=False, default="[]", server_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),
onupdate=lambda: datetime.now(timezone.utc),
)
class GeneratedVideoModel(Base):
__tablename__ = "generated_videos"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, index=True)
user_id = Column(String(36), nullable=False, default="", index=True)
generation_task_id = Column(String(36), 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(36), nullable=True)
class TitleLibraryModel(Base):
__tablename__ = "title_libraries"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(255), nullable=False)
description = Column(Text, nullable=False, default="")
category = Column(String(50), nullable=False, default="default", index=True)
text = Column(String(500), nullable=False)
tags = Column(JSON, nullable=False, default=list)
usage_count = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True, index=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class VoiceCloneProfileModel(Base):
"""音色克隆档案 ORM 模型。"""
__tablename__ = "voice_clone_profiles"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
description = Column(Text, nullable=False, default="")
source_audio_url = Column(Text, nullable=False, default="")
voice_id = Column(String(100), nullable=False, default="")
voice_model = Column(String(100), nullable=False, default="")
language = Column(String(20), nullable=False, default="zh-CN")
gender = Column(String(20), nullable=False, default="unknown")
status = Column(String(20), nullable=False, default="pending", index=True)
error_message = Column(Text, nullable=False, default="")
retry_count = Column(Integer, nullable=False, default=0)
max_retries = Column(Integer, nullable=False, default=3)
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class VoiceLibraryModel(Base):
__tablename__ = "voice_libraries"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
project_id = Column(String(36), nullable=True, index=True)
name = Column(String(255), nullable=False)
text = Column(Text, nullable=False, default="")
voice_provider = Column(String(50), nullable=False, default="")
voice_id = Column(String(100), nullable=False, default="")
voice_name = Column(String(100), nullable=False, default="")
audio_url = Column(String(1000), nullable=False, default="")
duration = Column(Float, nullable=False, default=0)
file_size = Column(Integer, nullable=False, default=0)
status = Column(String(20), nullable=False, default="completed", index=True)
tags = Column(JSON, nullable=False, default=list)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class DuplicationRecordModel(Base):
__tablename__ = "duplication_records"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
filename = Column(String(500), nullable=False)
file_size = Column(Integer, nullable=False)
storage_key = Column(String(500), nullable=False)
duration_seconds = Column(Float, nullable=False, default=0)
status = Column(String(20), nullable=False, default="pending", index=True)
duplicate_rate = Column(Float, nullable=True)
duplicate_count = Column(Integer, nullable=False, default=0)
video_fingerprint = Column(Text, nullable=True)
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))
class DuplicationSegmentModel(Base):
__tablename__ = "duplication_segments"
id = Column(String(36), primary_key=True)
record_id = Column(String(36), nullable=False, index=True)
source_start = Column(Float, nullable=False)
source_end = Column(Float, nullable=False)
matched_video_id = Column(String(36), nullable=False)
matched_video_name = Column(String(500), nullable=False, default="")
matched_start = Column(Float, nullable=False)
matched_end = Column(Float, nullable=False)
similarity = Column(Float, nullable=False)
class RecipeModel(Base):
__tablename__ = "recipes"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(200), nullable=False)
description = Column(Text, nullable=False, default="")
template_id = Column(String(36), nullable=False, default="")
generation_params = Column(JSON, nullable=False, default=dict)
is_active = Column(Boolean, nullable=False, default=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class RecipeItemModel(Base):
__tablename__ = "recipe_items"
id = Column(String(36), primary_key=True)
recipe_id = Column(String(36), nullable=False, index=True)
item_type = Column(String(20), nullable=False)
item_id = Column(String(36), nullable=False)
position = Column(Integer, nullable=False, default=0)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
class TemplateModel(Base):
__tablename__ = "templates"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(200), nullable=False)
mode = Column(String(30), nullable=False, index=True) # EditingMode 枚举值: pip / voice_pip / one_take / voice_over
category = Column(String(100), nullable=False, default="")
tags = Column(JSON, nullable=False, default=list)
title_config = Column(JSON, nullable=False, default=dict)
subtitle_config = Column(JSON, nullable=False, default=dict)
bgm_config = Column(JSON, nullable=False, default=dict)
estimated_duration = Column(Float, nullable=False, default=0.0)
is_active = Column(Boolean, nullable=False, default=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))
class TemplateSegmentModel(Base):
__tablename__ = "template_segments"
id = Column(String(36), primary_key=True)
template_id = Column(String(36), nullable=False, index=True)
segment_order = Column(Integer, nullable=False)
duration_min = Column(Float, nullable=False)
duration_max = Column(Float, nullable=False)
material_type = Column(String(20), nullable=True) # 仅 voice_over 模式: 人物/场景
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class TemplateCategoryModel(Base):
__tablename__ = "template_categories"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class JobModel(Base):
"""Phase 8 任务 2.10 — 统一异步任务 ORM 模型。"""
__tablename__ = "jobs"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, index=True)
job_type = Column(String(30), nullable=False, index=True)
status = Column(String(20), nullable=False, default="pending", index=True)
progress = Column(Float, nullable=False, default=0.0)
current_stage = Column(String(200), nullable=False, default="")
payload = Column(JSON, nullable=False, default=dict)
result = Column(JSON, nullable=False, default=dict)
error_message = Column(Text, nullable=False, default="")
retry_count = Column(Integer, nullable=False, default=0)
max_retries = Column(Integer, nullable=False, default=3)
celery_task_id = Column(String(100), nullable=False, default="")
source_id = Column(String(36), nullable=False, default="", index=True)
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
started_at = Column(DateTime, nullable=True)
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))
class TTSJobModel(Base):
"""TTS 合成任务 ORM 模型。"""
__tablename__ = "tts_jobs"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
input_text = Column(Text, nullable=False)
voice_id = Column(String(100), nullable=False, default="")
voice_model = Column(String(100), nullable=False, default="")
project_id = Column(String(36), nullable=False, default="")
voice_clone_profile_id = Column(String(36), nullable=False, default="")
status = Column(String(20), nullable=False, default="pending", index=True)
output_audio_url = Column(Text, nullable=False, default="")
output_audio_key = Column(String(500), nullable=False, default="")
duration = Column(Float, nullable=False, default=0.0)
file_size = Column(Integer, nullable=False, default=0)
sample_rate = Column(Integer, nullable=False, default=22050)
format = Column(String(20), nullable=False, default="mp3")
error_message = Column(Text, nullable=False, default="")
retry_count = Column(Integer, nullable=False, default=0)
max_retries = Column(Integer, nullable=False, default=3)
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
started_at = Column(DateTime, nullable=True)
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))
class BillingRecordModel(Base):
"""账单记录"""
__tablename__ = "billing_records"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
plan_name = Column(String(50), nullable=False)
amount = Column(Float, nullable=False)
billing_cycle = Column(String(20), nullable=False)
status = Column(String(20), nullable=False, default="pending") # pending, paid, failed, refunded
payment_method = Column(String(50), nullable=True)
payment_id = Column(String(100), nullable=True) # 第三方支付流水号
invoice_url = Column(String(500), nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
paid_at = Column(DateTime, nullable=True)
class VerificationCodeModel(Base):
"""验证码(邮箱/手机统一管理)"""
__tablename__ = "verification_codes"
id = Column(String(36), primary_key=True)
recipient = Column(String(255), nullable=False, index=True) # 邮箱或手机号
code = Column(String(10), nullable=False)
code_type = Column(String(32), nullable=False, index=True) # email_bind / phone_bind / ...
expires_at = Column(DateTime, nullable=False)
used_at = Column(DateTime, nullable=True)
attempts = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class VideoShareModel(Base):
"""视频分享记录."""
__tablename__ = "video_shares"
id = Column(String(32), primary_key=True)
video_id = Column(String(32), nullable=False, index=True)
user_id = Column(String(32), nullable=False, index=True)
share_token = Column(String(16), nullable=False, unique=True)
password_hash = Column(String(255), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
view_count = Column(Integer, nullable=False, default=0)
download_count = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
class CoverTemplateModel(Base):
"""封面模板"""
__tablename__ = "cover_templates"
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=True, index=True) # NULL = 系统模板
name = Column(String(200), nullable=False)
thumbnail_url = Column(String(1000), nullable=False, default="")
is_system = Column(Boolean, nullable=False, default=False, index=True)
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))