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
+15
View File
@@ -0,0 +1,15 @@
from app.models.api.api_key import ApiKey
from app.models.api.api_generation_task import ApiGenerationTask
from app.models.api.api_usage_log import ApiUsageLog
from app.models.api.api_key_upscale_config import ApiKeyUpscaleConfig
from app.models.api.api_upscale_link import ApiUpscaleLink
from app.models.api.api_model_pricing import ApiModelPricing
__all__ = [
"ApiKey",
"ApiGenerationTask",
"ApiUsageLog",
"ApiKeyUpscaleConfig",
"ApiUpscaleLink",
"ApiModelPricing",
]
@@ -0,0 +1,125 @@
from datetime import datetime
from sqlalchemy import Boolean, CheckConstraint, DateTime, Float, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
class ApiGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
"""对外开放 API 的生成任务表。
该表设计满足 ProviderGenerationRecordLike 协议,
使现有的 Volcano Ark SDK 封装函数可以直接复用。
"""
__tablename__ = "api_generation_tasks"
__table_args__ = (
# 幂等键唯一索引
Index(
"uq_api_generation_tasks_key_idempotency",
"api_key_id",
"external_idempotency_key",
unique=True,
postgresql_where=text("deleted_at IS NULL AND external_idempotency_key IS NOT NULL"),
),
# 视频轮询调度索引
Index(
"idx_api_generation_tasks_next_poll_at",
"next_poll_at",
postgresql_where=text(
"deleted_at IS NULL "
"AND status = 'generating' "
"AND gen_type = 'video' "
"AND next_poll_at IS NOT NULL"
),
),
Index("idx_api_generation_tasks_api_key_created", "api_key_id", "created_at"),
Index("idx_api_generation_tasks_provider_task_id", "provider_task_id"),
Index("idx_api_generation_tasks_status", "status"),
CheckConstraint("generation_count BETWEEN 1 AND 5", name="ck_api_generation_tasks_generation_count"),
)
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
)
external_idempotency_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
# === ProviderGenerationRecordLike 协议字段 ===
original_prompt: Mapped[str] = mapped_column(Text, nullable=False)
optimized_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
gen_type: Mapped[str] = mapped_column(String(16), default="video", nullable=False)
duration: Mapped[int | None] = mapped_column(Integer, nullable=True)
aspect_ratio: Mapped[str | None] = mapped_column(String(8), nullable=True)
resolution: Mapped[str | None] = mapped_column(String(8), nullable=True)
provider_generation_resolution: Mapped[str | None] = mapped_column(String(16), nullable=True)
image_size: Mapped[str | None] = mapped_column(String(16), nullable=True)
image_proportion: Mapped[str | None] = mapped_column(String(8), nullable=True)
image_px: Mapped[str | None] = mapped_column(String(16), nullable=True)
generation_count: Mapped[int] = mapped_column(Integer, default=1, server_default="1", nullable=False)
engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
model_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="模型名称")
media_references: Mapped[str | None] = mapped_column(Text, nullable=True, comment="用户原始上传的媒体URL")
local_media_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="下载到本地的媒体文件路径JSON")
engine_snapshot_json: Mapped[str | None] = mapped_column(Text, nullable=True)
# === 请求参数快照 ===
request_params_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="完整原始请求参数")
# === 流水线状态(镜像 ChatGenerationTask ===
status: Mapped[str] = mapped_column(String(32), default="pending", nullable=False)
pipeline_stage: Mapped[str | None] = mapped_column(String(32), nullable=True)
generation_attempt_no: Mapped[int] = mapped_column(Integer, default=1, server_default="1", nullable=False)
resource_generation_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
deadline_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# === 供应商交互 ===
provider_task_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
remote_result_url: Mapped[str | None] = mapped_column(Text, nullable=True)
provider_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
# === 结果 ===
image_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
video_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
video_cover_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
generated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# === 超分 ===
video_upscale_enabled_snapshot: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, server_default="false"
)
video_upscale_snapshot_json: Mapped[str | None] = mapped_column(Text, nullable=True)
# === 配额消耗 ===
credits_cost: Mapped[float] = mapped_column(Float, default=0.0, server_default="0.0")
video_tokens_used: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
image_tokens_used: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
# === 轮询控制 ===
next_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_interval_seconds: Mapped[int] = mapped_column(Integer, default=30, server_default="30")
poll_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# === Celery 执行租约(镜像 ChatGenerationTask ===
provider_create_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True)
provider_create_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
provider_create_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True)
poll_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_error_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
download_celery_task_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
download_enqueued_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
download_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
download_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True)
download_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
download_next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
download_attempt_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
download_last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
download_storage_date_dir: Mapped[str | None] = mapped_column(String(16), nullable=True)
# === 存储 ===
local_path: Mapped[str | None] = mapped_column(Text, nullable=True)
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Float, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
from app.utils.security import encrypt_text, decrypt_text
class ApiKey(Base, TimestampMixin, SoftDeleteMixin):
"""对外开放 API 的密钥管理表。
每个 api-key 对应一个外部调用方(公司/组织),
可配置可调用模型、配额、有效期、并发限制。
"""
__tablename__ = "api_keys"
__table_args__ = (
Index("idx_api_keys_active", "is_active", postgresql_where=text("deleted_at IS NULL")),
Index("idx_api_keys_company", "company_name"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
company_name: Mapped[str] = mapped_column(String(128), nullable=False)
api_key_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
api_key_prefix: Mapped[str] = mapped_column(String(16), nullable=False)
api_key_encrypted: Mapped[str] = mapped_column(Text, nullable=False, comment="AES-256-GCM 加密的完整 API Key")
description: Mapped[str | None] = mapped_column(Text, nullable=True)
def decrypt_api_key(self) -> str | None:
"""解密并返回完整 API Key。"""
return decrypt_text(self.api_key_encrypted)
def set_plaintext_key(self, plaintext: str) -> None:
"""设置明文 API Key(自动加密存储)。"""
self.api_key_encrypted = encrypt_text(plaintext)
# === 可调用模型配置 ===
callable_models: Mapped[str] = mapped_column(Text, nullable=False, server_default="[]",
comment='JSON数组: [{"engine_type":"video","engine_id":"xxx","model_name":"doubao-seedance-2-0-260128"}]')
# === 配额配置(不设置=无限制) ===
quota_limit: Mapped[float | None] = mapped_column(Float, nullable=True, comment="配额总量,NULL=无限")
quota_cycle: Mapped[str | None] = mapped_column(String(16), nullable=True, comment="daily|monthly|one_time|NULL=无限")
quota_used: Mapped[float] = mapped_column(Float, nullable=False, default=0.0, server_default="0.0", comment="当前周期已使用量")
# === 有效期(不设置=永不过期) ===
valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# === 并发限制(不设置=无限制) ===
max_concurrent_video_tasks: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="最大并发视频任务数,NULL=无限")
# === 状态 ===
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -0,0 +1,27 @@
from sqlalchemy import Boolean, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
class ApiKeyUpscaleConfig(Base, TimestampMixin):
"""API Key 级别的超分配置表。
每个 API Key 可独立配置超分规则,不依赖现有的 video_upscale 配置。
"""
__tablename__ = "api_key_upscale_configs"
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"), unique=True, nullable=False
)
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
delete_source_after_success: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=True, server_default="true"
)
rules_json: Mapped[str] = mapped_column(
Text, nullable=False, server_default="[]",
comment='JSON数组: [{"target_resolution":"1080p","provider_generation_resolution":"720p","processor_key":"volc_standard_v1","enabled":true}]'
)
@@ -0,0 +1,40 @@
from sqlalchemy import Float, Index, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
class ApiModelPricing(Base, TimestampMixin):
"""API 模型价格表(全局统一配置)。
完全镜像 credit_ratios 表结构,将积分字段替换为金额字段(元)。
所有 API Key 共用一套价格表。
model_config_id 兼容 credit_ratios 字段名约定:
- gen_type=image 时,该字段保存 image_engines.id
- gen_type=video 时,该字段保存 video_engines.id
"""
__tablename__ = "api_model_pricings"
__table_args__ = (
Index("ix_api_model_pricings_gen_type_engine_resolution", "gen_type", "model_config_id", "resolution"),
Index("ix_api_model_pricings_gen_type_resolution", "gen_type", "resolution"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
model_config_id: Mapped[str] = mapped_column(String(32), index=True)
gen_type: Mapped[str] = mapped_column(String(16), default="video", index=True)
resolution: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
# === 价格字段(元) ===
price_ratio: Mapped[float] = mapped_column(Float, nullable=False, default=1.0, comment="价格系数(乘数)")
base_price: Mapped[float] = mapped_column(Float, default=0.0, comment="基础价格(元)")
per_second_price: Mapped[float] = mapped_column(Float, default=0.0, comment="每秒价格(视频,元)")
# === 传入媒体附加费 ===
input_video_ratio: Mapped[float] = mapped_column(Float, default=1.0, comment="传入视频系数")
input_video_base_price: Mapped[float] = mapped_column(Float, default=0.0, comment="传入视频基础价(元)")
input_video_per_second_price: Mapped[float] = mapped_column(Float, default=0.0, comment="传入视频每秒价(元)")
input_image_ratio: Mapped[float] = mapped_column(Float, default=1.0, comment="传入图片系数")
input_image_base_price: Mapped[float] = mapped_column(Float, default=0.0, comment="传入图片基础价(元)")
input_image_per_image_price: Mapped[float] = mapped_column(Float, default=0.0, comment="传入图片每张价(元)")
@@ -0,0 +1,22 @@
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
class ApiUpscaleLink(Base, TimestampMixin):
"""API 任务与超分任务的关联表。
由于不能修改现有的 video_upscale_tasks 表结构,
通过此关联表追踪 API 任务对应的超分子任务。
"""
__tablename__ = "api_upscale_links"
id: Mapped[str] = mapped_column(String(32), primary_key=True)
api_generation_task_id: Mapped[str] = mapped_column(
String(32), ForeignKey("api_generation_tasks.id", ondelete="CASCADE"), nullable=False, index=True
)
video_upscale_task_id: Mapped[str] = mapped_column(
String(32), ForeignKey("video_upscale_tasks.id", ondelete="CASCADE"), nullable=False, index=True
)
@@ -0,0 +1,61 @@
from sqlalchemy import Float, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
class ApiUsageLog(Base, TimestampMixin):
"""API 调用详细消耗记录表。
记录每次 API 请求的完整消费信息,包括:
- 扣除金额和退回金额
- 模型详情(名称、分辨率、时长等)
- 对应的生成任务 ID
- 操作类型(扣除/退回)
"""
__tablename__ = "api_usage_logs"
__table_args__ = (
Index("idx_api_usage_logs_api_key_created", "api_key_id", "created_at"),
Index("idx_api_usage_logs_task_id", "api_generation_task_id"),
Index("idx_api_usage_logs_action", "price_action"),
)
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
)
api_generation_task_id: Mapped[str | None] = mapped_column(
String(32), ForeignKey("api_generation_tasks.id", ondelete="SET NULL"), nullable=True
)
# === 操作类型 ===
price_action: Mapped[str] = mapped_column(String(16), nullable=False, comment="deduct=扣除, refund=退回")
# === 请求信息 ===
request_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="video_create|image_generate")
model_name: Mapped[str] = mapped_column(String(128), nullable=False)
gen_type: Mapped[str] = mapped_column(String(16), nullable=False)
resolution: Mapped[str | None] = mapped_column(String(16), nullable=True)
duration: Mapped[int | None] = mapped_column(Integer, nullable=True)
# === 金额信息 ===
credits_cost: Mapped[float] = mapped_column(Float, default=0.0, server_default="0.0", comment="实际扣除金额")
refund_amount: Mapped[float] = mapped_column(Float, default=0.0, server_default="0.0", comment="退回金额")
quota_before: Mapped[float | None] = mapped_column(Float, nullable=True, comment="操作前配额余额")
quota_after: Mapped[float | None] = mapped_column(Float, nullable=True, comment="操作后配额余额")
# === Token 用量 ===
tokens_used: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
request_duration_ms: Mapped[int] = mapped_column(Integer, default=0, server_default="0", comment="端到端耗时")
# === 价格明细(JSON ===
price_detail_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="价格计算明细JSON")
# === 结果 ===
status: Mapped[str] = mapped_column(String(32), nullable=False, comment="success|failed")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
# === 调试 ===
request_payload_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="原始请求快照")