Files
fund-tracer/backend/app/services/report.py
2026-03-09 14:46:56 +08:00

126 lines
4.6 KiB
Python

"""Report generation: Excel and PDF export."""
from io import BytesIO
from decimal import Decimal
from datetime import datetime
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
from openpyxl.utils import get_column_letter
# WeasyPrint optional for PDF
try:
from weasyprint import HTML, CSS
HAS_WEASYPRINT = True
except ImportError:
HAS_WEASYPRINT = False
async def build_excel_report(case, transactions: list) -> bytes:
"""Build Excel workbook: summary sheet + transaction detail sheet. Returns file bytes."""
wb = Workbook()
ws_summary = wb.active
ws_summary.title = "汇总"
ws_summary.append(["案件编号", case.case_number])
ws_summary.append(["受害人", case.victim_name])
ws_summary.append(["总损失", str(case.total_loss)])
ws_summary.append(["交易笔数", len(transactions)])
total_out = sum(
(t.amount if isinstance(t.amount, Decimal) else Decimal(str(t.amount)))
for t in transactions
if t.transaction_type in ("转出", "消费", "付款", "提现") or "转出" in (t.transaction_type or "") or "消费" in (t.transaction_type or "")
)
total_in = sum(
(t.amount if isinstance(t.amount, Decimal) else Decimal(str(t.amount)))
for t in transactions
if t.transaction_type in ("转入", "收款", "充值") or "转入" in (t.transaction_type or "") or "收款" in (t.transaction_type or "")
)
ws_summary.append(["转出合计", str(total_out)])
ws_summary.append(["转入合计", str(total_in)])
ws_summary.append(["净损失", str(total_out - total_in)])
for row in range(1, 8):
ws_summary.cell(row=row, column=1).font = Font(bold=True)
ws_detail = wb.create_sheet("交易明细")
headers = ["APP来源", "类型", "金额", "币种", "对方名称", "对方账号", "订单号", "交易时间", "备注", "置信度"]
ws_detail.append(headers)
for t in transactions:
ws_detail.append([
t.app_source,
t.transaction_type or "",
str(t.amount),
t.currency or "CNY",
t.counterparty_name or "",
t.counterparty_account or "",
t.order_number or "",
t.transaction_time.isoformat() if t.transaction_time else "",
t.remark or "",
t.confidence or "",
])
for col in range(1, len(headers) + 1):
ws_detail.cell(row=1, column=col).font = Font(bold=True)
for col in range(1, ws_detail.max_column + 1):
ws_detail.column_dimensions[get_column_letter(col)].width = 16
buf = BytesIO()
wb.save(buf)
buf.seek(0)
return buf.getvalue()
def _pdf_html(case, transactions: list) -> str:
rows = []
for t in transactions:
time_str = t.transaction_time.strftime("%Y-%m-%d %H:%M") if t.transaction_time else ""
rows.append(
f"<tr><td>{t.app_source}</td><td>{t.transaction_type or ''}</td><td>{t.amount}</td>"
f"<td>{t.counterparty_name or ''}</td><td>{t.counterparty_account or ''}</td><td>{time_str}</td></tr>"
)
table_rows = "\n".join(rows)
return f"""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><title>案件报告</title></head>
<body>
<h1>资金追踪报告</h1>
<p><strong>案件编号:</strong>{case.case_number}</p>
<p><strong>受害人:</strong>{case.victim_name}</p>
<p><strong>总损失:</strong>{case.total_loss}</p>
<p><strong>交易笔数:</strong>{len(transactions)}</p>
<h2>交易明细</h2>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse: collapse; width: 100%;">
<thead><tr><th>APP</th><th>类型</th><th>金额</th><th>对方名称</th><th>对方账号</th><th>时间</th></tr></thead>
<tbody>{table_rows}</tbody>
</table>
</body>
</html>
"""
async def build_pdf_report(case, transactions: list) -> bytes:
"""Build PDF report. Returns file bytes. Falls back to empty PDF if weasyprint not available."""
if not HAS_WEASYPRINT:
return b"%PDF-1.4 (WeasyPrint not installed)"
html_str = _pdf_html(case, transactions)
html = HTML(string=html_str)
buf = BytesIO()
html.write_pdf(buf)
buf.seek(0)
return buf.getvalue()
async def build_excel_report_path(case, transactions: list, path: str) -> str:
"""Write Excel to file path; return path."""
data = await build_excel_report(case, transactions)
with open(path, "wb") as f:
f.write(data)
return path
async def build_pdf_report_path(case, transactions: list, path: str) -> str:
"""Write PDF to file path; return path."""
data = await build_pdf_report(case, transactions)
with open(path, "wb") as f:
f.write(data)
return path