Files
xiaoxia-saas/packages/adapters/sqlalchemy_impl/models.py
T
saas-backend-agent a5e56939a5
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m22s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m45s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit 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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 302h6m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 302h6m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 302h7m4s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 302h7m11s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 302h7m11s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 302h7m12s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 302h7m14s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 302h7m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 302h7m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 302h7m20s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 302h41m22s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 302h41m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 302h41m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 302h41m44s
feat(worker): 手动查重 worker task + visual_similarity/match_count 字段 #1661
- 新增 Celery task worker.process_duplication_check:
  OSS 下载上传视频 → 动态抽帧指纹 → 跨项目与用户所有成片比对
  (compute_duplicate_rate + find_duplicate_segments) → 更新 DuplicationRecord
- 状态流转 pending → processing → completed/failed,失败重试 3 次间隔 60s,
  重试耗尽标记 failed,临时文件 finally 清理
- 片段映射:worker 分片 DuplicateSegment(ms/汉明距离) → 领域 DuplicateSegment(秒/0-100 相似度)
- DuplicationRecord 实体新增 visual_similarity/match_count(可选 None,向后兼容),
  mark_completed()/reset_for_retry() 同步
- migration 065: duplication_records 表加两列
- repository/model/schemas/routes 读写映射与响应透传
- 上传接口与重试接口 enqueue task(此前记录一直 pending 的根因)
- 11 个新单测:成功/跳过/未找到/重试/失败标记/临时文件清理/片段映射/schema
2026-09-04 01:11:31 +08:00

645 lines
31 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)
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)
duplicate_rate = Column(Float, nullable=True)
match_count = Column(Integer, nullable=True, default=0)
visual_similarity = Column(Float, nullable=True, default=0.0)
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)
# #1661 手动查重:视觉相似度(0~1)/ 匹配视频数
visual_similarity = Column(Float, nullable=True)
match_count = Column(Integer, nullable=True)
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)
sort_order = Column(Integer, nullable=False, default=0)
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))
class VideoFingerprintChunkModel(Base):
"""分片视频指纹 — 每个视频按时间分片存储 pHash + color_histogram."""
__tablename__ = "video_fingerprint_chunks"
id = Column(String(36), primary_key=True)
video_id = Column(String(36), nullable=False, index=True)
project_id = Column(String(36), nullable=False, index=True)
user_id = Column(String(36), nullable=False, index=True, default="")
start_time_ms = Column(Integer, nullable=False)
end_time_ms = Column(Integer, nullable=False)
phash_binary = Column(String(16), nullable=False)
color_histogram = Column(JSON, nullable=False)
frame_count = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))