323 lines
12 KiB
Python
323 lines
12 KiB
Python
"""
|
||
私户收款检测算法
|
||
检测银行流水是否存在向个人账户转账的风险
|
||
"""
|
||
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="请检查参数设置或联系系统管理员",
|
||
)
|