from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, text from sqlalchemy.orm import Mapped, mapped_column from app.enums.private_portrait import ( PrivatePortraitProjectStatus, PrivatePortraitRemoteDeleteStatus, ) from app.models.base import Base, SoftDeleteMixin, TimestampMixin class VpV3Project(Base, TimestampMixin, SoftDeleteMixin): """API V3 虚拟素材项目(按 API Key 隔离)。 一个 VpV3Project 对应火山远端的 1 个 AssetGroup(一对一:这里不做 nested group)。 """ __tablename__ = "vp_v3_projects" __table_args__ = ( Index("idx_vp_v3_projects_key_status_created", "api_key_id", "status", "created_at"), Index( "idx_vp_v3_projects_key_deleted", "api_key_id", "deleted_at", postgresql_where=text("deleted_at IS NULL"), ), Index("idx_vp_v3_projects_remote_project_name", "remote_project_name"), Index("idx_vp_v3_projects_remote_group_id", "remote_group_id"), ) id: Mapped[str] = mapped_column(String(32), primary_key=True) api_key_id: Mapped[str] = mapped_column( String(32), ForeignKey("api_keys.id", ondelete="CASCADE"), nullable=False, index=True, comment="所属 API Key(V3 调用方)", ) name: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="项目展示名称") name_slug: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="名称安全 slug(构建远端 GroupName 用)") description: Mapped[str | None] = mapped_column(Text, nullable=True) # 火山远端映射 remote_project_name: Mapped[str] = mapped_column( String(256), nullable=False, index=True, comment="火山 ProjectName(快照)", ) remote_group_id: Mapped[str] = mapped_column( String(128), nullable=False, index=True, comment="火山 AssetGroup Id", ) remote_group_name: Mapped[str | None] = mapped_column(String(256), nullable=True, comment="火山 AssetGroup Name 快照") status: Mapped[str] = mapped_column( String(32), nullable=False, default=PrivatePortraitProjectStatus.ACTIVE.value, server_default=PrivatePortraitProjectStatus.ACTIVE.value, index=True, comment="项目状态:active/creating_remote_group/create_group_failed/deleting", ) # 计数 asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") active_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") image_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") video_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") active_image_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") active_video_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") storage_mb_used: Mapped[float] = mapped_column(Integer, nullable=False, default=0, server_default="0", comment="项目占用存储 MB(未删除素材文件大小合计)") last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # 远端删除状态(沿用 private_portrait 枚举) remote_delete_status: Mapped[str] = mapped_column( String(32), nullable=False, default=PrivatePortraitRemoteDeleteStatus.NONE.value, server_default=PrivatePortraitRemoteDeleteStatus.NONE.value, index=True, ) remote_deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) remote_delete_error: Mapped[str | None] = mapped_column(Text, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment="创建失败等错误信息") raw_response_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="火山原始响应")