Files
xiaoxia-saas/packages/adapters/sqlalchemy_impl/models.py
T
CI Bot 8e2e352cfe
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
Auto Approve CI PRs / Auto Approve on CI Green (pull_request) Failing after 0s
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 13s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 40s
AI Code Review / AI Code Review (pull_request) Failing after 1m23s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m47s
Auto Merge CI PRs / Auto Merge on CI Green + Approved (pull_request) Successful in 3m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m27s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m5s
fix(worker): 素材下载改用storage_key字段,修复file_url为URL导致下载无效文件
- AssetModel新增storage_key字段(OSS存储键)
- render_adapter _download_assets改用a.storage_key
- render_adapter BGM下载改用storage_key
- edit_plan_generation legacy路径同步修复
- generation.py BGM下载同步修复
- asset_repository映射同步更新storage_key字段
- 单元测试同步更新
2026-07-17 18:04:51 +08:00

530 lines
25 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)
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"
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)
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 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)
result_count = Column(Integer, nullable=False, default=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)
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))
class GeneratedVideoModel(Base):
__tablename__ = "generated_videos"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), nullable=False, 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)