over_book_transaction_recognizer.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. from pydantic import BaseModel, Field
  2. from typing import Dict, Any, Optional, Type, List, Tuple
  3. import pandas as pd
  4. from datetime import timedelta
  5. from .enhanced_base_recognizer import EnhancedBaseRecognizer
  6. class OverBookTransactionInput(BaseModel):
  7. """疑似过账流水识别工具输入"""
  8. csv_path: Optional[str] = Field(
  9. None,
  10. description="CSV文件路径(可选)。如果初始化时已提供csv_path,可以不用再次传入。"
  11. )
  12. class Config:
  13. arbitrary_types_allowed = True
  14. class OverBookTransactionRecognizer(EnhancedBaseRecognizer):
  15. """
  16. 疑似过账流水识别器
  17. 异常规则定义:
  18. 账户在接收大额资金入账后,7个自然日内即发生与该入账金额完全一致(或高度接近)的资金流出,
  19. 形成"入账-流出"的闭环资金流动,且缺乏合理商业背景、实际业务往来支撑或真实收付需求,
  20. 资金未发生实质性使用或流转,仅通过账户完成过渡划转,符合过账交易核心属性。
  21. 核心逻辑:
  22. 1. 筛选≥阈值金额的"收入"交易
  23. 2. 查找每笔大额收入后7天内的"支出"交易
  24. 3. 匹配金额(±容忍度范围内)
  25. 4. 分析交易背景合理性
  26. 5. 标记疑似过账的交易对
  27. """
  28. args_schema: Type[BaseModel] = OverBookTransactionInput
  29. # 配置参数
  30. amount_threshold: float = Field(
  31. 100000.0,
  32. description="金额阈值(元),交易金额≥此值才进行检测"
  33. )
  34. time_window_days: int = Field(
  35. 7,
  36. description="时间窗口(天),从收入发生日开始计算"
  37. )
  38. amount_tolerance: float = Field(
  39. 0.01,
  40. description="金额容忍度,±此比例内视为金额匹配"
  41. )
  42. min_stay_time_hours: int = Field(
  43. 1,
  44. description="最小停留时间(小时),避免即时进出被视为过账"
  45. )
  46. # 合理性判断参数
  47. enable_background_check: bool = Field(
  48. True,
  49. description="是否启用交易背景合理性检查"
  50. )
  51. reasonable_background_keywords: List[str] = Field(
  52. [
  53. "工资发放", "奖金发放", "绩效发放", "报销款",
  54. "货款", "租金收入", "投资款", "贷款", "还款",
  55. "采购付款", "支付货款", "缴税", "缴费", "消费",
  56. "日常支出", "生活支出", "业务往来", "贸易款"
  57. ],
  58. description="合理业务背景关键词列表"
  59. )
  60. high_risk_keywords: List[str] = Field(
  61. [
  62. "过账", "过渡", "走账", "倒账", "资金划转",
  63. "临时周转", "无实际业务", "过渡资金", "资金过桥",
  64. "代收代付", "代转", "垫资", "拆借", "内部往来"
  65. ],
  66. description="高风险关键词(过账特征)列表"
  67. )
  68. # 交易对手分析
  69. enable_counterparty_check: bool = Field(
  70. True,
  71. description="是否启用交易对手关联性检查"
  72. )
  73. # 模式检测配置
  74. detect_single_pair: bool = Field(
  75. True,
  76. description="检测单笔流入-流出对"
  77. )
  78. detect_split_pattern: bool = Field(
  79. True,
  80. description="检测拆分过账(一笔流入多笔流出)"
  81. )
  82. detect_merge_pattern: bool = Field(
  83. True,
  84. description="检测合并过账(多笔流入一笔流出)"
  85. )
  86. # 严重程度配置
  87. severity_level: str = Field(
  88. 'high',
  89. description="异常严重程度(high/medium/low)"
  90. )
  91. def __init__(self, csv_path: str = None, config: Dict[str, Any] = None, **kwargs):
  92. """
  93. 初始化疑似过账流水识别器
  94. Args:
  95. csv_path: CSV文件路径
  96. config: 配置参数
  97. **kwargs: 其他参数
  98. """
  99. # 调用父类的 __init__
  100. super().__init__(
  101. name="over_book_transaction_recognizer",
  102. description="识别疑似过账流水:大额资金短期内相同金额进出,缺乏真实业务背景。",
  103. display_name="疑似过账流水识别器",
  104. csv_path=csv_path,
  105. config=config,
  106. **kwargs
  107. )
  108. # 从config获取配置,更新Field属性
  109. overbook_config = self.get_config_value('over_book_transaction_recognition', {})
  110. if overbook_config:
  111. config_mapping = {
  112. 'amount_threshold': 'amount_threshold',
  113. 'time_window_days': 'time_window_days',
  114. 'amount_tolerance': 'amount_tolerance',
  115. 'min_stay_time_hours': 'min_stay_time_hours',
  116. 'enable_background_check': 'enable_background_check',
  117. 'reasonable_background_keywords': 'reasonable_background_keywords',
  118. 'high_risk_keywords': 'high_risk_keywords',
  119. 'enable_counterparty_check': 'enable_counterparty_check',
  120. 'detect_single_pair': 'detect_single_pair',
  121. 'detect_split_pattern': 'detect_split_pattern',
  122. 'detect_merge_pattern': 'detect_merge_pattern',
  123. 'severity_level': 'severity_level'
  124. }
  125. for config_key, attr_name in config_mapping.items():
  126. if config_key in overbook_config:
  127. setattr(self, attr_name, overbook_config[config_key])
  128. print(f"✅ {self.display_name} 初始化完成")
  129. print(f" 金额阈值: ¥{self.amount_threshold:,.2f}")
  130. print(f" 时间窗口: {self.time_window_days}天")
  131. print(f" 金额容忍度: ±{self.amount_tolerance:.1%}")
  132. print(f" 最小停留时间: {self.min_stay_time_hours}小时")
  133. print(f" 背景检查: {'启用' if self.enable_background_check else '禁用'}")
  134. print(f" 对手方检查: {'启用' if self.enable_counterparty_check else '禁用'}")
  135. print(f" 检测模式: 单笔匹配/拆分/合并")
  136. print(f" 异常严重程度: {self.severity_level.upper()}")
  137. def _is_large_inflow(self, row: pd.Series) -> bool:
  138. """
  139. 判断是否为需要检测的大额收入
  140. Args:
  141. row: 交易记录
  142. Returns:
  143. bool: 是否为大额收入
  144. """
  145. # 必须是收入方向
  146. if row.get('txDirection') != '收入':
  147. return False
  148. # 金额必须达到阈值
  149. amount = row.get('txAmount', 0)
  150. if pd.isna(amount) or amount < self.amount_threshold:
  151. return False
  152. return True
  153. def _find_matching_outflows(self, inflow: pd.Series, df: pd.DataFrame) -> List[pd.Series]:
  154. """
  155. 查找匹配的流出交易
  156. Args:
  157. inflow: 大额收入记录
  158. df: 完整数据集
  159. Returns:
  160. List[pd.Series]: 匹配的流出交易列表
  161. """
  162. if pd.isna(inflow.get('datetime')):
  163. return []
  164. inflow_time = inflow['datetime']
  165. inflow_amount = inflow['txAmount']
  166. # 计算时间窗口
  167. time_end = inflow_time + timedelta(days=self.time_window_days)
  168. # 筛选条件:时间窗口内、支出方向、金额匹配
  169. mask = (
  170. (df['datetime'] > inflow_time) & # 晚于流入时间
  171. (df['datetime'] <= time_end) & # 在时间窗口内
  172. (df['txDirection'] == '支出') & # 支出方向
  173. (df['txId'] != inflow['txId']) # 排除同一笔交易
  174. )
  175. candidate_outflows = df[mask].copy()
  176. # 金额匹配检查
  177. matching_outflows = []
  178. for _, outflow in candidate_outflows.iterrows():
  179. outflow_amount = outflow['txAmount']
  180. # 检查金额是否匹配(考虑容忍度)
  181. amount_ratio = outflow_amount / inflow_amount
  182. if abs(amount_ratio - 1.0) <= self.amount_tolerance:
  183. matching_outflows.append(outflow)
  184. return matching_outflows
  185. def _analyze_background_reasonableness(self, inflow: pd.Series, outflow: pd.Series) -> Tuple[bool, str]:
  186. """
  187. 分析交易背景合理性
  188. Args:
  189. inflow: 流入交易记录
  190. outflow: 流出交易记录
  191. Returns:
  192. Tuple[bool, str]: (是否合理, 合理性描述)
  193. """
  194. if not self.enable_background_check:
  195. return True, "背景检查已禁用"
  196. inflow_summary = str(inflow.get('txSummary', '')).lower()
  197. outflow_summary = str(outflow.get('txSummary', '')).lower()
  198. inflow_counterparty = str(inflow.get('txCounterparty', '')).lower()
  199. outflow_counterparty = str(outflow.get('txCounterparty', '')).lower()
  200. reasons = []
  201. is_reasonable = True
  202. # 1. 检查高风险关键词
  203. for keyword in self.high_risk_keywords:
  204. if keyword in inflow_summary or keyword in outflow_summary:
  205. reasons.append(f"包含高风险关键词: '{keyword}'")
  206. is_reasonable = False
  207. # 2. 检查合理背景关键词
  208. has_reasonable_keyword = False
  209. for keyword in self.reasonable_background_keywords:
  210. if keyword in inflow_summary or keyword in outflow_summary:
  211. has_reasonable_keyword = True
  212. reasons.append(f"包含合理背景关键词: '{keyword}'")
  213. if has_reasonable_keyword:
  214. is_reasonable = True
  215. # 3. 检查交易对手关系(如果启用)
  216. if self.enable_counterparty_check:
  217. if inflow_counterparty == outflow_counterparty and inflow_counterparty not in ['', 'nan']:
  218. reasons.append(f"相同交易对手: {inflow_counterparty}")
  219. # 相同对手方可能是正常业务(如还款),也可能是过账嫌疑
  220. if '还款' in inflow_summary or '还款' in outflow_summary:
  221. reasons.append("可能为正常还款业务")
  222. is_reasonable = True
  223. else:
  224. reasons.append("相同对手方资金来回流动,需关注")
  225. is_reasonable = False
  226. # 4. 检查停留时间(太短可能有问题)
  227. stay_time_hours = (outflow['datetime'] - inflow['datetime']).total_seconds() / 3600
  228. if stay_time_hours < self.min_stay_time_hours:
  229. reasons.append(f"资金停留时间过短: {stay_time_hours:.1f}小时")
  230. is_reasonable = False
  231. # 5. 检查摘要信息完整性
  232. if inflow_summary == '' or outflow_summary == '':
  233. reasons.append("交易摘要信息不完整")
  234. is_reasonable = False
  235. # 生成描述
  236. if not reasons:
  237. description = "背景检查未发现明显异常"
  238. else:
  239. description = "; ".join(reasons)
  240. return is_reasonable, description
  241. def _format_over_book_reason(self, inflow: pd.Series, outflow: pd.Series,
  242. background_analysis: str, stay_days: float) -> str:
  243. """
  244. 生成过账异常原因描述
  245. Args:
  246. inflow: 流入交易记录
  247. outflow: 流出交易记录
  248. background_analysis: 背景分析结果
  249. stay_days: 停留天数
  250. Returns:
  251. str: 异常原因描述
  252. """
  253. inflow_amount = inflow['txAmount']
  254. outflow_amount = outflow['txAmount']
  255. amount_diff = abs(outflow_amount - inflow_amount)
  256. amount_diff_percent = (amount_diff / inflow_amount) * 100
  257. reason_parts = [
  258. f"疑似过账交易:收入¥{inflow_amount:,.2f}后{stay_days:.1f}天内支出¥{outflow_amount:,.2f}",
  259. f"金额匹配度:差异¥{amount_diff:,.2f}({amount_diff_percent:.2f}%)"
  260. ]
  261. if stay_days < 1:
  262. reason_parts.append(f"资金停留时间仅{stay_days * 24:.1f}小时")
  263. else:
  264. reason_parts.append(f"资金停留时间{stay_days:.1f}天")
  265. if background_analysis:
  266. reason_parts.append(f"背景分析:{background_analysis}")
  267. return ";".join(reason_parts)
  268. def recognize(self, csv_path: str = None, **kwargs) -> Dict[str, Any]:
  269. """
  270. 识别疑似过账流水异常
  271. Args:
  272. csv_path: CSV文件路径
  273. **kwargs: 其他参数
  274. Returns:
  275. Dict[str, Any]: 识别结果
  276. """
  277. try:
  278. # 使用父类的load_data方法加载标准化数据
  279. df = self.load_data(csv_path)
  280. print(f"🔍 {self.display_name}开始检查,共 {len(df)} 条记录")
  281. print(f" 检查规则: ≥¥{self.amount_threshold:,.2f}收入 → {self.time_window_days}天内 → 匹配金额支出")
  282. # 检查必需字段
  283. required_fields = ['txId', 'txDate', 'txTime', 'txAmount', 'txDirection', 'txSummary']
  284. missing_fields = [field for field in required_fields if field not in df.columns]
  285. if missing_fields:
  286. return {
  287. 'recognition_type': self.display_name,
  288. 'identified_count': 0,
  289. 'identified_anomalies': [],
  290. 'recognition_status': '失败',
  291. 'error': f'缺少必需字段: {missing_fields}'
  292. }
  293. # 确保数据按时间排序
  294. if 'datetime' not in df.columns:
  295. return {
  296. 'recognition_type': self.display_name,
  297. 'identified_count': 0,
  298. 'identified_anomalies': [],
  299. 'recognition_status': '失败',
  300. 'error': '缺少datetime字段,无法进行时间序列分析'
  301. }
  302. df = df.sort_values('datetime').copy()
  303. # ============ 识别大额收入交易 ============
  304. print(f"🔍 正在识别大额收入交易...")
  305. # 筛选大额收入
  306. large_inflows_mask = df.apply(self._is_large_inflow, axis=1)
  307. large_inflows = df[large_inflows_mask].copy()
  308. if len(large_inflows) == 0:
  309. print(f"📊 未发现≥¥{self.amount_threshold:,.2f}的大额收入记录")
  310. return {
  311. 'recognition_type': self.display_name,
  312. 'identified_count': 0,
  313. 'identified_anomalies': [],
  314. 'recognition_status': '完成',
  315. 'recognition_parameters': {
  316. 'amount_threshold': self.amount_threshold,
  317. 'time_window_days': self.time_window_days,
  318. 'amount_tolerance': self.amount_tolerance,
  319. 'total_checked': len(df)
  320. },
  321. 'statistics': {
  322. 'total_transactions': len(df),
  323. 'large_inflows_count': 0,
  324. 'max_transaction_amount': float(df['txAmount'].max()),
  325. 'avg_transaction_amount': float(df['txAmount'].mean())
  326. }
  327. }
  328. print(f"📊 发现 {len(large_inflows)} 笔大额收入记录")
  329. print(f" 大额收入金额范围: ¥{large_inflows['txAmount'].min():,.2f} - ¥{large_inflows['txAmount'].max():,.2f}")
  330. # ============ 查找匹配的流出交易 ============
  331. print(f"🔍 正在查找匹配的流出交易...")
  332. identified_anomalies = []
  333. transaction_pairs = []
  334. match_statistics = {
  335. 'total_pairs_found': 0,
  336. 'reasonable_pairs': 0,
  337. 'suspicious_pairs': 0
  338. }
  339. for idx, inflow in large_inflows.iterrows():
  340. inflow_id = str(inflow['txId'])
  341. inflow_amount = inflow['txAmount']
  342. inflow_date = inflow['datetime'].strftime('%Y-%m-%d %H:%M:%S')
  343. print(f" 🔍 分析大额收入 {inflow_id}: ¥{inflow_amount:,.2f} ({inflow_date})")
  344. # 查找匹配的流出
  345. matching_outflows = self._find_matching_outflows(inflow, df)
  346. if not matching_outflows:
  347. print(f" ✅ 未发现匹配的流出交易")
  348. continue
  349. print(f" 📊 发现 {len(matching_outflows)} 笔匹配流出")
  350. # 分析每对交易
  351. for outflow in matching_outflows:
  352. outflow_id = str(outflow['txId'])
  353. outflow_amount = outflow['txAmount']
  354. # 计算停留时间
  355. stay_time = outflow['datetime'] - inflow['datetime']
  356. stay_days = stay_time.total_seconds() / 86400
  357. # 分析背景合理性
  358. is_reasonable, background_analysis = self._analyze_background_reasonableness(inflow, outflow)
  359. # 记录交易对信息
  360. pair_info = {
  361. 'inflow_id': inflow_id,
  362. 'outflow_id': outflow_id,
  363. 'inflow_amount': inflow_amount,
  364. 'outflow_amount': outflow_amount,
  365. 'amount_diff': abs(outflow_amount - inflow_amount),
  366. 'stay_days': stay_days,
  367. 'is_reasonable': is_reasonable,
  368. 'background_analysis': background_analysis
  369. }
  370. transaction_pairs.append(pair_info)
  371. match_statistics['total_pairs_found'] += 1
  372. if is_reasonable:
  373. match_statistics['reasonable_pairs'] += 1
  374. print(f" ✅ 交易对 {inflow_id}→{outflow_id}: 合理背景 ({background_analysis[:50]}...)")
  375. else:
  376. match_statistics['suspicious_pairs'] += 1
  377. # 生成异常原因
  378. reason = self._format_over_book_reason(inflow, outflow, background_analysis, stay_days)
  379. print(f" ❌ 发现疑似过账: {inflow_id}→{outflow_id}")
  380. print(f" 原因: {reason[:80]}...")
  381. # 创建异常记录(记录流出交易作为异常点)
  382. additional_info = {
  383. 'over_book_analysis': {
  384. 'inflow_transaction': {
  385. 'txId': inflow_id,
  386. 'txDate': inflow['txDate'],
  387. 'txTime': inflow['txTime'],
  388. 'txAmount': inflow_amount,
  389. 'txSummary': inflow.get('txSummary', ''),
  390. 'txCounterparty': inflow.get('txCounterparty', '')
  391. },
  392. 'outflow_transaction': {
  393. 'txId': outflow_id,
  394. 'txDate': outflow['txDate'],
  395. 'txTime': outflow['txTime'],
  396. 'txAmount': outflow_amount,
  397. 'txSummary': outflow.get('txSummary', ''),
  398. 'txCounterparty': outflow.get('txCounterparty', '')
  399. },
  400. 'pair_analysis': {
  401. 'stay_days': stay_days,
  402. 'stay_hours': stay_days * 24,
  403. 'amount_match_ratio': outflow_amount / inflow_amount,
  404. 'background_check_result': background_analysis,
  405. 'is_reasonable': is_reasonable,
  406. 'detection_parameters': {
  407. 'amount_threshold': self.amount_threshold,
  408. 'time_window_days': self.time_window_days,
  409. 'amount_tolerance': self.amount_tolerance
  410. }
  411. }
  412. }
  413. }
  414. # 使用流出交易作为异常记录主体
  415. anomaly = self.format_anomaly_record(
  416. row=outflow,
  417. reason=reason,
  418. severity=self.severity_level,
  419. check_type='over_book_transaction',
  420. **additional_info
  421. )
  422. identified_anomalies.append(anomaly)
  423. # ============ 结果统计 ============
  424. print(f"✅ {self.display_name}检查完成")
  425. print(f" 检查结果:")
  426. print(f" 大额收入记录: {len(large_inflows)} 笔")
  427. print(f" 匹配交易对: {match_statistics['total_pairs_found']} 对")
  428. print(f" 合理交易对: {match_statistics['reasonable_pairs']} 对")
  429. print(f" 疑似过账对: {match_statistics['suspicious_pairs']} 对")
  430. print(f" 异常记录数: {len(identified_anomalies)} 条")
  431. # 显示详细信息
  432. if match_statistics['suspicious_pairs'] > 0:
  433. print("📋 疑似过账交易详情:")
  434. for i, pair in enumerate(transaction_pairs[:5]): # 显示前5条
  435. if not pair['is_reasonable']:
  436. print(f" {i + 1}. {pair['inflow_id']}→{pair['outflow_id']}: "
  437. f"¥{pair['inflow_amount']:,.2f}→¥{pair['outflow_amount']:,.2f} "
  438. f"({pair['stay_days']:.1f}天)")
  439. return {
  440. 'recognition_type': self.display_name,
  441. 'identified_count': len(identified_anomalies),
  442. 'identified_anomalies': identified_anomalies,
  443. 'recognition_status': '完成',
  444. 'recognition_parameters': {
  445. 'amount_threshold': self.amount_threshold,
  446. 'time_window_days': self.time_window_days,
  447. 'amount_tolerance': self.amount_tolerance,
  448. 'min_stay_time_hours': self.min_stay_time_hours,
  449. 'enable_background_check': self.enable_background_check,
  450. 'enable_counterparty_check': self.enable_counterparty_check,
  451. 'total_large_inflows': len(large_inflows)
  452. },
  453. 'statistics': {
  454. 'total_transactions': len(df),
  455. 'large_inflows_count': len(large_inflows),
  456. 'large_inflows_amount_stats': {
  457. 'total': float(large_inflows['txAmount'].sum()),
  458. 'avg': float(large_inflows['txAmount'].mean()),
  459. 'max': float(large_inflows['txAmount'].max()),
  460. 'min': float(large_inflows['txAmount'].min())
  461. } if len(large_inflows) > 0 else {},
  462. 'match_statistics': match_statistics,
  463. 'transaction_pairs_count': len(transaction_pairs),
  464. 'suspicious_pairs_details': [
  465. {
  466. 'inflow_id': p['inflow_id'],
  467. 'outflow_id': p['outflow_id'],
  468. 'inflow_amount': p['inflow_amount'],
  469. 'outflow_amount': p['outflow_amount'],
  470. 'stay_days': p['stay_days'],
  471. 'background_analysis': p['background_analysis']
  472. }
  473. for p in transaction_pairs if not p['is_reasonable']
  474. ][:10] # 只保留前10条详情
  475. }
  476. }
  477. except FileNotFoundError as e:
  478. return {
  479. 'recognition_type': self.display_name,
  480. 'identified_count': 0,
  481. 'identified_anomalies': [],
  482. 'recognition_status': '失败',
  483. 'error': f'文件不存在: {str(e)}'
  484. }
  485. except Exception as e:
  486. import traceback
  487. traceback.print_exc()
  488. return {
  489. 'recognition_type': self.display_name,
  490. 'identified_count': 0,
  491. 'identified_anomalies': [],
  492. 'recognition_status': '失败',
  493. 'error': f'数据加载或处理失败: {str(e)}'
  494. }
  495. def get_summary(self) -> Dict[str, Any]:
  496. """获取识别器摘要"""
  497. summary = super().get_summary()
  498. summary.update({
  499. 'amount_threshold': self.amount_threshold,
  500. 'time_window_days': self.time_window_days,
  501. 'amount_tolerance': self.amount_tolerance,
  502. 'min_stay_time_hours': self.min_stay_time_hours,
  503. 'enable_background_check': self.enable_background_check,
  504. 'reasonable_keywords_count': len(self.reasonable_background_keywords),
  505. 'high_risk_keywords_count': len(self.high_risk_keywords),
  506. 'enable_counterparty_check': self.enable_counterparty_check,
  507. 'detect_patterns': {
  508. 'single_pair': self.detect_single_pair,
  509. 'split_pattern': self.detect_split_pattern,
  510. 'merge_pattern': self.detect_merge_pattern
  511. },
  512. 'severity_level': self.severity_level,
  513. 'data_loaded': self._data is not None
  514. })
  515. return summary
  516. def get_config_summary(self) -> Dict[str, Any]:
  517. """获取配置摘要"""
  518. return {
  519. "金额阈值": f"¥{self.amount_threshold:,.2f}",
  520. "时间窗口": f"{self.time_window_days}天",
  521. "金额容忍度": f"±{self.amount_tolerance:.1%}",
  522. "最小停留时间": f"{self.min_stay_time_hours}小时",
  523. "背景检查": "启用" if self.enable_background_check else "禁用",
  524. "合理关键词": f"{len(self.reasonable_background_keywords)}个",
  525. "高风险关键词": f"{len(self.high_risk_keywords)}个",
  526. "对手方检查": "启用" if self.enable_counterparty_check else "禁用",
  527. "检测逻辑": f"大额收入后{self.time_window_days}天内出现匹配金额支出,且缺乏合理业务背景",
  528. "业务规则描述": "资金短暂停留即流出,缺乏真实业务背景,疑似过账交易"
  529. }