fix: user

This commit is contained in:
2026-03-17 23:43:19 +08:00
parent ef8ce120bb
commit ca12e55554
8 changed files with 132 additions and 26 deletions

View File

@@ -51,7 +51,8 @@ def is_fee_tolerant_transit_pair(tx_a: TransactionRecord, tx_b: TransactionRecor
amount_b = float(tx_b.amount or 0)
if amount_a <= 0 or amount_b <= 0:
return False
if _amount_ratio_diff(amount_a, amount_b) > FEE_TOLERANCE_RATIO:
diff_ratio = _amount_ratio_diff(amount_a, amount_b)
if diff_ratio > FEE_TOLERANCE_RATIO:
return False
time_a = tx_a.trade_time
@@ -59,7 +60,8 @@ def is_fee_tolerant_transit_pair(tx_a: TransactionRecord, tx_b: TransactionRecor
if not isinstance(time_a, datetime) or not isinstance(time_b, datetime):
return False
try:
return abs((time_a - time_b).total_seconds()) <= FEE_TRANSIT_WINDOW_SECONDS
seconds_gap = abs((time_a - time_b).total_seconds())
return seconds_gap <= FEE_TRANSIT_WINDOW_SECONDS
except TypeError:
return False

View File

@@ -13,6 +13,7 @@ class ReportCreate(CamelModel):
include_timeline: bool = True
include_reasons: bool = True
include_inquiry: bool = False
include_record_qa: bool = False
class ReportOut(CamelModel):

View File

@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
async def assess_case(case_id: UUID, db: AsyncSession) -> list[FraudAssessment]:
"""Run rule-based assessment on all non-duplicate transactions and replace old results."""
"""Run rule-based assessment and replace old results."""
# Replace mode: rerun analysis for the same case should overwrite prior assessments
# instead of appending duplicated rows.
await db.execute(
@@ -26,23 +26,40 @@ async def assess_case(case_id: UUID, db: AsyncSession) -> list[FraudAssessment]:
result = await db.execute(
select(TransactionRecord)
.where(TransactionRecord.case_id == case_id)
.where(TransactionRecord.is_duplicate.is_(False))
.order_by(TransactionRecord.trade_time.asc())
)
transactions = list(result.scalars().all())
assessments: list[FraudAssessment] = []
for tx in transactions:
level, reason, exclude_reason = classify_transaction(tx)
if tx.is_duplicate:
level = ConfidenceLevel.low
reason = "该笔交易在交易归并中已标记为重复记录,属于同一笔交易的重复展示。"
exclude_reason = "交易归并已判定重复(订单号一致或人工标记为重复),不重复计入被骗金额。"
review_status = ReviewStatus.rejected
assessed_amount = 0
elif tx.is_transit:
level = ConfidenceLevel.low
reason = (
f"该笔交易在交易归并中已标记为中转({tx.source_app.value} -> {tx.counterparty_name}"
"属于本人账户间资金流转。"
)
exclude_reason = "交易归并已判定中转(本人账户间互转),不直接计入被骗金额。"
review_status = ReviewStatus.rejected
assessed_amount = 0
else:
level, reason, exclude_reason = classify_transaction(tx)
review_status = ReviewStatus.pending
assessed_amount = float(tx.amount) if level != ConfidenceLevel.low else 0
fa = FraudAssessment(
case_id=case_id,
transaction_id=tx.id,
confidence_level=level,
assessed_amount=float(tx.amount) if level != ConfidenceLevel.low else 0,
assessed_amount=assessed_amount,
reason=reason,
exclude_reason=exclude_reason,
review_status=ReviewStatus.pending,
review_status=review_status,
)
db.add(fa)
assessments.append(fa)

View File

@@ -102,6 +102,27 @@ def _mask_text(value: str, keep: int = 12) -> str:
return f"{text[:keep]}..."
def _build_record_qa_draft(transactions: list[TransactionRecord], assessment_by_tx: dict) -> str:
candidates = [tx for tx in transactions if not tx.is_duplicate]
if not candidates:
return "问:具体的转账信息?\n答:目前暂无可确认的转账信息。"
candidates = sorted(candidates, key=lambda x: x.trade_time or datetime.min)
lines: list[str] = []
for idx, tx in enumerate(candidates, 1):
fa = assessment_by_tx.get(tx.id)
status = fa.review_status.value if fa else "pending"
cp_account = f",对方账号{tx.counterparty_account}" if tx.counterparty_account else ""
order_no = f",订单号{tx.order_no}" if tx.order_no else ""
remark = f",备注“{tx.remark}" if tx.remark else ""
lines.append(
f"{idx}笔是{_fmt_dt(tx.trade_time, with_seconds=True)}通过{tx.source_app.value}"
f"{'转出' if tx.direction.value == 'out' else '转入'}人民币{float(tx.amount):,.2f}元至{tx.counterparty_name}"
f"{cp_account}{order_no}{remark},当前认定状态为{status}"
)
return "问:具体的转账信息?\n答:" + "".join(lines)
async def _build_report_dataset(case_id: UUID, db: AsyncSession) -> dict:
case_obj = await db.get(Case, case_id)
transactions = await _get_all_transactions(case_id, db)
@@ -245,6 +266,14 @@ async def _gen_excel(case_id: UUID, report_dir: Path, body: ReportCreate, db: As
ws4.append([i, s])
sheet_created = True
if body.include_record_qa:
ws5 = wb.active if not sheet_created else wb.create_sheet()
ws5.title = "笔录问答草稿"
ws5.append(["问答内容"])
dataset = await _build_report_dataset(case_id, db)
ws5.append([_build_record_qa_draft(dataset["transactions"], dataset["assessment_by_tx"])])
sheet_created = True
if not sheet_created:
ws = wb.active
ws.title = "报告"
@@ -482,6 +511,15 @@ async def _gen_pdf(case_id: UUID, report_dir: Path, body: ReportCreate, db: Asyn
elements.append(Paragraph(f"{i}. {s}", normal_style))
elements.append(Spacer(1, 8 * mm))
# Section 9: record QA draft
if body.include_record_qa:
elements.append(Paragraph("笔录问答草稿", h2_style))
elements.append(Spacer(1, 4 * mm))
qa_text = _build_record_qa_draft(transactions, assessment_by_tx)
for line in qa_text.split("\n"):
elements.append(Paragraph(line, normal_style))
elements.append(Spacer(1, 8 * mm))
if not elements or len(elements) <= 2:
elements.append(Paragraph("未选择任何报告内容。", normal_style))