first commit

This commit is contained in:
2026-03-11 16:28:04 +08:00
commit c0f9ddabbf
101 changed files with 11601 additions and 0 deletions

View File

View 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置信度不足可能存在识别误差。",
)

View 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

View 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