From 3cab3b90556856234179cf75b039ea91757bc736 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 17 Jun 2026 23:32:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(compare-record):=20admin=20=E6=94=BE?= =?UTF-8?q?=E8=A1=8C=20cancelled=20=E7=AD=9B=E9=80=89=20+=20llm=5Fcalls=20?= =?UTF-8?q?=E6=8B=89=E5=8F=96=E6=94=B9=E5=90=8E=E5=8F=B0=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin 列表 status pattern 加 cancelled(客户端会上报用户终止比价) - 上报端点拉 pricebot llm_calls 改 BackgroundTasks 异步回填, 不阻塞上报响应(独立 session) Co-Authored-By: Claude Opus 4.8 --- app/admin/routers/comparison.py | 2 +- app/api/v1/compare_record.py | 46 ++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/admin/routers/comparison.py b/app/admin/routers/comparison.py index c9b0ccb..9aeb859 100644 --- a/app/admin/routers/comparison.py +++ b/app/admin/routers/comparison.py @@ -30,7 +30,7 @@ def list_comparison_records( db: AdminDb, user_id: Annotated[int | None, Query()] = None, phone: Annotated[str | None, Query(description="手机号前缀")] = None, - status: Annotated[str | None, Query(pattern="^(success|failed)$")] = None, + status: Annotated[str | None, Query(pattern="^(success|failed|cancelled)$")] = None, business_type: Annotated[str | None, Query()] = None, limit: Annotated[int, Query(ge=1, le=100)] = 20, cursor: Annotated[int | None, Query()] = None, diff --git a/app/api/v1/compare_record.py b/app/api/v1/compare_record.py index 14286d7..d4b1a03 100644 --- a/app/api/v1/compare_record.py +++ b/app/api/v1/compare_record.py @@ -15,9 +15,11 @@ from __future__ import annotations import logging -from fastapi import APIRouter, HTTPException, Query, status +from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, status from app.api.deps import CurrentUser, DbSession +from app.db.session import SessionLocal +from app.models.comparison import ComparisonRecord from app.repositories import comparison as crud_compare from app.services.pricebot_llm_calls import fetch_llm_calls from app.schemas.compare_record import ( @@ -40,29 +42,49 @@ router = APIRouter(prefix="/api/v1/compare", tags=["compare-record"]) summary="上报一次比价结果(幂等)", ) def report_record( - payload: ComparisonRecordIn, user: CurrentUser, db: DbSession + payload: ComparisonRecordIn, + user: CurrentUser, + db: DbSession, + background_tasks: BackgroundTasks, ) -> ComparisonRecordCreatedOut: rec = crud_compare.upsert_record(db, user_id=user.id, payload=payload) - # 同机拉 pricebot 本次比价的 LLM 调用明细落库(best-effort,失败不阻断上报)。 - # llm_call_count/retry_count 直接由明细派生:次数=条数,重试=带 error 的条数。 - calls = fetch_llm_calls(rec.trace_id) - if calls: - rec.llm_calls = calls - rec.llm_call_count = len(calls) - rec.retry_count = sum(1 for c in calls if c.get("error")) - db.commit() + # LLM 调用明细异步回填:同机拉 pricebot llm_calls 最长 5s 且是 best-effort,放后台 + # 任务做,不阻塞上报响应(顺带给 pricebot 落盘留足余量)。upsert 已 commit,后台用 + # 独立 session 按 record id 回填 llm_calls + 派生 llm_call_count/retry_count。 + background_tasks.add_task(_backfill_llm_calls, rec.id, rec.trace_id) logger.info( - "compare record user=%s trace=%s biz=%s status=%s saved=%s llm_calls=%d", + "compare record user=%s trace=%s biz=%s status=%s saved=%s (llm_calls backfill queued)", user.id, rec.trace_id, rec.business_type, rec.status, rec.saved_amount_cents, - len(calls), ) return ComparisonRecordCreatedOut(id=rec.id) +def _backfill_llm_calls(record_id: int, trace_id: str) -> None: + """后台回填本次比价的 LLM 调用明细 + 派生 llm_call_count/retry_count。 + 独立 DB session(请求 session 此时已关);拉取/写库失败只 log,绝不影响已落库的上报。""" + calls = fetch_llm_calls(trace_id) + if not calls: + return + db = SessionLocal() + try: + rec = db.get(ComparisonRecord, record_id) + if rec is None: + return + rec.llm_calls = calls + rec.llm_call_count = len(calls) + rec.retry_count = sum(1 for c in calls if c.get("error")) + db.commit() + logger.info("backfill llm_calls trace=%s n=%d", trace_id, len(calls)) + except Exception as e: # noqa: BLE001 best-effort + logger.warning("backfill llm_calls failed trace=%s: %s", trace_id, e) + finally: + db.close() + + @router.get( "/stats", response_model=CompareStatsOut,