from __future__ import annotations from datetime import datetime from typing import Any from sqlalchemy import ( Boolean, CheckConstraint, DateTime, Float, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.db.base_class import Base class AttachmentAssociationJob(Base): """可跨进程恢复的票据归集任务状态。""" __tablename__ = "attachment_association_jobs" __table_args__ = ( UniqueConstraint( "tenant_id", "owner_username", "dedupe_key", "generation", name="uq_attachment_association_jobs_owner_dedupe", ), CheckConstraint( "status IN ('queued', 'running', 'succeeded', 'failed')", name="ck_attachment_association_jobs_status", ), CheckConstraint( "attempt_count >= 0", name="ck_attachment_association_jobs_attempt_count", ), CheckConstraint( "generation >= 1", name="ck_attachment_association_jobs_generation", ), CheckConstraint( "status != 'running' OR lease_expires_at IS NOT NULL", name="ck_attachment_association_jobs_running_lease", ), Index( "ix_attachment_association_jobs_owner_time", "tenant_id", "owner_username", "created_at", ), Index( "ix_attachment_association_jobs_status_lease", "status", "lease_expires_at", ), ) id: Mapped[str] = mapped_column(String(80), primary_key=True) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) owner_username: Mapped[str] = mapped_column(String(255), nullable=False) owner_name: Mapped[str] = mapped_column(String(255), nullable=False, default="") owner_context_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) dedupe_key: Mapped[str] = mapped_column(String(64), nullable=False) generation: Mapped[int] = mapped_column(Integer, nullable=False, default=1) receipt_ids_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) prompt: Mapped[str] = mapped_column(Text, nullable=False, default="") conversation_id: Mapped[str] = mapped_column(String(120), nullable=False, default="") status: Mapped[str] = mapped_column(String(20), nullable=False, default="queued") message: Mapped[str] = mapped_column(Text, nullable=False, default="") claim_id: Mapped[str] = mapped_column(String(36), nullable=False, default="") claim_no: Mapped[str] = mapped_column(String(80), nullable=False, default="") uploaded_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) skipped_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) resolution: Mapped[str] = mapped_column(String(40), nullable=False, default="pending") requires_confirmation: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) expense_case_id: Mapped[str] = mapped_column(String(36), nullable=False, default="") application_claim_id: Mapped[str] = mapped_column(String(36), nullable=False, default="") application_claim_no: Mapped[str] = mapped_column(String(80), nullable=False, default="") confidence: Mapped[str] = mapped_column(String(20), nullable=False, default="") confidence_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) match_reasons_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) exceptions_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) missing_fields_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) risk_items_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) candidates_json: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False, default=list) draft_payload_json: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) error: Mapped[str] = mapped_column(Text, nullable=False, default="") attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) lease_expires_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, )