"""比价记录上报 / 读取 schemas。 约定同 welfare:字段 snake_case、金额存整数(分)、时间 ISO 8601。 上报请求(ComparisonRecordIn)的字段刻意对齐 pricebot 协议 (docs/main/02_api_protocol.md 的 calibration + done.params.comparison_results), 让客户端把 Phase1 的 calibration 和 done 帧的 params 字段**零翻译**直接映射上来, server 端负责拆成结构化列(best_*/saved/is_source_best 由 comparison_results 派生)。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, Field # ===== 上报请求 ===== class ComparisonItemIn(BaseModel): """下单菜品(来自 calibration.items)。""" name: str qty: int = 1 specs: list[str] | None = None class ComparisonResultIn(BaseModel): """逐平台对比项(来自 done.params.comparison_results)。price 单位:元。""" platform_id: str | None = None platform_name: str | None = None package: str | None = None price: float | None = None is_source: bool = False rank: int | None = None class ComparisonRecordIn(BaseModel): # 幂等键:同一用户同一 trace_id 重复上报只保留一条(覆盖) trace_id: str = Field(..., min_length=1, description="pricebot 侧 trace_id") business_type: str = Field("food", description="food / ecom / coupon") device_id: str | None = None # 源平台(来自 calibration) store_name: str | None = Field(None, description="店铺名(外卖)") source_platform_id: str | None = None source_platform_name: str | None = None source_package: str | None = None source_price: float | None = Field(None, description="源平台到手价(元)") # 明细 items: list[ComparisonItemIn] = Field(default_factory=list) comparison_results: list[ComparisonResultIn] = Field(default_factory=list) skipped_dish_count: int | None = None skipped_dish_names: list[str] = Field(default_factory=list) total_dish_count: int | None = None information: str | None = Field(None, description="done 帧文案,留存备查") # 不传则服务端按 comparison_results 派生(有非源有效价=success,否则 failed) status: str | None = Field(None, description="success / failed,可不传由服务端派生") # ===== 读取出参 ===== class ComparisonRecordOut(BaseModel): """列表项:结构化概要字段(不含 raw_payload,减小列表 payload)。""" model_config = ConfigDict(from_attributes=True) id: int business_type: str trace_id: str source_platform_id: str | None = None source_platform_name: str | None = None source_package: str | None = None source_price_cents: int | None = None best_platform_id: str | None = None best_platform_name: str | None = None best_price_cents: int | None = None saved_amount_cents: int | None = None is_source_best: bool | None = None store_name: str | None = None total_dish_count: int | None = None skipped_dish_count: int | None = None status: str information: str | None = None items: list = [] comparison_results: list = [] skipped_dish_names: list = [] created_at: datetime class ComparisonRecordDetailOut(ComparisonRecordOut): """详情:在概要之上额外带 raw_payload 全量。""" raw_payload: dict | None = None class ComparisonRecordPage(BaseModel): items: list[ComparisonRecordOut] next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底") class ComparisonRecordCreatedOut(BaseModel): id: int = Field(..., description="写入(或已存在)的记录 id") # ===== 比价战绩里程碑(福利页「记录比价战绩」)===== class MilestoneStateOut(BaseModel): milestone: int = Field(..., description="档位序号 1-based(= 解锁所需的成功比价次数)") coin: int = Field(..., description="该档奖励金币") state: str = Field(..., description="claimed(已领) / active(可领) / locked(未解锁)") class MilestoneStatusOut(BaseModel): success_count: int = Field(..., description="累计成功比价次数(解锁进度)") claimable_count: int = Field(..., description="当前可领(active)档数") milestones: list[MilestoneStateOut] class MilestoneClaimResultOut(BaseModel): milestone: int = Field(..., description="本次领取的档位序号") coin_awarded: int = Field(..., description="本次发放金币") coin_balance: int = Field(..., description="领奖后金币余额")