from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import ( CheckConstraint, DateTime, ForeignKeyConstraint, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import JSON from app.db.base_class import Base def _new_id() -> str: return str(uuid.uuid4()) class RiskDisposition(Base): """风险观察的当前处置投影;裁决结论和处置生命周期彼此独立。""" __tablename__ = "risk_dispositions" __table_args__ = ( UniqueConstraint( "tenant_id", "id", name="uq_risk_dispositions_tenant_id", ), UniqueConstraint( "tenant_id", "observation_id", name="uq_risk_dispositions_tenant_observation", ), CheckConstraint( "adjudication IN ('unreviewed', 'confirmed', 'false_positive')", name="ck_risk_dispositions_adjudication", ), CheckConstraint( "lifecycle_status IN ('open', 'supplement_requested', " "'remediation_in_progress', 'waiver_requested', 'resolved')", name="ck_risk_dispositions_lifecycle", ), CheckConstraint("version >= 0", name="ck_risk_dispositions_version"), ForeignKeyConstraint( ["tenant_id", "observation_id"], ["risk_observations.tenant_id", "risk_observations.id"], ondelete="RESTRICT", name="fk_risk_dispositions_tenant_observation", ), Index( "ix_risk_dispositions_tenant_lifecycle_due", "tenant_id", "lifecycle_status", "due_at", ), Index("ix_risk_dispositions_assignee", "tenant_id", "assignee"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) observation_id: Mapped[str] = mapped_column(String(36), nullable=False) adjudication: Mapped[str] = mapped_column( String(24), nullable=False, default="unreviewed", server_default="unreviewed", ) lifecycle_status: Mapped[str] = mapped_column( String(32), nullable=False, default="open", server_default="open", ) version: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default="0", ) assignee: Mapped[str | None] = mapped_column(String(120), nullable=True) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) resolution: Mapped[str | None] = mapped_column(Text(), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) observation = relationship("RiskObservation", back_populates="disposition") events = relationship( "RiskDispositionEvent", back_populates="disposition", order_by="asc(RiskDispositionEvent.version)", lazy="selectin", passive_deletes=True, ) class RiskDispositionEvent(Base): """只追加的风险处置事实;每个版本只对应一个类型化动作。""" __tablename__ = "risk_disposition_events" __table_args__ = ( UniqueConstraint( "tenant_id", "request_id", name="uq_risk_disposition_events_tenant_request", ), UniqueConstraint( "disposition_id", "version", name="uq_risk_disposition_events_version", ), CheckConstraint( "action IN ('confirm', 'false_positive', 'request_supplement', " "'start_remediation', 'resolve', 'request_waiver')", name="ck_risk_disposition_events_action", ), CheckConstraint("version > 0", name="ck_risk_disposition_events_version"), ForeignKeyConstraint( ["tenant_id", "disposition_id"], ["risk_dispositions.tenant_id", "risk_dispositions.id"], ondelete="RESTRICT", name="fk_risk_disposition_events_tenant_disposition", ), ForeignKeyConstraint( ["tenant_id", "observation_id"], ["risk_observations.tenant_id", "risk_observations.id"], ondelete="RESTRICT", name="fk_risk_disposition_events_tenant_observation", ), Index( "ix_risk_disposition_events_tenant_observation_time", "tenant_id", "observation_id", "created_at", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) disposition_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) observation_id: Mapped[str] = mapped_column(String(36), nullable=False) version: Mapped[int] = mapped_column(Integer, nullable=False) action: Mapped[str] = mapped_column(String(32), nullable=False) actor_id: Mapped[str] = mapped_column(String(120), nullable=False) actor_name: Mapped[str] = mapped_column(String(120), nullable=False) request_id: Mapped[str] = mapped_column(String(120), nullable=False) payload_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False) comment: Mapped[str | None] = mapped_column(Text(), nullable=True) before_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) after_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) disposition = relationship("RiskDisposition", back_populates="events")