feat(approval): add safe risk disposition workflow
This commit is contained in:
174
server/src/app/models/risk_disposition.py
Normal file
174
server/src/app/models/risk_disposition.py
Normal file
@@ -0,0 +1,174 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user