Files
InternalAuditInterprise/backend/app/scenarios/split_contract.py
T
selfrelease 74e35fed96 feat: 添加可配置场景注册表与规则引擎
- 新增场景基类、配置加载、注册表与原语模块
- 添加 r10_refund_split 规则及场景 JSON Schema
- 扩展 scan 引擎与 scenarios API
- 新增场景注册表/配置/集成测试
- 更新前端 App、api、labels 支持新场景
2026-06-17 09:57:02 +08:00

137 lines
4.9 KiB
Python

"""场景一 · 政企收入全链路穿透:拆单规避检测(R8)。
检测点:
1. 合同金额集中分布在审批阈值边缘(如阈值 80% 以上但不超阈值)。
2. 结合知识图谱穿透识别隐性实控人(多个客户经法人关联到同一实控人)。
满足上述模式则生成线索,附证据链与人话理由。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from app.scenarios.base import BaseScenario, ClueDraft, ScanOutcome
from app.scenarios.registry import register_scenario
@dataclass
class ContractRecord:
"""穿透分析输入:一份合同的关键信息。"""
contract_id: str
customer_key: str
amount: float
@dataclass
class SplitFinding:
"""拆单检测结果。"""
near_threshold: list[ContractRecord] = field(default_factory=list)
ratio: float = 0.0
total_amount: float = 0.0
@property
def hit(self) -> bool:
return len(self.near_threshold) >= 3
def detect_threshold_edge(
contracts: list[ContractRecord],
approval_threshold: float,
edge_ratio: float = 0.8,
) -> SplitFinding:
"""识别金额集中在审批阈值边缘 [edge_ratio*阈值, 阈值) 的合同。
这类"刚好低于阈值"的批量合同是典型的拆单规避特征。
"""
if approval_threshold <= 0:
raise ValueError("审批阈值必须为正数")
lower = edge_ratio * approval_threshold
near = [c for c in contracts if lower <= c.amount < approval_threshold]
finding = SplitFinding(
near_threshold=near,
ratio=(len(near) / len(contracts)) if contracts else 0.0,
total_amount=sum(c.amount for c in near),
)
return finding
def split_risk_score(finding: SplitFinding, shared_controller: bool) -> float:
"""综合评分:阈值边缘集中度 + 是否穿透到同一实控人。"""
if not finding.hit:
return 0.0
base = min(0.6, 0.1 * len(finding.near_threshold)) # 数量越多越可疑
base += 0.2 * finding.ratio
if shared_controller:
base += 0.3 # 同一实控人是强证据
return round(min(base, 1.0), 3)
def build_rationale(finding: SplitFinding, threshold: float, shared_controller: bool) -> str:
parts = [
f"检测到 {len(finding.near_threshold)} 份合同金额集中在审批阈值 "
f"{threshold:.0f} 的边缘区间(占比 {finding.ratio:.0%}),",
f"边缘合同金额合计约 {finding.total_amount:.0f}",
]
if shared_controller:
parts.append("且经工商关联穿透,相关客户疑似同属一个隐性实控人,高度符合拆单规避特征。")
else:
parts.append("建议进一步穿透客户关联关系以确认是否同一实控人。")
return "".join(parts)
@register_scenario
class SplitContractScenario(BaseScenario):
"""场景一拆单检测的插件封装:检测→评分→产出线索草稿。
数据通过构造注入(兼容现有 seed/测试的数据流);后续可改为从 session 自取数,
接口保持不变。
"""
code = "R8"
title = "疑似政企拆单规避审批"
risk_domain = "收入"
label = "政企拆单"
score_threshold = 0.0 # 命中即出线索(与原 run_split_contract_scan 的 score>0 一致)
def __init__(
self,
contracts: list[ContractRecord],
approval_threshold: float,
shared_controller: bool = False,
) -> None:
self.contracts = contracts
self.approval_threshold = approval_threshold
self.shared_controller = shared_controller
def scan(self, session, *, data_version_id=None) -> ScanOutcome:
finding = detect_threshold_edge(self.contracts, self.approval_threshold)
score = split_risk_score(finding, self.shared_controller)
drafts: list[ClueDraft] = []
if score > 0:
drafts.append(
ClueDraft(
score=score,
rationale=build_rationale(
finding, self.approval_threshold, self.shared_controller
),
evidence={
"near_threshold_contracts": [
c.contract_id for c in finding.near_threshold
],
"edge_ratio": finding.ratio,
"near_threshold_amount": finding.total_amount,
"approval_threshold": self.approval_threshold,
"shared_controller": self.shared_controller,
},
subjects={
"customers": sorted(
{c.customer_key for c in finding.near_threshold}
)
},
amount_involved=finding.total_amount,
)
)
return ScanOutcome(scanned_count=len(self.contracts), drafts=drafts)