# POST /api/v1/compare/record — 上报一次比价结果(幂等) > 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md) 比价 `done` 帧后上报一条比价结果,落 `comparison_record` 表,作为「我的比价记录」数据源 + 用户级行为画像。 > ⚠️ **灰度定位(2026-07)**:写 `comparison_record` 现以透传壳 `compare.py` 的**后端 harvest** 为主(帧0 建 `running` 行 → done/finalize 落终态,新客户端不再 POST)。本接口降级为**老客户端兼容**通道,与 harvest 按 `trace_id` reconcile(**success 不被降级**);新版覆盖够高后可下线本 POST。 > 与软鉴权透传端点 [`/api/v1/price/step`](./compare-price-step.md) 不同:那是转发壳(顺带 harvest 落库),本接口按用户维度显式上报,**必须鉴权**。 ## 入参(JSON body) 字段刻意对齐 pricebot 协议([02_api_protocol.md](../../../pricebot-backend/docs/main/02_api_protocol.md) 的 `calibration` + `done.params`),客户端把 Phase1 的 calibration 和 done 帧 params **零翻译**映射上来即可。 | 字段 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `trace_id` | string | ✅ | — | 一次比价的唯一标识(app-server 帧0 签发)。**幂等键(trace_id 单列唯一)**:同 trace_id 重复上报覆盖、返回同一 id;与 harvest 行按它 reconcile | | `business_type` | string | ❌ | `food` | `food`(外卖,当前唯一接通) / `ecom`(电商) / `coupon`(领券) | | `device_id` | string \| null | ❌ | null | 设备号 | | `store_name` | string \| null | ❌ | null | 店铺名(外卖,来自 calibration.result) | | `source_platform_id` | string \| null | ❌ | null | 源平台代号,如 `taobao_flash` | | `source_platform_name` | string \| null | ❌ | null | 源平台中文名 | | `source_package` | string \| null | ❌ | null | 源平台 Android 包名 | | `source_price` | float \| null | ❌ | null | 源平台到手价(**元**) | | `items` | Item[] | ❌ | [] | 下单菜品,`{name, qty, specs?}` | | `comparison_results` | Result[] | ❌ | [] | 逐平台对比,见下表(price 单位**元**) | | `skipped_dish_count` | int \| null | ❌ | null | 目标平台未找到、跳过的菜品数 | | `skipped_dish_names` | string[] | ❌ | [] | 被跳过的菜名 | | `total_dish_count` | int \| null | ❌ | null | 原始菜品总数 | | `information` | string \| null | ❌ | null | done 帧文案(成功摘要 / 失败具体原因),持久化为列并在列表/详情返回,前端失败时当原因展示 | | `status` | string \| null | ❌ | null | `success` / `failed`。**不传则服务端派生** | **Item**:`{ name: string, qty: int=1, specs: string[]\|null }` **Result(comparison_results 元素)**:`{ platform_id, platform_name, package, price(元,float\|null), is_source(bool), rank(int\|null), coupon_saved(元,float\|null), coupon_name(string\|null) }` - `coupon_saved`:该平台本单**平台主优惠额**(元)——美团红包 / 淘宝平台红包 / 京东优惠券·百亿补贴,**只取那一笔**,不含配送费减免/共减总额。仅外卖目标平台带值,源平台/没用为 `null`,前端记录页据此展示**「已优惠 ¥X」**(null 不展示)。 - `coupon_name`:优惠**来源名**(展示用,best-effort)——美团「外卖大额神券」/ 京东「百亿补贴」/ 淘宝「平台红包」。仅在该行有 `coupon_saved` 时带;`null` 时前端走通用「红包」。非用户选中的具体券名(选中态不可读)。 ## 服务端派生(客户端不用算) 从 `comparison_results` 派生并落结构化列: - `best_*`:取最便宜的一家(按 `rank` 升序、再按 `price`),写 `best_platform_id/name/best_price_cents` - `source_price_cents`:优先用 `source_price`,缺失则取 `comparison_results` 里 `is_source=true` 行的价 - `saved_amount_cents` = `源价 − 最优价`(可为 0 / 负:源平台本来就最便宜) - `is_source_best`:最便宜的一家是否为源平台(= 这次没省到) - `status`(未显式传时):存在「非源且有价」的结果 → `success`,否则 → `failed` - 金额一律 `round(元 × 100)` 存**分** `comparison_results` / `items` / `skipped_dish_names` 原样存 JSON;完整上报 body 另存 `raw_payload`(详情接口可取回)。 ## 出参 响应 `200`:`{ "id": int }`(写入或已存在记录的 id) ## 错误 - `401` 未鉴权 - `422` 缺 `trace_id` 或字段类型不符 ## 说明 失败的比价(done 帧只有源/空 comparison_results)也会落一条 `status=failed`——「越详细越好」,是否在 UI 展示由前端过滤。