003fd9d986
compare.py 加 /trace/finalize 透传(按 trace_id 一致性 hash 落同一 pricebot 进程);compare_record.py list_records 加 include_trace,对本人记录在调试态(客户端 agent 调试模式)放行 trace_url。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
"""比价记录 endpoint(「我的比价记录」数据源)。
|
|
|
|
路由前缀 `/api/v1/compare`:
|
|
POST /record 上报一次比价结果(幂等:同 user+trace_id 覆盖)
|
|
GET /records 比价记录列表(游标分页)
|
|
GET /records/{id} 单条详情(含 raw_payload 全量)
|
|
|
|
**均需鉴权**(CurrentUser)——与同文件无关的不鉴权透传 `compare.py` 分开:那个是
|
|
转发壳(MVP 不鉴权),这里是按用户维度落库的业务接口,必须有 user_id。
|
|
|
|
注:本轮只做 server 端,客户端(android 仓)在 done 帧后调 POST /record 上报的改动
|
|
另起一轮(见 app-server docs/待办与技术债.md P1)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
|
|
|
from app.api.deps import CurrentUser, DbSession
|
|
from app.repositories import comparison as crud_compare
|
|
from app.schemas.compare_record import (
|
|
CompareStatsOut,
|
|
ComparisonRecordCreatedOut,
|
|
ComparisonRecordDetailOut,
|
|
ComparisonRecordIn,
|
|
ComparisonRecordPage,
|
|
ComparisonRecordOut,
|
|
)
|
|
|
|
logger = logging.getLogger("shagua.compare_record")
|
|
|
|
router = APIRouter(prefix="/api/v1/compare", tags=["compare-record"])
|
|
|
|
|
|
@router.post(
|
|
"/record",
|
|
response_model=ComparisonRecordCreatedOut,
|
|
summary="上报一次比价结果(幂等)",
|
|
)
|
|
def report_record(
|
|
payload: ComparisonRecordIn, user: CurrentUser, db: DbSession
|
|
) -> ComparisonRecordCreatedOut:
|
|
rec = crud_compare.upsert_record(db, user_id=user.id, payload=payload)
|
|
logger.info(
|
|
"compare record user=%s trace=%s biz=%s status=%s saved=%s",
|
|
user.id,
|
|
rec.trace_id,
|
|
rec.business_type,
|
|
rec.status,
|
|
rec.saved_amount_cents,
|
|
)
|
|
return ComparisonRecordCreatedOut(id=rec.id)
|
|
|
|
|
|
@router.get(
|
|
"/stats",
|
|
response_model=CompareStatsOut,
|
|
summary="比价口径战绩(完成比价数 + 累计发现可省)",
|
|
)
|
|
def stats(user: CurrentUser, db: DbSession) -> CompareStatsOut:
|
|
count, saved = crud_compare.get_stats(db, user.id)
|
|
return CompareStatsOut(compare_count=count, discovered_saved_cents=saved)
|
|
|
|
|
|
@router.get(
|
|
"/records",
|
|
response_model=ComparisonRecordPage,
|
|
summary="比价记录列表(游标分页)",
|
|
)
|
|
def list_records(
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
limit: int = Query(20, ge=1, le=100),
|
|
cursor: int | None = Query(None, description="上一页末条 id"),
|
|
include_trace: bool = Query(
|
|
False,
|
|
description="客户端开了本机 agent 调试模式时带 true,放行本人记录的 trace_url",
|
|
),
|
|
) -> ComparisonRecordPage:
|
|
items, next_cursor = crud_compare.list_records(
|
|
db, user.id, limit=limit, cursor=cursor
|
|
)
|
|
outs = [ComparisonRecordOut.model_validate(it) for it in items]
|
|
# 权限闸:未开 debug_trace_enabled 的用户不下发 trace_url(列表页「复制调试链接」靠它)。
|
|
# include_trace=true 例外:客户端开了本机 agent 调试模式时带上,放行**本人记录**的 trace_url
|
|
# ——list_records 只查 user.id 自己的记录,给本人看自己的调试链接无越权,与实时结果页
|
|
# CompResultScreen「debug 权限 OR 本机 agent 调试」同口径(领导 2026-06-12 拍板)。
|
|
if not (user.debug_trace_enabled or include_trace):
|
|
for o in outs:
|
|
o.trace_url = None
|
|
return ComparisonRecordPage(items=outs, next_cursor=next_cursor)
|
|
|
|
|
|
@router.get(
|
|
"/records/{record_id}",
|
|
response_model=ComparisonRecordDetailOut,
|
|
summary="比价记录详情(含 raw_payload)",
|
|
)
|
|
def get_record(
|
|
record_id: int, user: CurrentUser, db: DbSession
|
|
) -> ComparisonRecordDetailOut:
|
|
rec = crud_compare.get_record(db, user.id, record_id)
|
|
if rec is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="record not found")
|
|
out = ComparisonRecordDetailOut.model_validate(rec)
|
|
# 权限闸:未开 debug_trace_enabled 的用户不下发 trace_url。
|
|
# ⚠️ raw_payload 是上报体全量(model_dump),里面也藏着一份 trace_url,必须一并抹掉——
|
|
# 否则无权限用户从详情接口的 raw_payload 绕过权限闸拿到 trace_url。
|
|
if not user.debug_trace_enabled:
|
|
out.trace_url = None
|
|
if isinstance(out.raw_payload, dict):
|
|
out.raw_payload.pop("trace_url", None)
|
|
return out
|