""" 私户收款检测算法 检测银行流水是否存在向个人账户转账的风险 """ from typing import Dict, Any, List, Optional from datetime import datetime, timedelta from sqlalchemy import select, func, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession from loguru import logger from .base import ( RiskDetectionAlgorithm, DetectionContext, DetectionResult, RiskEvidence, ) from app.models.risk_detection import RiskLevel from app.models.bank_transaction import BankTransaction class PrivateAccountDetectionAlgorithm(RiskDetectionAlgorithm): """私户收款检测算法""" def get_algorithm_code(self) -> str: return "PRIVATE_ACCOUNT_DETECTION" def get_algorithm_name(self) -> str: return "私户收款检测" def get_description(self) -> str: return ( "通过分析银行流水记录," "检测是否存在向个人账户转账、使用私人账户收款等违规行为" ) async def _do_detect(self, context: DetectionContext) -> DetectionResult: """执行私户收款检测""" # 获取参数 account_no = context.get_parameter("account_no") period = context.get_parameter("period") # 格式:YYYY-MM threshold_amount = context.get_parameter("threshold_amount", 10000) # 阈值金额 if not account_no or not period: return self._create_error_result(context, "缺少必要参数:account_no 或 period") db_session = context.db_session if not db_session: return self._create_error_result(context, "缺少数据库会话") try: # 获取银行流水 transactions = await self._get_bank_transactions( db_session, account_no, period ) # 分析私户收款情况 private_transfers = self._analyze_private_transfers( transactions, threshold_amount ) # 计算风险指标 total_private_amount = sum(t["amount"] for t in private_transfers) private_count = len(private_transfers) total_inflow = sum( t["amount"] for t in transactions if t["transaction_type"] == "转入" ) private_ratio = ( (total_private_amount / total_inflow * 100) if total_inflow > 0 else 0 ) # 判断风险等级 risk_level, risk_score = self._calculate_risk_level( private_ratio, private_count, total_private_amount, threshold_amount ) # 生成风险描述和建议 description, suggestion = self._generate_risk_description( private_ratio, private_count, total_private_amount, total_inflow ) # 创建检测结果 result = DetectionResult( task_id=context.task_id, rule_id=context.rule_id, entity_id=account_no, entity_type="bank_account", risk_level=risk_level, risk_score=risk_score, description=description, suggestion=suggestion, risk_data={ "total_transactions": len(transactions), "private_transfers": private_transfers, "private_count": private_count, "total_private_amount": total_private_amount, "total_inflow": total_inflow, "private_ratio": private_ratio, "threshold_amount": threshold_amount, "period": period, }, ) # 添加证据 result.add_evidence(RiskEvidence( evidence_type="account_summary", description=f"账户 {account_no} 在 {period} 期间共有 {len(transactions)} 笔交易", data={ "account_no": account_no, "period": period, "total_transactions": len(transactions), }, )) result.add_evidence(RiskEvidence( evidence_type="private_transfer_summary", description=f"检测到 {private_count} 笔私人账户转账,总金额 {total_private_amount:,.2f}元", data={ "count": private_count, "total_amount": total_private_amount, "ratio": private_ratio, }, )) # 添加主要违规记录作为证据 for transfer in private_transfers[:5]: # 只记录前5条 result.add_evidence(RiskEvidence( evidence_type="private_transfer_detail", description=f"{transfer['date']}: {transfer['counterparty']} - {transfer['amount']:,.2f}元", data=transfer, )) return result except Exception as e: logger.error(f"私户收款检测执行失败: {str(e)}", exc_info=True) return self._create_error_result(context, f"检测执行失败: {str(e)}") async def _get_bank_transactions( self, db_session: AsyncSession, account_no: str, period: str, ) -> List[Dict[str, Any]]: """获取银行流水记录""" try: # 解析期间 start_date, end_date = self._parse_period(period) stmt = select(BankTransaction).where( and_( BankTransaction.account_no == account_no, BankTransaction.transaction_date >= start_date, BankTransaction.transaction_date <= end_date, ) ).order_by(BankTransaction.transaction_date.desc()) result = await db_session.execute(stmt) transactions = result.scalars().all() # 转换为字典格式 return [ { "transaction_id": t.transaction_id, "transaction_date": t.transaction_date, "transaction_type": t.transaction_type, "amount": t.transaction_amount, "counterparty_name": t.counterparty_account_name, "counterparty_account": t.counterparty_account_no, "bank_name": t.counterparty_bank_name, "purpose": t.transaction_purpose, } for t in transactions ] except Exception as e: logger.error(f"获取银行流水失败: {str(e)}") return [] def _analyze_private_transfers( self, transactions: List[Dict[str, Any]], threshold_amount: float ) -> List[Dict[str, Any]]: """分析私人账户转账""" private_patterns = [ "个人", "有限公司", "有限责任公司", "股份有限公司", "集团", "传媒", "科技", "文化", "商贸", "贸易", "工作室", ] private_transfers = [] for t in transactions: # 只检查转入交易 if t["transaction_type"] != "转入": continue # 检查金额是否超过阈值 if t["amount"] < threshold_amount: continue # 检查对手方账户名 counterparty = t["counterparty_name"] or "" account_no = t["counterparty_account"] or "" # 判断是否为私人账户 is_private = self._is_private_account(counterparty, private_patterns) if is_private: private_transfers.append({ "date": t["transaction_date"].strftime("%Y-%m-%d"), "amount": t["amount"], "counterparty": counterparty, "counterparty_account": account_no, "bank": t["bank_name"], "purpose": t["purpose"], }) return private_transfers def _is_private_account( self, account_name: str, private_patterns: List[str] ) -> bool: """判断是否为私人账户""" if not account_name: return True # 没有账户名,疑似私人账户 # 检查是否包含企业标识 for pattern in private_patterns: if pattern in account_name: return False # 检查是否是个人姓名(2-4个汉字) if len(account_name) <= 4 and len(account_name) >= 2: # 简单判断:如果都是汉字,可能是个人姓名 if all(ord(char) >= 0x4e00 and ord(char) <= 0x9fa5 for char in account_name): return True return False def _calculate_risk_level( self, private_ratio: float, private_count: int, total_amount: float, threshold_amount: float, ) -> tuple[RiskLevel, float]: """计算风险等级""" if private_count == 0: return RiskLevel.LOW, 0.0 # 基于私户比例、笔数和金额综合判断 if private_ratio > 80 or (private_count > 10 and total_amount > 500000): return RiskLevel.CRITICAL, 95.0 elif private_ratio > 60 or (private_count > 5 and total_amount > 200000): return RiskLevel.HIGH, 80.0 elif private_ratio > 40 or (private_count > 3 and total_amount > 100000): return RiskLevel.MEDIUM, 60.0 elif private_ratio > 20 or private_count > 1: return RiskLevel.LOW, 30.0 else: return RiskLevel.LOW, 10.0 def _generate_risk_description( self, private_ratio: float, private_count: int, total_private_amount: float, total_inflow: float, ) -> tuple[str, str]: """生成风险描述和建议""" if private_count > 0: description = ( f"检测到私户收款风险:该账户共收到 {private_count} 笔来自私人账户的转账," f"总金额 {total_private_amount:,.2f}元," f"占全部流入资金的比例为 {private_ratio:.2f}%," f"可能存在使用私人账户违规收款的税务风险。" ) suggestion = ( "1. 核实私人账户转账的合法性和真实性;\n" "2. 检查是否存在未申报的收入;\n" "3. 补充相关业务合同和发票证明;\n" "4. 如涉及税务问题,建议及时自查补报;\n" "5. 建立完善的资金流水管理制度。" ) else: description = "未检测到明显的私户收款风险,银行流水基本正常。" suggestion = "继续保持规范的账户使用习惯。" return description, suggestion def _parse_period(self, period: str) -> tuple[datetime, datetime]: """解析期间为开始和结束日期""" try: year, month = map(int, period.split("-")) start_date = datetime(year, month, 1) if month == 12: end_date = datetime(year + 1, 1, 1) - timedelta(days=1) else: end_date = datetime(year, month + 1, 1) - timedelta(days=1) return start_date, end_date except Exception as e: logger.error(f"解析期间失败: {period}, 错误: {str(e)}") # 默认返回当前月 now = datetime.now() start_date = datetime(now.year, now.month, 1) end_date = now return start_date, end_date def _create_error_result(self, context: DetectionContext, error_message: str) -> DetectionResult: """创建错误结果""" return DetectionResult( task_id=context.task_id, rule_id=context.rule_id, entity_id=context.get_parameter("account_no", ""), entity_type="bank_account", risk_level=RiskLevel.UNKNOWN, risk_score=0.0, description=f"私户收款检测失败: {error_message}", suggestion="请检查参数设置或联系系统管理员", )