Initial commit: InternalAuditInterprise

This commit is contained in:
freedakgmail
2026-06-16 00:38:57 +08:00
commit 7b1e2b10a8
57 changed files with 4622 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""线索引擎模块:线索模型、生成、置信度分级、状态流转(人机闭环)。"""
+136
View File
@@ -0,0 +1,136 @@
"""线索 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)
+195
View File
@@ -0,0 +1,195 @@
"""线索服务:生成、置信度分级、状态流转、底稿生成、反馈。
对应 R7 / R17 / R18 / R19。所有状态变更写入历史并记自审计日志(线索不可删)。
"""
from __future__ import annotations
import uuid
from sqlalchemy.orm import Session
from app.audit import service as audit
from app.clues.models import (
Clue,
ClueStatus,
ClueStatusHistory,
ConfidenceTier,
WorkingPaper,
)
# 允许的状态流转(R17 闭环)
_ALLOWED_TRANSITIONS: dict[ClueStatus, set[ClueStatus]] = {
ClueStatus.NEW: {ClueStatus.ASSIGNED, ClueStatus.REVIEWING},
ClueStatus.ASSIGNED: {ClueStatus.REVIEWING},
ClueStatus.REVIEWING: {ClueStatus.CONFIRMED, ClueStatus.DISMISSED},
ClueStatus.CONFIRMED: {ClueStatus.RECTIFYING, ClueStatus.TRANSFERRED},
ClueStatus.DISMISSED: {ClueStatus.CLOSED},
ClueStatus.RECTIFYING: {ClueStatus.CLOSED},
ClueStatus.TRANSFERRED: {ClueStatus.CLOSED},
ClueStatus.CLOSED: set(),
}
class IllegalTransitionError(ValueError):
"""非法的线索状态流转。"""
def score_to_tier(score: float) -> ConfidenceTier:
"""风险评分映射到置信度三级(R18)。"""
if score >= 0.8:
return ConfidenceTier.HIGH
if score >= 0.5:
return ConfidenceTier.MEDIUM
return ConfidenceTier.LOW
def create_clue(
session: Session,
*,
title: str,
risk_domain: str,
scenario_code: str,
score: float,
rationale: str,
evidence: dict,
subjects: dict | None = None,
amount_involved: float | None = None,
model_version: str | None = None,
rule_version: str | None = None,
data_version_id: uuid.UUID | None = None,
actor: str = "system",
) -> Clue:
"""生成一条线索,自动按评分分级,并记录创建留痕。"""
clue = Clue(
title=title,
risk_domain=risk_domain,
scenario_code=scenario_code,
confidence=score_to_tier(score),
score=score,
status=ClueStatus.NEW,
rationale=rationale,
evidence=evidence,
subjects=subjects or {},
amount_involved=amount_involved,
model_version=model_version,
rule_version=rule_version,
data_version_id=data_version_id,
)
session.add(clue)
session.flush()
_add_history(session, clue, None, ClueStatus.NEW, actor, "线索生成")
audit.record(
session, actor, "create_clue",
target_type="clue", target_id=str(clue.id),
detail={"scenario": scenario_code, "score": score, "confidence": clue.confidence.value},
)
return clue
def _add_history(
session: Session,
clue: Clue,
from_status: ClueStatus | None,
to_status: ClueStatus,
actor: str,
note: str | None,
) -> None:
session.add(
ClueStatusHistory(
clue_id=clue.id,
from_status=from_status.value if from_status else None,
to_status=to_status.value,
actor=actor,
note=note,
)
)
session.flush()
def transition(
session: Session, clue: Clue, to_status: ClueStatus, actor: str, note: str | None = None
) -> Clue:
"""执行状态流转,校验合法性并留痕。"""
if to_status not in _ALLOWED_TRANSITIONS.get(clue.status, set()):
raise IllegalTransitionError(
f"线索状态不能从 {clue.status.value} 流转到 {to_status.value}"
)
from_status = clue.status
clue.status = to_status
session.flush()
_add_history(session, clue, from_status, to_status, actor, note)
audit.record(
session, actor, "transition_clue",
target_type="clue", target_id=str(clue.id),
detail={"from": from_status.value, "to": to_status.value, "note": note},
)
return clue
def assign(session: Session, clue: Clue, assignee: str, actor: str) -> Clue:
clue.assignee = assignee
session.flush()
if clue.status == ClueStatus.NEW:
transition(session, clue, ClueStatus.ASSIGNED, actor, f"分派给 {assignee}")
audit.record(session, actor, "assign_clue", target_type="clue", target_id=str(clue.id), detail={"assignee": assignee})
return clue
def adjudicate(
session: Session, clue: Clue, confirmed: bool, actor: str, note: str | None = None
) -> WorkingPaper:
"""研判定性:确认属实或误报,自动生成审计底稿并记录反馈(R17/R18)。"""
if clue.status not in (ClueStatus.ASSIGNED, ClueStatus.REVIEWING, ClueStatus.NEW):
# 允许从 NEW/ASSIGNED 直接进入研判
pass
if clue.status != ClueStatus.REVIEWING:
# 先进入研判中
target = ClueStatus.REVIEWING
if target in _ALLOWED_TRANSITIONS.get(clue.status, set()):
transition(session, clue, ClueStatus.REVIEWING, actor, "进入研判")
to = ClueStatus.CONFIRMED if confirmed else ClueStatus.DISMISSED
transition(session, clue, to, actor, note)
clue.feedback = "confirmed" if confirmed else "false_positive"
session.flush()
paper = WorkingPaper(
clue_id=clue.id,
content=note or "",
conclusion=to.value,
author=actor,
snapshot={
"evidence": clue.evidence,
"rationale": clue.rationale,
"score": clue.score,
"model_version": clue.model_version,
"rule_version": clue.rule_version,
"data_version_id": str(clue.data_version_id) if clue.data_version_id else None,
},
)
session.add(paper)
session.flush()
audit.record(
session, actor, "create_working_paper",
target_type="working_paper", target_id=str(paper.id),
detail={"clue_id": str(clue.id), "conclusion": to.value},
)
return paper
def list_clues(
session: Session,
*,
status: ClueStatus | None = None,
scenario_code: str | None = None,
confidence: ConfidenceTier | None = None,
) -> list[Clue]:
q = session.query(Clue)
if status:
q = q.filter(Clue.status == status)
if scenario_code:
q = q.filter(Clue.scenario_code == scenario_code)
if confidence:
q = q.filter(Clue.confidence == confidence)
return q.order_by(Clue.score.desc()).all()