1、增加调用 AI 视频生成能力和虚拟素材库管理的对外api
2、增加后台apikkey管理 3、增加apikey单独的模型定价 4、增加apikey调用情况 5、完善所有数据的注释增加
This commit is contained in:
@@ -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)
|
||||
@@ -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="原始请求快照")
|
||||
Reference in New Issue
Block a user