Files
InternalAuditInterprise/backend/app/clues/models.py
T
2026-06-16 00:38:57 +08:00

137 lines
5.3 KiB
Python

"""线索 ORM 模型。
对应需求 R7(线索+证据链+解释)、R17(闭环状态)、R18(置信度分级)、R19(线索不可删)。
"""
from __future__ import annotations
import datetime as dt
import enum
import uuid
from sqlalchemy import DateTime, Enum, Float, ForeignKey, Index, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _enum_values(enum_cls):
"""让 SQLAlchemy 使用枚举的 value(小写)写入 PG 原生 enum,而非 name。"""
return [m.value for m in enum_cls]
def _uuid() -> uuid.UUID:
return uuid.uuid4()
def _now() -> dt.datetime:
return dt.datetime.now(dt.UTC)
class ConfidenceTier(str, enum.Enum):
"""置信度三级分流(R18)。"""
HIGH = "high" # 高置信:直接推送处置
MEDIUM = "medium" # 中置信:人工复核
LOW = "low" # 低置信:归档备查
class ClueStatus(str, enum.Enum):
"""线索闭环状态机(R17)。"""
NEW = "new" # 新生成
ASSIGNED = "assigned" # 已分派
REVIEWING = "reviewing" # 研判中
CONFIRMED = "confirmed" # 已定性属实
DISMISSED = "dismissed" # 已定性误报
RECTIFYING = "rectifying" # 整改中
TRANSFERRED = "transferred" # 已移交
CLOSED = "closed" # 已销项闭环
class Clue(Base):
"""审计线索。线索一经生成不可物理删除(R19),失效通过状态表达。"""
__tablename__ = "clue"
__table_args__ = (
Index("ix_clue_status", "status"),
Index("ix_clue_scenario", "scenario_code"),
Index("ix_clue_assignee", "assignee"),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
title: Mapped[str] = mapped_column(String(256), nullable=False)
risk_domain: Mapped[str] = mapped_column(String(32), nullable=False) # 收入/成本/采购/资金/合规
scenario_code: Mapped[str] = mapped_column(String(32), nullable=False) # 如 R8/R9
confidence: Mapped[ConfidenceTier] = mapped_column(
Enum(ConfidenceTier, name="confidence_tier", values_callable=_enum_values),
nullable=False,
)
score: Mapped[float] = mapped_column(Float, default=0.0) # 0-1 风险评分
status: Mapped[ClueStatus] = mapped_column(
Enum(ClueStatus, name="clue_status", values_callable=_enum_values),
default=ClueStatus.NEW,
nullable=False,
)
# 人话解释(判定理由)与证据链
rationale: Mapped[str] = mapped_column(Text, default="")
evidence: Mapped[dict] = mapped_column(JSONB, default=dict)
# 涉及的主体(金额、实体 id 列表等)
subjects: Mapped[dict] = mapped_column(JSONB, default=dict)
amount_involved: Mapped[float | None] = mapped_column(Float, nullable=True)
assignee: Mapped[str | None] = mapped_column(String(64), nullable=True)
# 误报/属实反馈(R18 反馈学习)
feedback: Mapped[str | None] = mapped_column(String(16), nullable=True) # confirmed/false_positive
# 可追溯:产生该线索时的模型/规则/数据版本(R19 三重留痕)
model_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
rule_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
data_version_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
updated_at: Mapped[dt.datetime] = mapped_column(
DateTime(timezone=True), default=_now, onupdate=_now
)
history: Mapped[list[ClueStatusHistory]] = relationship(
back_populates="clue", cascade="all, delete-orphan"
)
class ClueStatusHistory(Base):
"""线索状态流转留痕(R17/R19)。"""
__tablename__ = "clue_status_history"
__table_args__ = (Index("ix_csh_clue", "clue_id"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
clue_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("clue.id"), nullable=False
)
from_status: Mapped[str | None] = mapped_column(String(16), nullable=True)
to_status: Mapped[str] = mapped_column(String(16), nullable=False)
actor: Mapped[str] = mapped_column(String(64), nullable=False)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)
clue: Mapped[Clue] = relationship(back_populates="history")
class WorkingPaper(Base):
"""审计底稿(R17):研判完成自动生成,可追溯。"""
__tablename__ = "working_paper"
__table_args__ = (Index("ix_wp_clue", "clue_id"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=_uuid)
clue_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("clue.id"), nullable=False
)
content: Mapped[str] = mapped_column(Text, default="")
conclusion: Mapped[str | None] = mapped_column(String(32), nullable=True)
author: Mapped[str] = mapped_column(String(64), nullable=False)
snapshot: Mapped[dict] = mapped_column(JSONB, default=dict) # 证据/版本快照
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=_now)