15dcfa604b
- comparison_record 表 + 上报(幂等 user+trace)/列表/详情 接口;服务端派生 best/saved/is_source_best/status + information(done 帧文案/失败具体原因) - 战绩里程碑:comparison_milestone_claim 表 + 进度/领取接口,按成功比价次数 解锁,复用 grant_coins 真发金币(RECORD_MILESTONES 120/180/300/500/800/1200) - 迁移 record/information/milestone 三个,单 head;rewards.py 集中档位配置 - 测试 test_compare_record(8)+test_compare_milestone(7) 全绿;docs/api 12a-12e + 索引 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
3.7 KiB
Markdown
58 lines
3.7 KiB
Markdown
# POST /api/v1/compare/record — 上报一次比价结果(幂等)
|
||
|
||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||
|
||
比价 `done` 帧后,客户端用**带 JWT 的通道**上报一条比价结果,落 `comparison_record` 表,作为「我的比价记录」数据源 + 用户级行为画像。
|
||
|
||
> ⚠️ 与不鉴权的透传端点 [`/api/v1/price/step`](./compare-price-step.md) 不同:那是转发壳,本接口按用户维度落库,**必须鉴权**。
|
||
> 本轮只做 server 端;客户端在 done 帧后调本接口的改动另起一轮(见 [待办与技术债.md](../待办与技术债.md) P1)。
|
||
|
||
## 入参(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 | ✅ | — | pricebot 侧 trace_id。**幂等键**:同用户同 trace_id 重复上报覆盖、返回同一 id |
|
||
| `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) }`
|
||
|
||
## 服务端派生(客户端不用算)
|
||
|
||
从 `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 展示由前端过滤。
|