first commit
This commit is contained in:
0
backend/app/rules/__init__.py
Normal file
0
backend/app/rules/__init__.py
Normal file
57
backend/app/rules/assessment_rules.py
Normal file
57
backend/app/rules/assessment_rules.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Rule-based fraud amount assessment.
|
||||
|
||||
Classifies each transaction into high / medium / low confidence fraud,
|
||||
and generates initial reason text.
|
||||
"""
|
||||
from app.models.transaction import TransactionRecord
|
||||
from app.models.assessment import ConfidenceLevel
|
||||
|
||||
FRAUD_KEYWORDS = ["投资", "保证金", "手续费", "解冻", "税费", "充值", "提币", "提现"]
|
||||
|
||||
|
||||
def classify_transaction(tx: TransactionRecord) -> tuple[ConfidenceLevel, str, str]:
|
||||
"""Return (confidence_level, reason, exclude_reason)."""
|
||||
if tx.is_transit:
|
||||
return (
|
||||
ConfidenceLevel.low,
|
||||
f"该笔为本人账户间中转({tx.source_app.value} -> {tx.counterparty_name}),不直接计入被骗损失。",
|
||||
"本人账户间互转,仅作为资金路径展示。",
|
||||
)
|
||||
|
||||
if tx.direction.value == "in":
|
||||
return (
|
||||
ConfidenceLevel.low,
|
||||
f"该笔为收入方向交易(+¥{float(tx.amount):,.2f}),通常不属于被骗损失。",
|
||||
"收入交易不计入损失。",
|
||||
)
|
||||
|
||||
remark = tx.remark or ""
|
||||
counterparty = tx.counterparty_name or ""
|
||||
confidence = tx.confidence
|
||||
|
||||
has_fraud_keyword = any(kw in remark or kw in counterparty for kw in FRAUD_KEYWORDS)
|
||||
|
||||
if confidence >= 0.9 and has_fraud_keyword:
|
||||
return (
|
||||
ConfidenceLevel.high,
|
||||
f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f},"
|
||||
f"备注为「{remark}」,与诈骗常见话术吻合,OCR置信度{confidence:.0%}。",
|
||||
"",
|
||||
)
|
||||
|
||||
if confidence >= 0.85:
|
||||
reason = (
|
||||
f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f}。"
|
||||
)
|
||||
if has_fraud_keyword:
|
||||
reason += f"备注「{remark}」含涉诈关键词。"
|
||||
return ConfidenceLevel.high, reason, ""
|
||||
reason += "建议结合笔录确认是否受诱导操作。"
|
||||
return ConfidenceLevel.medium, reason, "如经核实该笔为受害人主动日常消费,应排除。"
|
||||
|
||||
return (
|
||||
ConfidenceLevel.medium,
|
||||
f"受害人经{tx.source_app.value}向「{counterparty}」转账¥{float(tx.amount):,.2f},"
|
||||
f"OCR置信度较低({confidence:.0%}),需人工复核。",
|
||||
"OCR置信度不足,可能存在识别误差。",
|
||||
)
|
||||
32
backend/app/rules/dedup_rules.py
Normal file
32
backend/app/rules/dedup_rules.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Transaction deduplication rules.
|
||||
|
||||
Determines whether two transaction records likely represent the same
|
||||
underlying financial event captured from different screenshots / pages.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from app.models.transaction import TransactionRecord
|
||||
|
||||
TIME_WINDOW = timedelta(minutes=5)
|
||||
|
||||
|
||||
def is_duplicate_pair(a: TransactionRecord, b: TransactionRecord) -> bool:
|
||||
# Rule 1: exact order_no match
|
||||
if a.order_no and b.order_no and a.order_no == b.order_no:
|
||||
return True
|
||||
|
||||
# Rule 2: same amount + close time + same account tail
|
||||
if (
|
||||
float(a.amount) == float(b.amount)
|
||||
and a.trade_time
|
||||
and b.trade_time
|
||||
and abs(a.trade_time - b.trade_time) <= TIME_WINDOW
|
||||
):
|
||||
if a.self_account_tail_no and b.self_account_tail_no:
|
||||
if a.self_account_tail_no == b.self_account_tail_no:
|
||||
return True
|
||||
# same counterparty and close time is also strong signal
|
||||
if a.counterparty_name and a.counterparty_name == b.counterparty_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
35
backend/app/rules/transit_rules.py
Normal file
35
backend/app/rules/transit_rules.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Transit (self-transfer) detection rules.
|
||||
|
||||
Identifies transactions that are internal transfers between the victim's
|
||||
own accounts (e.g. bank -> Alipay -> WeChat) and should NOT be counted
|
||||
as fraud loss.
|
||||
"""
|
||||
from app.models.transaction import TransactionRecord
|
||||
|
||||
SELF_KEYWORDS = ["本人", "自己", "余额", "充值", "提现", "银行卡转入", "银行卡充值"]
|
||||
|
||||
|
||||
def is_self_transfer(tx: TransactionRecord, known_self_accounts: list[str]) -> bool:
|
||||
"""Check if a transaction is an inter-account transfer by the victim."""
|
||||
counterparty = (tx.counterparty_name or "").lower()
|
||||
remark = (tx.remark or "").lower()
|
||||
|
||||
# Rule 1: counterparty matches known self accounts
|
||||
for acct in known_self_accounts:
|
||||
if acct and acct.lower() in counterparty:
|
||||
return True
|
||||
|
||||
# Rule 2: counterparty contains self-transfer keywords
|
||||
for kw in SELF_KEYWORDS:
|
||||
if kw in counterparty or kw in remark:
|
||||
return True
|
||||
|
||||
# Rule 3: counterparty references another payment app owned by victim
|
||||
app_keywords = ["支付宝", "微信", "银行卡", "数字钱包"]
|
||||
victim_patterns = [f"{app}-" for app in app_keywords] + app_keywords
|
||||
for pat in victim_patterns:
|
||||
if pat in counterparty:
|
||||
if tx.direction.value == "out":
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user