Files
X-Financial/server/src/app/models/risk_observation.py
caoxiaozhu 7989f3a159 feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
2026-05-30 15:46:51 +08:00

171 lines
7.1 KiB
Python

from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from app.db.base_class import Base
class RiskObservation(Base):
__tablename__ = "risk_observations"
__table_args__ = (
Index("ix_risk_observations_subject", "subject_type", "subject_key"),
Index("ix_risk_observations_signal_level", "risk_signal", "risk_level"),
Index("ix_risk_observations_status_created", "status", "created_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
observation_key: Mapped[str] = mapped_column(String(160), unique=True, index=True)
subject_type: Mapped[str] = mapped_column(String(50), index=True)
subject_key: Mapped[str] = mapped_column(String(160), index=True)
subject_label: Mapped[str] = mapped_column(String(160), default="")
claim_id: Mapped[str | None] = mapped_column(
ForeignKey("expense_claims.id"),
nullable=True,
index=True,
)
claim_no: Mapped[str] = mapped_column(String(80), default="", index=True)
run_id: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
execution_log_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
risk_type: Mapped[str] = mapped_column(String(80), index=True)
risk_signal: Mapped[str] = mapped_column(String(100), index=True)
title: Mapped[str] = mapped_column(String(200), default="")
description: Mapped[str] = mapped_column(Text(), default="")
risk_score: Mapped[int] = mapped_column(Integer, default=0, index=True)
risk_level: Mapped[str] = mapped_column(String(20), index=True)
confidence_score: Mapped[float] = mapped_column(Float, default=0.0)
control_stage: Mapped[str] = mapped_column(String(50), default="")
control_mode: Mapped[str] = mapped_column(String(50), default="")
automation_mode: Mapped[str] = mapped_column(String(50), default="")
source: Mapped[str] = mapped_column(String(60), default="", index=True)
algorithm_version: Mapped[str] = mapped_column(String(80), default="", index=True)
status: Mapped[str] = mapped_column(String(30), default="pending_review", index=True)
feedback_status: Mapped[str] = mapped_column(String(30), default="unreviewed", index=True)
contribution_scores_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
baseline_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
evidence_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
graph_node_keys_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
graph_edge_keys_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
policy_refs_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
similar_case_claim_ids_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
ontology_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
decision_trace_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
)
claim = relationship("ExpenseClaim", foreign_keys=[claim_id])
feedback_items = relationship(
"RiskObservationFeedback",
back_populates="observation",
cascade="all, delete-orphan",
order_by="desc(RiskObservationFeedback.created_at)",
)
@property
def sampling_strategy(self) -> dict[str, Any]:
value = (self.decision_trace_json or {}).get("sampling_strategy")
return dict(value) if isinstance(value, dict) else {}
@property
def evaluation_case_id(self) -> str:
return _json_text((self.decision_trace_json or {}).get("evaluation_case_id"))
@property
def ontology_parse_id(self) -> str:
return _json_text((self.ontology_json or {}).get("ontology_parse_id"))
@property
def ontology_version(self) -> str:
return _json_text((self.ontology_json or {}).get("ontology_version"))
@property
def domain(self) -> str:
return _json_text((self.ontology_json or {}).get("domain"))
@property
def scenario(self) -> str:
return _json_text((self.ontology_json or {}).get("scenario"))
@property
def intent(self) -> str:
return _json_text((self.ontology_json or {}).get("intent"))
@property
def ontology_entities_json(self) -> list[Any]:
value = (self.ontology_json or {}).get("ontology_entities_json")
if value is None:
value = (self.ontology_json or {}).get("entities")
return list(value) if isinstance(value, list) else []
@property
def risk_signals_json(self) -> list[Any]:
value = (self.ontology_json or {}).get("risk_signals_json")
if value is None:
value = (self.ontology_json or {}).get("risk_signals")
return list(value) if isinstance(value, list) else []
@property
def canonical_subject_key(self) -> str:
return _json_text((self.ontology_json or {}).get("canonical_subject_key"))
class RiskObservationFeedback(Base):
__tablename__ = "risk_observation_feedback"
__table_args__ = (
Index("ix_risk_observation_feedback_type_created", "feedback_type", "created_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
observation_id: Mapped[str] = mapped_column(
ForeignKey("risk_observations.id"),
index=True,
)
feedback_type: Mapped[str] = mapped_column(String(30), index=True)
action: Mapped[str] = mapped_column(String(50), default="")
actor: Mapped[str] = mapped_column(String(100), default="")
comment: Mapped[str | None] = mapped_column(Text(), nullable=True)
payload_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
observation = relationship("RiskObservation", back_populates="feedback_items")
@property
def decision(self) -> str:
return _json_text((self.payload_json or {}).get("decision")) or self.feedback_type
@property
def candidate_rule_source(self) -> str:
return _json_text((self.payload_json or {}).get("candidate_rule_source"))
@property
def confidence_score(self) -> float:
try:
return float((self.payload_json or {}).get("confidence_score") or 0)
except (TypeError, ValueError):
return 0.0
@property
def escalation_target(self) -> str:
return _json_text((self.payload_json or {}).get("escalation_target"))
@property
def supplement_required(self) -> bool:
return bool((self.payload_json or {}).get("supplement_required"))
def _json_text(value: Any) -> str:
return str(value or "").strip()