# 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 展示由前端过滤。
