1、增加调用 AI 视频生成能力和虚拟素材库管理的对外api

2、增加后台apikkey管理
3、增加apikey单独的模型定价
4、增加apikey调用情况
5、完善所有数据的注释增加
This commit is contained in:
2026-08-06 13:13:28 +08:00
parent a55d4d649c
commit 0c511f3451
102 changed files with 13986 additions and 41 deletions
@@ -0,0 +1,9 @@
from app.models.virtual_portrait_v3.api_key_quota import VpV3ApiKeyQuota
from app.models.virtual_portrait_v3.project import VpV3Project
from app.models.virtual_portrait_v3.asset import VpV3Asset
__all__ = [
"VpV3ApiKeyQuota",
"VpV3Project",
"VpV3Asset",
]
@@ -0,0 +1,61 @@
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.models.base import Base, TimestampMixin, SoftDeleteMixin
class VpV3ApiKeyQuota(Base, TimestampMixin):
"""API V3 虚拟素材库配额(每个 ApiKey 一份,默认 0=不可用)。
配额在创建/删除项目、上传/删除素材时实时统计(直接 COUNT/SUM),
避免缓存不准;配额字段默认 0,后台管理配置后才可用。
"""
__tablename__ = "vp_v3_api_key_quotas"
__table_args__ = (
Index("uq_vp_v3_api_key_quotas_key_id", "api_key_id", unique=True),
)
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,
unique=True,
index=True,
comment="所属 API Key,唯一:一个 API Key 只有一份虚拟素材配额",
)
# 配额上限(默认 0 = 不可使用该功能)
project_limit: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="虚拟项目上限,默认 0 不可创建",
)
asset_limit: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="虚拟素材总数上限(图片+视频),默认 0 不可上传",
)
storage_mb_limit: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="上传存储上限 MB,默认 0 不可上传文件",
)
# 已使用量(冗余字段提升性能,每次增删同步,和真实 COUNT 不一致时以 COUNT 为准)
project_used: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="已创建项目数(未删除)",
)
asset_used: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="已上传素材数(未删除,图片+视频)",
)
storage_mb_used: Mapped[float] = mapped_column(
Integer, nullable=False, default=0, server_default="0",
comment="已占用存储 MB(未删除文件大小合计,1MB=1024*1024",
)
remark: Mapped[str | None] = mapped_column(Text, nullable=True, comment="后台备注")
@@ -0,0 +1,95 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.private_portrait import (
PrivatePortraitAssetStatus,
PrivatePortraitAssetType,
PrivatePortraitRemoteDeleteStatus,
)
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
class VpV3Asset(Base, TimestampMixin, SoftDeleteMixin):
"""API V3 虚拟素材(图片/视频),归属某个 Project=火山 1 个 AssetGroup)。
字段语义和 private_portrait.PrivatePortraitAsset 保持一致,便于 service 层复用逻辑。
"""
__tablename__ = "vp_v3_assets"
__table_args__ = (
Index("uq_vp_v3_assets_remote_asset_id", "remote_asset_id", unique=True),
Index("idx_vp_v3_assets_key_status_created", "api_key_id", "status", "created_at"),
Index("idx_vp_v3_assets_project_status_created", "project_id", "status", "created_at"),
Index(
"idx_vp_v3_assets_next_poll_status",
"next_poll_at",
"status",
postgresql_where=text("deleted_at IS NULL AND next_poll_at IS NOT NULL"),
),
Index("idx_vp_v3_assets_remote_delete_status", "remote_delete_status"),
Index("idx_vp_v3_assets_asset_type", "asset_type"),
)
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,
)
project_id: Mapped[str] = mapped_column(
String(32), ForeignKey("vp_v3_projects.id", ondelete="CASCADE"), nullable=False, index=True,
)
# 火山远端映射
remote_project_name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
remote_group_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
remote_asset_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
# 素材元信息
asset_type: Mapped[str] = mapped_column(
String(16), nullable=False, default=PrivatePortraitAssetType.IMAGE.value, index=True,
comment="素材类型:Image=图片 / Video=视频",
)
name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
# 资源 URL
source_url: Mapped[str] = mapped_column(Text, nullable=False, comment="本地上传后的访问 URLUploadResource 返回的)")
preview_url: Mapped[str | None] = mapped_column(Text, nullable=True, comment="给前端预览/显示用的 URL(签名 URL 可能过期)")
remote_url: Mapped[str | None] = mapped_column(Text, nullable=True, comment="火山返回的资源访问 URL(可能带签名和过期)")
remote_url_expired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
upload_resource_id: Mapped[str | None] = mapped_column(
String(32), nullable=True, index=True, comment="本地 UploadResource 账本 resource_id(容量释放用)",
)
video_duration: Mapped[float | None] = mapped_column(Float, nullable=True, comment="视频时长,秒")
video_cover_url: Mapped[str | None] = mapped_column(Text, nullable=True, comment="视频封面预览")
file_size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="素材文件大小,字节")
mime_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
status: Mapped[str] = mapped_column(
String(32), nullable=False,
default=PrivatePortraitAssetStatus.CREATING.value,
server_default=PrivatePortraitAssetStatus.CREATING.value,
index=True,
comment="素材状态:creating/审核中 active/可用 failed/失败 deleting/删除中",
)
moderation_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="火山审核结果 JSON")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment="失败原因")
raw_response_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="火山原始响应 JSON")
# 轮询控制(异步审核)
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
next_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
poll_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
# 远端删除
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)
@@ -0,0 +1,86 @@
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 KeyV3 调用方)",
)
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="火山原始响应")