e1bd0e3ef7
改了什么:新增新版大盘日期窗口聚合、广告收益拆分、比价耗时字段、美团 CPS 拉单入库与大盘展示,并同步反馈审核和邀请奖励下线口径。 验证:python -m pytest tests/test_admin_read.py tests/test_admin_write.py tests/test_compare_record.py tests/test_invite.py tests/test_feedback.py -q。
153 lines
6.6 KiB
Python
153 lines
6.6 KiB
Python
"""比价记录上报 / 读取 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
|
|
# 该平台本单用红包省的**纯红包优惠额**(元, 不含配送费/代金券/满减)。None=没用/没抠到。
|
|
# 必须显式声明: 落库走 model_dump(), pydantic 默认丢未知字段, 不声明这行会被悄悄吞掉。
|
|
# 各平台抠到红包即带值(2026-06 起源平台 Phase1 意图识别也抠, 当前仅淘宝源)。见 pricebot 侧 比价红包额留痕-实现方案.md。
|
|
coupon_saved: float | None = None
|
|
# 优惠**来源名**(展示用, best-effort): 美团"外卖大额神券"/京东"百亿补贴"/淘宝"平台红包"。
|
|
# None=没抠到 → 前端走通用"红包"。同样必须显式声明否则上报边界被 pydantic 静默丢弃(pricebot#38 引入)。
|
|
coupon_name: str | 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,可不传由服务端派生")
|
|
# 最优平台商家/商品深链(客户端从 collectedLinks[best_index] 取);「再次比价」直达用
|
|
best_deeplink: str | None = Field(None, description="最优平台深链,再次比价直达")
|
|
# pricebot done.params.trace_url 原样上报,落库供记录页「复制调试链接」(dir 名含落盘
|
|
# 时分秒前端拼不出,必须由后端透传)。
|
|
trace_url: str | None = Field(None, description="本次比价公网调试链接")
|
|
total_ms: int | None = Field(None, description="整场比价墙钟耗时(ms)")
|
|
|
|
|
|
# ===== 读取出参 =====
|
|
|
|
class ComparisonRecordOut(BaseModel):
|
|
"""列表项:结构化概要字段(不含 raw_payload,减小列表 payload)。"""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
business_type: str
|
|
trace_id: str
|
|
# 公网调试链接;仅当 user.debug_trace_enabled 时由端点填充,否则端点层置 None(权限闸)。
|
|
trace_url: str | None = None
|
|
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
|
|
best_deeplink: str | 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 = []
|
|
total_ms: int | None = None
|
|
# 「已下单」(店级):该店名在该用户真实下单(source='compare')里出现过即 True。
|
|
# 由 list_records 动态算出挂在 ORM 实例上(非 DB 列),from_attributes 读出;缺省 False。
|
|
ordered: bool = False
|
|
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 CompareStatsOut(BaseModel):
|
|
"""「我的」页省钱战绩卡(比价口径)聚合。"""
|
|
|
|
compare_count: int = Field(..., description="累计完成比价次数(status='success' 记录数)")
|
|
discovered_saved_cents: int = Field(..., description="累计发现可省额(分;各成功比价 源价−最低价 之和)")
|
|
|
|
|
|
# ===== 比价战绩里程碑(福利页「记录比价战绩」)=====
|
|
|
|
class MilestoneStateOut(BaseModel):
|
|
milestone: int = Field(..., description="档位序号 1-based(= 解锁所需的成功比价次数)")
|
|
coin: int = Field(..., description="该档应发金币额(产品规则值);当前领取暂不真发, 见 compare-milestone-claim 文档")
|
|
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="领奖后金币余额")
|