Files
fund-tracer/backend/app/services/flow_service.py

73 lines
2.3 KiB
Python
Raw Normal View History

2026-03-11 16:28:04 +08:00
"""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, "未知账户")