This commit is contained in:
2026-07-20 14:01:22 +08:00
parent 53e01f6f74
commit a2e98ef3a3
75 changed files with 5912 additions and 2666 deletions
@@ -91,6 +91,8 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
)
generation_count: Mapped[int] = mapped_column(Integer, default=1, server_default="1", nullable=False)
generation_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
generation_attempt_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1")
resource_generation_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
# 图片主任务同步调用供应商时的分布式执行租约。
# 防止重复 Celery 消息或恢复任务同时触发多次组图请求。
@@ -118,7 +120,10 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
video_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
image_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
# retry_count is retained as a compatibility mirror of manual retries.
retry_count: Mapped[int] = mapped_column(Integer, default=0)
manual_retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
poll_error_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
poll_count: Mapped[int] = mapped_column(Integer, default=0)
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# 视频降频轮询调度字段。
@@ -126,6 +131,8 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
poll_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
next_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_interval_seconds: Mapped[int] = mapped_column(Integer, default=0)
poll_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
poll_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
deadline_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
generated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -135,6 +142,7 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
download_celery_task_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=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, index=True)
download_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
download_next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
download_attempt_count: Mapped[int] = mapped_column(Integer, default=0)
@@ -1,4 +1,4 @@
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
@@ -6,20 +6,31 @@ from app.utils.id_gen import generate_id
class ChatGenerationTaskEvent(Base):
"""Append-only event log for project-independent chat generation tasks."""
"""Append-only event log shared by ChatGenerationTask and GenerationRecord."""
__tablename__ = "chat_generation_task_events"
__table_args__ = (
CheckConstraint(
"(owner_type = 'chat_generation_task' AND task_id IS NOT NULL AND generation_record_id IS NULL) "
"OR (owner_type = 'generation_record' AND task_id IS NULL AND generation_record_id IS NOT NULL)",
name="ck_chat_generation_task_events_owner",
),
Index("idx_chat_generation_task_events_task_created", "owner_type", "task_id", "created_at"),
Index("idx_chat_generation_task_events_record_created", "owner_type", "generation_record_id", "created_at"),
Index("idx_chat_generation_task_events_attempt_created", "owner_type", "generation_attempt_no", "created_at"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=generate_id)
task_id: Mapped[str] = mapped_column(
String(32), ForeignKey("chat_generation_tasks.id", ondelete="CASCADE"), index=True
)
owner_type: Mapped[str] = mapped_column(String(32), nullable=False, default="chat_generation_task", server_default="chat_generation_task", index=True)
task_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("chat_generation_tasks.id", ondelete="CASCADE"), nullable=True, index=True)
generation_record_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("generation_records.id", ondelete="CASCADE"), nullable=True, index=True)
generation_attempt_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1", index=True)
generation_mode: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
event_type: Mapped[str] = mapped_column(String(64), index=True)
from_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
to_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
from_stage: Mapped[str | None] = mapped_column(String(32), nullable=True)
to_stage: Mapped[str | None] = mapped_column(String(32), nullable=True)
message: Mapped[str | None] = mapped_column(String(512), nullable=True)
from_stage: Mapped[str | None] = mapped_column(String(48), nullable=True)
to_stage: Mapped[str | None] = mapped_column(String(48), nullable=True)
message: Mapped[str | None] = mapped_column(Text, nullable=True)
detail_json: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
@@ -1,4 +1,4 @@
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
@@ -6,14 +6,25 @@ from app.utils.id_gen import generate_id
class ChatProviderCallLog(Base):
"""Provider call audit log for chat_generation_tasks."""
"""Provider call audit log shared by both generation owner models."""
__tablename__ = "chat_provider_call_logs"
__table_args__ = (
CheckConstraint(
"(owner_type = 'chat_generation_task' AND task_id IS NOT NULL AND generation_record_id IS NULL) "
"OR (owner_type = 'generation_record' AND task_id IS NULL AND generation_record_id IS NOT NULL)",
name="ck_chat_provider_call_logs_owner",
),
Index("idx_chat_provider_call_logs_task_created", "owner_type", "task_id", "created_at"),
Index("idx_chat_provider_call_logs_record_created", "owner_type", "generation_record_id", "created_at"),
Index("idx_chat_provider_call_logs_attempt_created", "owner_type", "generation_attempt_no", "created_at"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=generate_id)
task_id: Mapped[str] = mapped_column(
String(32), ForeignKey("chat_generation_tasks.id", ondelete="CASCADE"), index=True
)
owner_type: Mapped[str] = mapped_column(String(32), nullable=False, default="chat_generation_task", server_default="chat_generation_task", index=True)
task_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("chat_generation_tasks.id", ondelete="CASCADE"), nullable=True, index=True)
generation_record_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("generation_records.id", ondelete="CASCADE"), nullable=True, index=True)
generation_attempt_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1", index=True)
generation_mode: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
provider: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
api_type: Mapped[str] = mapped_column(String(64), index=True)
@@ -1,6 +1,6 @@
from datetime import date, datetime
from sqlalchemy import BigInteger, Date, DateTime, ForeignKey, Index, String, Text
from sqlalchemy import BigInteger, Date, DateTime, ForeignKey, Index, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
@@ -44,4 +44,12 @@ class GeneratedResource(Base, TimestampMixin, SoftDeleteMixin):
Index("ix_generated_resources_user_month", GeneratedResource.user_id, GeneratedResource.resource_month)
Index("ix_generated_resources_source", GeneratedResource.source_model, GeneratedResource.source_id)
Index(
"uq_generated_resources_active_source_type",
GeneratedResource.source_model,
GeneratedResource.source_id,
GeneratedResource.resource_type,
unique=True,
postgresql_where=text("deleted_at IS NULL"),
)
Index("ix_generated_resources_active_user", GeneratedResource.user_id, GeneratedResource.deleted_at)
+43 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, Float, Index
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, Float, Index, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
@@ -52,7 +52,49 @@ class GenerationRecord(Base, TimestampMixin, SoftDeleteMixin):
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
idempotency_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
# Unified Celery generation pipeline state.
generation_attempt_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1")
resource_generation_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
deadline_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
engine_snapshot_json: Mapped[str | None] = mapped_column(Text, nullable=True)
provider_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
remote_result_url: Mapped[str | None] = mapped_column(Text, nullable=True)
provider_create_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
provider_create_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
provider_create_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# retry_count is retained for API/backward compatibility. New business
# logic uses manual_retry_count and poll_error_count separately.
retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
manual_retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
poll_error_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
poll_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
next_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
poll_interval_seconds: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
poll_claim_token: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
poll_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
download_celery_task_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=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, index=True)
download_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
download_next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
download_attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, 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)
__table_args__ = (
Index('idx_genrec_user_status_created', 'user_id', 'status', 'created_at'),
Index('idx_genrec_project_status', 'project_id', 'status'),
Index(
'idx_genrec_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"),
),
)
+2 -2
View File
@@ -1,10 +1,10 @@
from sqlalchemy import Boolean, CheckConstraint, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
class ImageEngine(Base, TimestampMixin):
class ImageEngine(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "image_engines"
__table_args__ = (
CheckConstraint("max_generation_count BETWEEN 1 AND 5", name="ck_image_engines_max_generation_count"),
+2 -2
View File
@@ -1,10 +1,10 @@
from sqlalchemy import Boolean, Float, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
class ModelConfig(Base, TimestampMixin):
class ModelConfig(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "model_configs"
id: Mapped[str] = mapped_column(String(32), primary_key=True)
+2 -2
View File
@@ -1,10 +1,10 @@
from sqlalchemy import Boolean, CheckConstraint, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
class VideoEngine(Base, TimestampMixin):
class VideoEngine(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "video_engines"
__table_args__ = (
CheckConstraint("max_generation_count BETWEEN 1 AND 5", name="ck_video_engines_max_generation_count"),