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

# Conflicts:
#	alembic/versions/077_merge_title_libs.py
#	tests/unit/test_scripts_routes.py
This commit is contained in:
xiaoxia-agent
2026-09-16 05:12:08 +08:00
559 changed files with 13936 additions and 25864 deletions
@@ -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
+129 -59
View File
@@ -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,
)