73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
"""Build the fund-flow graph from deduplicated transactions."""
|
|
from uuid import UUID
|
|
from collections import defaultdict
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.repositories.transaction_repo import TransactionRepository
|
|
from app.schemas.transaction import FlowGraphOut, FlowNodeOut, FlowEdgeOut
|
|
|
|
|
|
async def build_flow_graph(case_id: UUID, db: AsyncSession) -> FlowGraphOut:
|
|
repo = TransactionRepository(db)
|
|
transactions = await repo.get_all_by_case(case_id)
|
|
|
|
valid = [tx for tx in transactions if not tx.is_duplicate]
|
|
|
|
nodes_map: dict[str, str] = {} # label -> type
|
|
edge_agg: dict[tuple[str, str], dict] = defaultdict(
|
|
lambda: {"amount": 0.0, "count": 0, "trade_time": ""}
|
|
)
|
|
|
|
for tx in valid:
|
|
self_label = _self_label(tx)
|
|
counter_label = tx.counterparty_name or "未知对手方"
|
|
|
|
if self_label not in nodes_map:
|
|
nodes_map[self_label] = "self"
|
|
|
|
if counter_label not in nodes_map:
|
|
nodes_map[counter_label] = "suspect" if not tx.is_transit else "transit"
|
|
|
|
if tx.direction.value == "out":
|
|
key = (self_label, counter_label)
|
|
else:
|
|
key = (counter_label, self_label)
|
|
|
|
edge_agg[key]["amount"] += float(tx.amount)
|
|
edge_agg[key]["count"] += 1
|
|
time_str = tx.trade_time.strftime("%Y-%m-%d %H:%M") if tx.trade_time else ""
|
|
if not edge_agg[key]["trade_time"]:
|
|
edge_agg[key]["trade_time"] = time_str
|
|
|
|
nodes = [
|
|
FlowNodeOut(id=f"n-{i}", label=label, type=ntype)
|
|
for i, (label, ntype) in enumerate(nodes_map.items())
|
|
]
|
|
|
|
label_to_id = {n.label: n.id for n in nodes}
|
|
|
|
edges = [
|
|
FlowEdgeOut(
|
|
source=label_to_id[src],
|
|
target=label_to_id[tgt],
|
|
amount=info["amount"],
|
|
count=info["count"],
|
|
trade_time=info["trade_time"],
|
|
)
|
|
for (src, tgt), info in edge_agg.items()
|
|
]
|
|
|
|
return FlowGraphOut(nodes=nodes, edges=edges)
|
|
|
|
|
|
def _self_label(tx) -> str:
|
|
app_names = {
|
|
"wechat": "微信支付",
|
|
"alipay": "支付宝",
|
|
"bank": f"银行卡({tx.self_account_tail_no})" if tx.self_account_tail_no else "银行卡",
|
|
"digital_wallet": "数字钱包",
|
|
"other": "其他账户",
|
|
}
|
|
return app_names.get(tx.source_app.value, "未知账户")
|