Files
shaguabijia-app-server/app/models/meituan_coupon.py
T
2026-06-23 21:09:10 +08:00

98 lines
5.1 KiB
Python

"""美团 CPS 券本地缓存表(meituan_coupon)。
把美团联盟 CPS 的券定时抓进本地库,供「销量 / 佣金排序」等查询**直接从库里捞、本地排序**,
不再每次实时打美团接口——美团搜索/供给对销量排序支持差(实测乱序)、且有 402 限流和召回上限。
数据来源(source,北京试点):
- search_waimai : 到家/外卖, 搜索关键词「外卖」, 翻到尽头
- search_meishi : 到家/外卖, 搜索关键词「美食」, 翻到尽头
- store_supply : 到店, 多业务线供给(到餐+到综+酒店+门票), 翻到尽头
每 5min(本地测试)/ 1h(线上)全量抓一次,按 (source, product_view_sign) upsert 存最新态,
last_seen 每轮刷新(可据此清理长期未再出现的陈旧券)。
去重说明:`product_view_sign` / `sku_view_id` 都是「按召回渠道生成」的,跨渠道(搜索 vs 供给)
会变,**不能当商品全局唯一 id**。因此:
- 存储:按 (source, product_view_sign) upsert(同源短周期内 sign 稳定)。
- 查询:用 `dedup_key = md5(brand|name|price)` 跨源去重,再按 `sale_volume_num` / `commission_percent` 排序。
价格统一存「分」(cents),与 price_report / comparison_record 一致。`raw` 保留整条原始返回
(字段越详细越好,避免后续漏字段还要重抓)。
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import JSON, DateTime, Float, Integer, String, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class MeituanCoupon(Base):
__tablename__ = "meituan_coupon"
__table_args__ = (
UniqueConstraint("source", "product_view_sign", name="uq_meituan_coupon_source_sign"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# ===== 来源 / 分类 =====
# source: search_waimai | search_meishi | store_supply
source: Mapped[str] = mapped_column(String(16), index=True, nullable=False)
platform: Mapped[int] = mapped_column(Integer, nullable=False) # 1 到家/外卖, 2 到店
biz_line: Mapped[int | None] = mapped_column(Integer, nullable=True) # 到店: 1到餐 2到综 3酒店 4门票
city_id: Mapped[str] = mapped_column(String(32), index=True, nullable=False)
# ===== 召回 id(按渠道生成,跨渠道会变;product_view_sign 用于换推广链) =====
product_view_sign: Mapped[str] = mapped_column(String(128), nullable=False)
sku_view_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
# ===== 商品本体 =====
name: Mapped[str | None] = mapped_column(String(256), nullable=True)
brand_name: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
sell_price_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
original_price_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
head_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
# 头图字节大小 / MIME 类型:采集时 HEAD head_url 得到(Content-Length / Content-Type)。
# 读取侧已统一压缩(不再按大小判定),这两列保留用于分析/监控(GIF 占比、体积分布等)。
image_size: Mapped[int | None] = mapped_column(Integer, nullable=True) # 字节
image_type: Mapped[str | None] = mapped_column(String(32), nullable=True) # 如 image/jpeg
# ===== 销量(美团只给粗档位:热销1w+;num=排序用的下界数值,如 1w+ → 10000) =====
sale_volume: Mapped[str | None] = mapped_column(String(32), nullable=True)
sale_volume_num: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True)
# ===== 佣金(percent 如 1.4 表示 1.4%;amount 为分) =====
commission_percent: Mapped[float | None] = mapped_column(Float, index=True, nullable=True)
commission_amount_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
# ===== 门店 / 距离 =====
poi_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
available_poi_num: Mapped[int | None] = mapped_column(Integer, nullable=True)
delivery_distance_m: Mapped[float | None] = mapped_column(Float, nullable=True)
# ===== 跨源去重键 + 原始返回 =====
dedup_key: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
raw: Mapped[dict] = mapped_column(
JSON().with_variant(JSONB(), "postgresql"), nullable=False, default=dict
)
# ===== 抓取时间窗 =====
first_seen: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
last_seen: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
def __repr__(self) -> str: # pragma: no cover
return (
f"<MeituanCoupon id={self.id} source={self.source} "
f"name={self.name!r} sale={self.sale_volume} comm={self.commission_percent}>"
)