Files
X-Financial/server/src/app/models/risk_disposition.py
2026-07-16 15:49:43 +08:00

176 lines
6.1 KiB
Python

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)
response_json: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
disposition = relationship("RiskDisposition", back_populates="events")