Merge remote-tracking branch 'origin/develop' into refactor/1894-merge-title-library-into-scripts
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 16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m42s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m3s
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 Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 10h2m29s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 10h2m59s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 10h4m52s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 10h1m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 10h1m35s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 10h1m40s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 10h1m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 10h4m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 10h4m4s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 10h4m6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 10h1m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 10h1m30s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 10h2m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 10h1m30s
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 16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m42s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m3s
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 Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 10h2m29s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 10h2m59s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 10h4m52s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 10h1m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 10h1m35s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 10h1m40s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 10h1m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 10h4m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 10h4m4s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 10h4m6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 10h1m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 10h1m30s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 10h2m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 10h1m30s
# Conflicts: # alembic/versions/077_merge_title_libs.py # tests/unit/test_scripts_routes.py
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -112,7 +112,7 @@ class SQLAlchemyAssetRepository:
|
||||
return self.find_by_id(asset_id)
|
||||
|
||||
def create(self, asset: Asset) -> Asset:
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
model = AssetModel(
|
||||
id=asset.id,
|
||||
project_id=asset.project_id,
|
||||
@@ -176,7 +176,7 @@ class SQLAlchemyAssetRepository:
|
||||
model.file_hash = asset.file_hash or model.file_hash
|
||||
if getattr(model, "client_upload_id", None) is None and asset.client_upload_id:
|
||||
model.client_upload_id = asset.client_upload_id
|
||||
model.updated_at = datetime.now(timezone.utc)
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.flush()
|
||||
self._sync_asset_tags(asset.id, asset.tag_ids)
|
||||
self.session.commit()
|
||||
@@ -214,9 +214,9 @@ class SQLAlchemyAssetRepository:
|
||||
"""批量删除素材(软删除,标记 status=deleted),返回实际影响数量。"""
|
||||
if not asset_ids:
|
||||
return 0
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
# 先查询待删除素材的库分布(用于更新计数)
|
||||
to_delete = (
|
||||
self.session.query(AssetModel.asset_library_id, AssetModel.file_size)
|
||||
@@ -262,9 +262,9 @@ class SQLAlchemyAssetRepository:
|
||||
"""批量更新素材 metadata(合并 patch),返回实际影响数量。"""
|
||||
if not asset_ids:
|
||||
return 0
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
# 逐条读取 + 合并 + 更新,保证 JSON 合并正确
|
||||
models = self.session.query(AssetModel).filter(AssetModel.id.in_(asset_ids)).all()
|
||||
count = 0
|
||||
@@ -286,9 +286,9 @@ class SQLAlchemyAssetRepository:
|
||||
"""批量给素材添加标签(合并去重),返回实际影响数量。"""
|
||||
if not asset_ids or not tag_ids:
|
||||
return 0
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
clean_tag_ids = list(set(tag_ids))
|
||||
count = 0
|
||||
for aid in asset_ids:
|
||||
@@ -313,9 +313,9 @@ class SQLAlchemyAssetRepository:
|
||||
"""批量替换素材标签(全量覆盖),返回实际影响数量。"""
|
||||
if not asset_ids:
|
||||
return 0
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
clean_tag_ids = list(set(tag_ids))
|
||||
count = 0
|
||||
for aid in asset_ids:
|
||||
@@ -556,13 +556,13 @@ class SQLAlchemyAssetRepository:
|
||||
file_size=0(大小未知)时直接返回 None——宁可漏判(极端情况下多建一条
|
||||
占位)也不可仅凭同名 + processing 误杀内容全新的视频。
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
if not name:
|
||||
return None
|
||||
if not file_size or file_size <= 0:
|
||||
return None
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes)
|
||||
cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes)
|
||||
query = self.session.query(AssetModel).filter(
|
||||
AssetModel.asset_library_id == library_id,
|
||||
AssetModel.name == name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -36,7 +36,7 @@ class SQLAlchemyBillingRepository:
|
||||
model.status = "paid"
|
||||
model.payment_method = payment_method
|
||||
model.payment_id = payment_id
|
||||
model.paid_at = datetime.now(timezone.utc)
|
||||
model.paid_at = datetime.now(UTC)
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from datetime import UTC, date, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import DailyUsageRecordModel
|
||||
from packages.domain.daily_usage_record import DailyUsageRecord
|
||||
|
||||
|
||||
class SQLAlchemyDailyUsageRepository:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def create(self, record: DailyUsageRecord) -> DailyUsageRecord:
|
||||
model = DailyUsageRecordModel(
|
||||
id=record.id,
|
||||
user_id=record.user_id,
|
||||
usage_date=record.usage_date,
|
||||
usage_type=record.usage_type,
|
||||
count=record.count,
|
||||
updated_at=record.updated_at,
|
||||
)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return record
|
||||
|
||||
def get_by_user_and_date(
|
||||
self, user_id: str, usage_date: date, usage_type: str = "free_clip"
|
||||
) -> DailyUsageRecord | None:
|
||||
model = (
|
||||
self.session.query(DailyUsageRecordModel)
|
||||
.filter(
|
||||
DailyUsageRecordModel.user_id == user_id,
|
||||
DailyUsageRecordModel.usage_date == usage_date,
|
||||
DailyUsageRecordModel.usage_type == usage_type,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if model is None:
|
||||
return None
|
||||
return self._to_domain(model)
|
||||
|
||||
def update_count(self, record: DailyUsageRecord) -> DailyUsageRecord:
|
||||
model = self.session.query(DailyUsageRecordModel).filter(DailyUsageRecordModel.id == record.id).first()
|
||||
if model is None:
|
||||
return record
|
||||
model.count = record.count
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return record
|
||||
|
||||
def upsert(self, user_id: str, usage_date: date, usage_type: str = "free_clip") -> DailyUsageRecord:
|
||||
"""Increment usage count for the given user/date/type, creating if needed."""
|
||||
model = (
|
||||
self.session.query(DailyUsageRecordModel)
|
||||
.filter(
|
||||
DailyUsageRecordModel.user_id == user_id,
|
||||
DailyUsageRecordModel.usage_date == usage_date,
|
||||
DailyUsageRecordModel.usage_type == usage_type,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if model is None:
|
||||
record = DailyUsageRecord.create(user_id=user_id, usage_date=usage_date, usage_type=usage_type)
|
||||
record.count = 1
|
||||
model = DailyUsageRecordModel(
|
||||
id=record.id,
|
||||
user_id=record.user_id,
|
||||
usage_date=record.usage_date,
|
||||
usage_type=record.usage_type,
|
||||
count=1,
|
||||
updated_at=datetime.now(UTC),
|
||||
)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return record
|
||||
|
||||
model.count += 1
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return self._to_domain(model)
|
||||
|
||||
@staticmethod
|
||||
def _to_domain(model: DailyUsageRecordModel) -> DailyUsageRecord:
|
||||
return DailyUsageRecord(
|
||||
id=model.id,
|
||||
user_id=model.user_id,
|
||||
usage_date=model.usage_date,
|
||||
usage_type=model.usage_type,
|
||||
count=model.count,
|
||||
updated_at=model.updated_at,
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -23,7 +23,7 @@ class SQLAlchemyEditPlanClipRepository:
|
||||
status: Optional[EditPlanClipStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[EditPlanClip]:
|
||||
) -> list[EditPlanClip]:
|
||||
"""按计划列出片段,按 order 排序"""
|
||||
query = self.session.query(EditPlanClipModel).filter(
|
||||
EditPlanClipModel.plan_id == plan_id,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -23,7 +23,7 @@ class SQLAlchemyEditPlanRepository:
|
||||
status: Optional[EditPlanStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditPlan]:
|
||||
) -> list[EditPlan]:
|
||||
"""按模板列出剪辑计划"""
|
||||
query = self.session.query(EditPlanModel).filter(
|
||||
EditPlanModel.template_id == template_id,
|
||||
@@ -40,7 +40,7 @@ class SQLAlchemyEditPlanRepository:
|
||||
status: Optional[EditPlanStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditPlan]:
|
||||
) -> list[EditPlan]:
|
||||
"""列出所有剪辑计划"""
|
||||
query = self.session.query(EditPlanModel)
|
||||
if status:
|
||||
@@ -63,7 +63,7 @@ class SQLAlchemyEditPlanRepository:
|
||||
status: Optional[EditPlanStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditPlan]:
|
||||
) -> list[EditPlan]:
|
||||
"""按项目列出剪辑计划"""
|
||||
query = self.session.query(EditPlanModel).filter(
|
||||
EditPlanModel.project_id == project_id,
|
||||
@@ -81,7 +81,7 @@ class SQLAlchemyEditPlanRepository:
|
||||
status: Optional[EditPlanStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditPlan]:
|
||||
) -> list[EditPlan]:
|
||||
"""列出用户创建的剪辑计划"""
|
||||
query = self.session.query(EditPlanModel).filter(
|
||||
EditPlanModel.created_by_user_id == user_id,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -22,7 +22,7 @@ class SQLAlchemyEditTemplateRepository:
|
||||
template_type: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditTemplate]:
|
||||
) -> list[EditTemplate]:
|
||||
"""列出所有激活的模板"""
|
||||
query = self.session.query(EditTemplateModel).filter(
|
||||
EditTemplateModel.status == EditTemplateStatus.ACTIVE,
|
||||
@@ -43,7 +43,7 @@ class SQLAlchemyEditTemplateRepository:
|
||||
status: Optional[EditTemplateStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[EditTemplate]:
|
||||
) -> list[EditTemplate]:
|
||||
"""列出所有模板(含非激活)"""
|
||||
query = self.session.query(EditTemplateModel)
|
||||
if template_type:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -339,7 +339,7 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
"""同 cleanup_stale_running,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。"""
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
|
||||
cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
|
||||
models = (
|
||||
self.session.query(GenerationTaskModel)
|
||||
.filter(
|
||||
@@ -358,9 +358,9 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
model.error_info = {
|
||||
"error_type": "WorkerInterrupted",
|
||||
"message": "任务在运行中中断,可能因 worker 重启或超时",
|
||||
"failed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"failed_at": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
model.completed_at = datetime.now(timezone.utc)
|
||||
model.completed_at = datetime.now(UTC)
|
||||
self.session.commit()
|
||||
return result
|
||||
|
||||
@@ -377,7 +377,7 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
"""同 cleanup_stale_pending,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。"""
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
|
||||
cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
|
||||
models = (
|
||||
self.session.query(GenerationTaskModel)
|
||||
.filter(
|
||||
@@ -391,7 +391,7 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
error_info = {
|
||||
"error_type": "PendingTimeout",
|
||||
"message": f"任务在 pending 状态停留超过 {timeout_minutes} 分钟,自动清理",
|
||||
"failed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"failed_at": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
result: list[tuple[str, str]] = []
|
||||
for model in models:
|
||||
@@ -399,6 +399,6 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
model.status = GenerationTaskStatus.FAILED.value
|
||||
model.error_message = "pending timeout: auto cleanup"
|
||||
model.error_info = error_info
|
||||
model.completed_at = datetime.now(timezone.utc)
|
||||
model.completed_at = datetime.now(UTC)
|
||||
self.session.commit()
|
||||
return result
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Index, Integer, String, Text, UniqueConstraint, text
|
||||
@@ -39,7 +39,12 @@ class UserModel(Base):
|
||||
phone_verified = Column(Boolean, nullable=False, default=False)
|
||||
binding_completed_at = Column(DateTime, nullable=True)
|
||||
profile_completed = Column(Boolean, nullable=False, default=True, server_default="true")
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
# 会员+积分 (#1895)
|
||||
is_member = Column(Boolean, nullable=False, default=False)
|
||||
member_type = Column(String(20), nullable=True)
|
||||
member_expires_at = Column(DateTime, nullable=True)
|
||||
points_balance = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class ProjectModel(Base):
|
||||
@@ -57,7 +62,7 @@ class ProjectModel(Base):
|
||||
shared_users = Column(JSON, nullable=False, default=list) # 被共享的用户 ID 列表
|
||||
is_default = Column(Boolean, nullable=False, default=False, server_default="false")
|
||||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class AssetLibraryModel(Base):
|
||||
@@ -71,8 +76,8 @@ class AssetLibraryModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class AssetModel(Base):
|
||||
@@ -103,8 +108,8 @@ class AssetModel(Base):
|
||||
file_hash = Column(String(64), nullable=True, index=True)
|
||||
client_upload_id = 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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC), index=True)
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class TagModel(Base):
|
||||
@@ -115,7 +120,7 @@ class TagModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
__table_args__ = (UniqueConstraint("user_id", "name", name="uq_tags_user_name"),)
|
||||
|
||||
@@ -127,7 +132,7 @@ class AssetTagModel(Base):
|
||||
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class EditTemplateModel(Base):
|
||||
@@ -148,8 +153,8 @@ class EditTemplateModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class EditTemplateVersionModel(Base):
|
||||
@@ -169,7 +174,7 @@ class EditTemplateVersionModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class EditPlanModel(Base):
|
||||
@@ -189,8 +194,8 @@ class EditPlanModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class TemplateClipConfigModel(Base):
|
||||
@@ -211,8 +216,8 @@ class TemplateClipConfigModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class EditPlanClipModel(Base):
|
||||
@@ -237,8 +242,8 @@ class EditPlanClipModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class IngestJobModel(Base):
|
||||
@@ -254,8 +259,8 @@ class IngestJobModel(Base):
|
||||
file_hash = Column(String(64), nullable=True, index=True)
|
||||
asset_id = Column(String(36), nullable=False, default="", index=True)
|
||||
celery_task_id = Column(String(64), 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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class ClassificationJobModel(Base):
|
||||
@@ -268,8 +273,8 @@ class ClassificationJobModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class GenerationTaskModel(Base):
|
||||
@@ -315,12 +320,12 @@ class GenerationTaskModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
|
||||
|
||||
@@ -344,8 +349,8 @@ class GeneratedVideoModel(Base):
|
||||
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))
|
||||
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=True)
|
||||
video_fingerprint = Column(Text, nullable=True)
|
||||
is_duplicate = Column(Boolean, nullable=False, default=False)
|
||||
@@ -368,8 +373,8 @@ class TitleLibraryModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class VoiceCloneProfileModel(Base):
|
||||
@@ -391,8 +396,8 @@ class VoiceCloneProfileModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class VoiceLibraryModel(Base):
|
||||
@@ -412,8 +417,8 @@ class VoiceLibraryModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class DuplicationRecordModel(Base):
|
||||
@@ -433,8 +438,8 @@ class DuplicationRecordModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class DuplicationSegmentModel(Base):
|
||||
@@ -462,8 +467,8 @@ class RecipeModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class RecipeItemModel(Base):
|
||||
@@ -491,8 +496,8 @@ class TemplateModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class TemplateSegmentModel(Base):
|
||||
@@ -504,8 +509,8 @@ class TemplateSegmentModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class TemplateCategoryModel(Base):
|
||||
@@ -515,7 +520,7 @@ class TemplateCategoryModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class JobModel(Base):
|
||||
@@ -539,8 +544,8 @@ class JobModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class TTSJobModel(Base):
|
||||
@@ -568,8 +573,8 @@ class TTSJobModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class BillingRecordModel(Base):
|
||||
@@ -586,7 +591,7 @@ class BillingRecordModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
paid_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
@@ -602,7 +607,7 @@ class VerificationCodeModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class VideoShareModel(Base):
|
||||
@@ -619,8 +624,8 @@ class VideoShareModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class CoverTemplateModel(Base):
|
||||
@@ -634,8 +639,8 @@ class CoverTemplateModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class VideoFingerprintChunkModel(Base):
|
||||
@@ -652,7 +657,7 @@ class VideoFingerprintChunkModel(Base):
|
||||
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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class ScriptModel(Base):
|
||||
@@ -670,8 +675,8 @@ class ScriptModel(Base):
|
||||
title_text = Column(String(500), nullable=False, default="")
|
||||
title_category = Column(String(50), nullable=False, default="")
|
||||
title_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))
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class LipsyncJobModel(Base):
|
||||
@@ -713,8 +718,8 @@ class LipsyncJobModel(Base):
|
||||
# 时间戳
|
||||
submitted_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))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class AiAvatarRenderJob(Base):
|
||||
@@ -745,5 +750,70 @@ class AiAvatarRenderJob(Base):
|
||||
submitted_at = Column(DateTime, nullable=True)
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
completed_at = Column(DateTime, nullable=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))
|
||||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class PointsAccountModel(Base):
|
||||
"""积分账户 ORM 模型 (#1895)"""
|
||||
|
||||
__tablename__ = "points_accounts"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
user_id = Column(String(36), nullable=False, unique=True, index=True)
|
||||
balance = Column(Integer, nullable=False, default=0)
|
||||
total_earned = Column(Integer, nullable=False, default=0)
|
||||
total_spent = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class PointsTransactionModel(Base):
|
||||
"""积分流水 ORM 模型 (#1895)"""
|
||||
|
||||
__tablename__ = "points_transactions"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
user_id = Column(String(36), nullable=False, index=True)
|
||||
account_id = Column(String(36), nullable=False, index=True)
|
||||
type = Column(String(20), nullable=False, index=True) # earn / spend / refund
|
||||
source = Column(String(50), nullable=False, index=True)
|
||||
amount = Column(Integer, nullable=False)
|
||||
balance_after = Column(Integer, nullable=False)
|
||||
description = Column(String(255), nullable=False, default="")
|
||||
ref_id = Column(String(100), nullable=False, default="")
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class PointsOrderModel(Base):
|
||||
"""积分/会员订单 ORM 模型 (#1895)"""
|
||||
|
||||
__tablename__ = "points_orders"
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
user_id = Column(String(36), nullable=False, index=True)
|
||||
order_type = Column(String(20), nullable=False) # membership / points
|
||||
product_code = Column(String(50), nullable=False)
|
||||
amount_cents = Column(Integer, nullable=False)
|
||||
original_amount_cents = Column(Integer, nullable=False, default=0)
|
||||
discount = Column(Float, nullable=False, default=1.0)
|
||||
points_amount = Column(Integer, nullable=False, default=0)
|
||||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||||
payment_method = Column(String(50), nullable=True)
|
||||
payment_id = Column(String(100), nullable=True)
|
||||
paid_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class DailyUsageRecordModel(Base):
|
||||
"""每日使用记录 ORM 模型 (#1895)"""
|
||||
|
||||
__tablename__ = "daily_usage_records"
|
||||
__table_args__ = (UniqueConstraint("user_id", "usage_date", "usage_type", name="uq_daily_usage_user_date_type"),)
|
||||
|
||||
id = Column(String(36), primary_key=True)
|
||||
user_id = Column(String(36), nullable=False, index=True)
|
||||
usage_date = Column(DateTime, nullable=False) # stored as DATE in SQL but DateTime for ORM compat
|
||||
usage_type = Column(String(50), nullable=False, default="free_clip")
|
||||
count = Column(Integer, nullable=False, default=0)
|
||||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import PointsAccountModel
|
||||
from packages.domain.points_account import PointsAccount
|
||||
|
||||
|
||||
class SQLAlchemyPointsAccountRepository:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def create(self, account: PointsAccount) -> PointsAccount:
|
||||
model = PointsAccountModel(
|
||||
id=account.id,
|
||||
user_id=account.user_id,
|
||||
balance=account.balance,
|
||||
total_earned=account.total_earned,
|
||||
total_spent=account.total_spent,
|
||||
created_at=account.created_at,
|
||||
updated_at=account.updated_at,
|
||||
)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return account
|
||||
|
||||
def get_by_user_id(self, user_id: str) -> PointsAccount | None:
|
||||
model = self.session.query(PointsAccountModel).filter(PointsAccountModel.user_id == user_id).first()
|
||||
if model is None:
|
||||
return None
|
||||
return self._to_domain(model)
|
||||
|
||||
def update_balance(self, account: PointsAccount) -> PointsAccount:
|
||||
model = self.session.query(PointsAccountModel).filter(PointsAccountModel.id == account.id).first()
|
||||
if model is None:
|
||||
return account
|
||||
model.balance = account.balance
|
||||
model.total_earned = account.total_earned
|
||||
model.total_spent = account.total_spent
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return account
|
||||
|
||||
@staticmethod
|
||||
def _to_domain(model: PointsAccountModel) -> PointsAccount:
|
||||
return PointsAccount(
|
||||
id=model.id,
|
||||
user_id=model.user_id,
|
||||
balance=model.balance,
|
||||
total_earned=model.total_earned,
|
||||
total_spent=model.total_spent,
|
||||
created_at=model.created_at,
|
||||
updated_at=model.updated_at,
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import PointsOrderModel
|
||||
from packages.domain.points_order import PointsOrder
|
||||
|
||||
|
||||
class SQLAlchemyPointsOrderRepository:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def create(self, order: PointsOrder) -> PointsOrder:
|
||||
model = PointsOrderModel(
|
||||
id=order.id,
|
||||
user_id=order.user_id,
|
||||
order_type=order.order_type,
|
||||
product_code=order.product_code,
|
||||
amount_cents=order.amount_cents,
|
||||
original_amount_cents=order.original_amount_cents,
|
||||
discount=order.discount,
|
||||
points_amount=order.points_amount,
|
||||
status=order.status,
|
||||
payment_method=order.payment_method,
|
||||
payment_id=order.payment_id,
|
||||
paid_at=order.paid_at,
|
||||
created_at=order.created_at,
|
||||
)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return order
|
||||
|
||||
def get(self, order_id: str) -> PointsOrder | None:
|
||||
model = self.session.query(PointsOrderModel).filter(PointsOrderModel.id == order_id).first()
|
||||
if model is None:
|
||||
return None
|
||||
return self._to_domain(model)
|
||||
|
||||
def update_status(
|
||||
self,
|
||||
order_id: str,
|
||||
status: str,
|
||||
*,
|
||||
payment_id: str | None = None,
|
||||
paid_at: datetime | None = None,
|
||||
) -> PointsOrder | None:
|
||||
model = self.session.query(PointsOrderModel).filter(PointsOrderModel.id == order_id).first()
|
||||
if model is None:
|
||||
return None
|
||||
model.status = status
|
||||
if payment_id is not None:
|
||||
model.payment_id = payment_id
|
||||
if paid_at is not None:
|
||||
model.paid_at = paid_at
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return self._to_domain(model)
|
||||
|
||||
def list_by_user(
|
||||
self,
|
||||
user_id: str,
|
||||
*,
|
||||
order_type: str | None = None,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[PointsOrder], int]:
|
||||
query = self.session.query(PointsOrderModel).filter(PointsOrderModel.user_id == user_id)
|
||||
if order_type:
|
||||
query = query.filter(PointsOrderModel.order_type == order_type)
|
||||
if status:
|
||||
query = query.filter(PointsOrderModel.status == status)
|
||||
|
||||
total = query.count()
|
||||
models = (
|
||||
query.order_by(PointsOrderModel.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
)
|
||||
return [self._to_domain(m) for m in models], total
|
||||
|
||||
@staticmethod
|
||||
def _to_domain(model: PointsOrderModel) -> PointsOrder:
|
||||
return PointsOrder(
|
||||
id=model.id,
|
||||
user_id=model.user_id,
|
||||
order_type=model.order_type,
|
||||
product_code=model.product_code,
|
||||
amount_cents=model.amount_cents,
|
||||
original_amount_cents=model.original_amount_cents,
|
||||
discount=model.discount,
|
||||
points_amount=model.points_amount,
|
||||
status=model.status,
|
||||
payment_method=model.payment_method,
|
||||
payment_id=model.payment_id,
|
||||
paid_at=model.paid_at,
|
||||
created_at=model.created_at,
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import PointsTransactionModel
|
||||
from packages.domain.points_transaction import PointsTransaction
|
||||
|
||||
|
||||
class SQLAlchemyPointsTransactionRepository:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def create(self, transaction: PointsTransaction) -> PointsTransaction:
|
||||
model = PointsTransactionModel(
|
||||
id=transaction.id,
|
||||
user_id=transaction.user_id,
|
||||
account_id=transaction.account_id,
|
||||
type=transaction.type,
|
||||
source=transaction.source,
|
||||
amount=transaction.amount,
|
||||
balance_after=transaction.balance_after,
|
||||
description=transaction.description,
|
||||
ref_id=transaction.ref_id,
|
||||
created_at=transaction.created_at,
|
||||
)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return transaction
|
||||
|
||||
def list_by_user(
|
||||
self,
|
||||
user_id: str,
|
||||
*,
|
||||
type: str | None = None,
|
||||
source: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[PointsTransaction], int]:
|
||||
query = self.session.query(PointsTransactionModel).filter(PointsTransactionModel.user_id == user_id)
|
||||
if type:
|
||||
query = query.filter(PointsTransactionModel.type == type)
|
||||
if source:
|
||||
query = query.filter(PointsTransactionModel.source == source)
|
||||
|
||||
total = query.count()
|
||||
models = (
|
||||
query.order_by(PointsTransactionModel.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
return [self._to_domain(m) for m in models], total
|
||||
|
||||
@staticmethod
|
||||
def _to_domain(model: PointsTransactionModel) -> PointsTransaction:
|
||||
return PointsTransaction(
|
||||
id=model.id,
|
||||
user_id=model.user_id,
|
||||
account_id=model.account_id,
|
||||
type=model.type,
|
||||
source=model.source,
|
||||
amount=model.amount,
|
||||
balance_after=model.balance_after,
|
||||
description=model.description or "",
|
||||
ref_id=model.ref_id or "",
|
||||
created_at=model.created_at,
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -22,7 +22,7 @@ class SQLAlchemyRecipeRepository:
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[Recipe]:
|
||||
) -> list[Recipe]:
|
||||
models = (
|
||||
self.session.query(RecipeModel)
|
||||
.filter(
|
||||
@@ -121,7 +121,7 @@ class SQLAlchemyRecipeRepository:
|
||||
.count()
|
||||
)
|
||||
|
||||
def list_items(self, recipe_id: str) -> List[RecipeItem]:
|
||||
def list_items(self, recipe_id: str) -> list[RecipeItem]:
|
||||
models = (
|
||||
self.session.query(RecipeItemModel)
|
||||
.filter(RecipeItemModel.recipe_id == recipe_id)
|
||||
@@ -130,7 +130,7 @@ class SQLAlchemyRecipeRepository:
|
||||
)
|
||||
return [self._item_model_to_entity(m) for m in models]
|
||||
|
||||
def create_items(self, items: List[RecipeItem]) -> List[RecipeItem]:
|
||||
def create_items(self, items: list[RecipeItem]) -> list[RecipeItem]:
|
||||
for item in items:
|
||||
model = RecipeItemModel(
|
||||
id=item.id,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -30,7 +30,7 @@ class SQLAlchemyTemplateClipConfigRepository:
|
||||
clip_type: Optional[ClipType] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[TemplateClipConfig]:
|
||||
) -> list[TemplateClipConfig]:
|
||||
"""按模板列出片段配置,按 order 排序"""
|
||||
query = self.session.query(TemplateClipConfigModel).filter(
|
||||
TemplateClipConfigModel.template_id == template_id,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -56,7 +56,7 @@ class SQLAlchemyTemplateRepository:
|
||||
keyword: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
valid_only: bool = False,
|
||||
) -> List[Template]:
|
||||
) -> list[Template]:
|
||||
query = self.session.query(TemplateModel).filter(
|
||||
TemplateModel.user_id == user_id,
|
||||
TemplateModel.is_active.is_(True),
|
||||
@@ -252,7 +252,7 @@ class SQLAlchemyTemplateRepository:
|
||||
created = self.create(new_template)
|
||||
|
||||
# 复用 create_segments 写入 template_clip_configs
|
||||
new_segments: List[TemplateSegment] = []
|
||||
new_segments: list[TemplateSegment] = []
|
||||
for seg in source.segments:
|
||||
new_segments.append(
|
||||
TemplateSegment(
|
||||
@@ -274,7 +274,7 @@ class SQLAlchemyTemplateRepository:
|
||||
|
||||
# ── Segments ──
|
||||
|
||||
def list_segments(self, template_id: str) -> List[TemplateSegment]:
|
||||
def list_segments(self, template_id: str) -> list[TemplateSegment]:
|
||||
"""优先从 template_clip_configs 读取,回退读 template_segments。"""
|
||||
clips = (
|
||||
self.session.query(TemplateClipConfigModel)
|
||||
@@ -293,7 +293,7 @@ class SQLAlchemyTemplateRepository:
|
||||
)
|
||||
return [self._segment_model_to_entity(m) for m in old]
|
||||
|
||||
def create_segments(self, segments: List[TemplateSegment]) -> List[TemplateSegment]:
|
||||
def create_segments(self, segments: list[TemplateSegment]) -> list[TemplateSegment]:
|
||||
"""写入 template_clip_configs 表。material_type 存入 config JSON。"""
|
||||
for seg in segments:
|
||||
config = {"material_type": seg.material_type} if seg.material_type else {}
|
||||
@@ -330,7 +330,7 @@ class SQLAlchemyTemplateRepository:
|
||||
|
||||
# ── Categories ──
|
||||
|
||||
def list_categories(self, user_id: str) -> List[TemplateCategory]:
|
||||
def list_categories(self, user_id: str) -> list[TemplateCategory]:
|
||||
models = (
|
||||
self.session.query(TemplateCategoryModel)
|
||||
.filter(TemplateCategoryModel.user_id == user_id)
|
||||
@@ -380,7 +380,7 @@ class SQLAlchemyTemplateRepository:
|
||||
|
||||
# ── Tags ──
|
||||
|
||||
def list_tags(self, user_id: str) -> List[str]:
|
||||
def list_tags(self, user_id: str) -> list[str]:
|
||||
"""获取用户所有模板的标签(去重)。"""
|
||||
models = (
|
||||
self.session.query(TemplateModel)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.domain.template_version import EditTemplateVersion
|
||||
@@ -51,7 +49,7 @@ class SQLAlchemyTemplateVersionRepository:
|
||||
return None
|
||||
return self._to_entity(model)
|
||||
|
||||
def list_by_template(self, template_id: str, limit: int = 50) -> List[EditTemplateVersion]:
|
||||
def list_by_template(self, template_id: str, limit: int = 50) -> list[EditTemplateVersion]:
|
||||
"""列出模板的所有历史版本(按版本号倒序)"""
|
||||
from packages.adapters.sqlalchemy_impl.models import EditTemplateVersionModel
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -24,7 +24,7 @@ class SQLAlchemyTitleLibraryRepository:
|
||||
is_active: bool = True,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[TitleLibraryItem]:
|
||||
) -> list[TitleLibraryItem]:
|
||||
query = self.session.query(TitleLibraryModel).filter(
|
||||
TitleLibraryModel.user_id == user_id,
|
||||
TitleLibraryModel.is_active == is_active,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -98,7 +98,7 @@ class SQLAlchemyTTSJobRepository:
|
||||
status: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> List[TTSJob]:
|
||||
) -> list[TTSJob]:
|
||||
query = self.session.query(TTSJobModel).filter(
|
||||
TTSJobModel.user_id == user_id,
|
||||
TTSJobModel.status != "deleted",
|
||||
@@ -125,7 +125,7 @@ class SQLAlchemyTTSJobRepository:
|
||||
status: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> List[TTSJob]:
|
||||
) -> list[TTSJob]:
|
||||
query = self.session.query(TTSJobModel).filter(
|
||||
TTSJobModel.voice_clone_profile_id == voice_clone_profile_id,
|
||||
TTSJobModel.status != "deleted",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -51,7 +51,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository):
|
||||
return self._to_entity(self.session.get(VerificationCodeModel, code_id))
|
||||
|
||||
def count_today(self, recipient: str, code_type: str) -> int:
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return (
|
||||
self.session.query(VerificationCodeModel)
|
||||
@@ -75,7 +75,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository):
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
return dt.replace(tzinfo=UTC)
|
||||
return dt
|
||||
|
||||
return VerificationCode(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -69,7 +69,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
return None
|
||||
return _model_to_domain(model)
|
||||
|
||||
def list_by_video(self, video_id: str, user_id: str) -> List[VideoShare]:
|
||||
def list_by_video(self, video_id: str, user_id: str) -> list[VideoShare]:
|
||||
models = (
|
||||
self.session.query(VideoShareModel)
|
||||
.filter(
|
||||
@@ -81,7 +81,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
)
|
||||
return [_model_to_domain(m) for m in models]
|
||||
|
||||
def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> List[VideoShare]:
|
||||
def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> list[VideoShare]:
|
||||
models = (
|
||||
self.session.query(VideoShareModel)
|
||||
.filter(VideoShareModel.user_id == user_id)
|
||||
@@ -104,7 +104,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
model.is_active = share.is_active
|
||||
model.view_count = share.view_count
|
||||
model.download_count = share.download_count
|
||||
model.updated_at = datetime.now(timezone.utc)
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return share
|
||||
@@ -121,7 +121,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
if model is None:
|
||||
return False
|
||||
model.is_active = False
|
||||
model.updated_at = datetime.now(timezone.utc)
|
||||
model.updated_at = datetime.now(UTC)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return True
|
||||
@@ -130,7 +130,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update(
|
||||
{
|
||||
"view_count": VideoShareModel.view_count + 1,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(UTC),
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
@@ -140,7 +140,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
|
||||
self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update(
|
||||
{
|
||||
"download_count": VideoShareModel.download_count + 1,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(UTC),
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -86,7 +86,7 @@ class SQLAlchemyVoiceCloneProfileRepository:
|
||||
status: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> List[VoiceCloneProfile]:
|
||||
) -> list[VoiceCloneProfile]:
|
||||
query = self.session.query(VoiceCloneProfileModel).filter(
|
||||
VoiceCloneProfileModel.user_id == user_id,
|
||||
VoiceCloneProfileModel.status != "deleted",
|
||||
@@ -119,7 +119,7 @@ class SQLAlchemyVoiceCloneProfileRepository:
|
||||
return None
|
||||
return self._model_to_entity(model)
|
||||
|
||||
def find_profile_ids_by_voice_ids(self, voice_ids: List[str]) -> Dict[str, str]:
|
||||
def find_profile_ids_by_voice_ids(self, voice_ids: list[str]) -> dict[str, str]:
|
||||
"""批量查询 voice_id → profile_id 映射。用于填充统一列表的 voice_clone_profile_id。"""
|
||||
if not voice_ids:
|
||||
return {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -23,7 +23,7 @@ class SQLAlchemyVoiceLibraryRepository:
|
||||
status: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[VoiceLibraryItem]:
|
||||
) -> list[VoiceLibraryItem]:
|
||||
query = self.session.query(VoiceLibraryModel).filter(
|
||||
VoiceLibraryModel.user_id == user_id,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user