Files
shaguabijia-app-server/docs/database/meituan_coupon.md
T
左辰勇 a2270ee1b2 功能:新手引导视频 + 美团券首页分页索引
新手引导视频:运营后台上传 MP4(上限 100MB,魔数校验只认 ISO BMFF),
App 端在领券等候浮层前 N 次以引导视频替代广告。新增 guide_video
的 model/schema/repository/router(App 侧 + 后台侧)与播放记录表迁移。

美团券:首页「销量最高 / 智能推荐」两个 tab 改游标分页,配套两条
(city_id, dedup_key, 排序键 DESC) 复合索引,让 Postgres 顺着索引流式
去重,免掉每翻一页重排整城券的开销。美团 CPS client 在 lifespan 预热
并在关闭时释放连接池。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 22:50:44 +08:00

6.1 KiB

meituan_coupon — 美团 CPS 券本地缓存

模型 app/models/meituan_coupon.py · ETL scripts/pull_meituan_coupons.py · 接口 meituan-feed(rec/distance)/ meituan-top-sales(sales)· ← 索引 · 总览

把美团联盟 CPS 的券定时抓进本地库,供「智能推荐(佣金≥3%)」「销量最高」从库里捞、本地去重排序,不每次实时打美团(美团对销量排序支持差、有 402 限流和召回上限)。北京单城试点。

用在哪 / 增删改查

  • C/U(ETL upsert):scripts/pull_meituan_coupons.py 每小时全量抓 3 路(search_waimai 搜「外卖」/ search_meishi 搜「美食」/ store_supply 到店多业务线供给),按 (source, product_view_sign) upsert(PG ON CONFLICT DO UPDATE),last_seen 每轮刷新。
  • R(读):
    • rec(智能推荐):WHERE commission_percent>=3.0DISTINCT ON(dedup_key) 取佣金最高 → 按 sale_volume_num 降序分页。
    • sales(销量最高):WHERE sale_volume_num IS NOT NULLDISTINCT ON(dedup_key) 取销量最高 → 按销量降序分页。
  • D(清理):见下「过期清理策略」。

字段

类型 约束 / 默认 说明
id Integer PK, autoincrement
source String(16) index, NOT NULL 召回来源:search_waimai / search_meishi / store_supply
platform Integer NOT NULL 1 外卖/到家, 2 到店
biz_line Integer nullable 到店子类:1到餐 2到综 3酒店 4门票
city_id String(32) index, NOT NULL 城市 id(当前恒为北京 WKV2HMXUEK634WP64CUCUQGM64)
product_view_sign String(128) NOT NULL 换链主键;按召回渠道生成、跨渠道会变,不能当商品全局 id
sku_view_id String(128) nullable
name String(256) nullable 商品名(解析截断 [:256])
brand_name String(128) index, nullable 品牌名([:128])
sell_price_cents Integer nullable 现价(分)
original_price_cents Integer nullable 原价(分)
head_url String(512) nullable 头图(去 @ 后缀,[:512])
sale_volume String(32) nullable 原始销量档:如「热销1w+」
sale_volume_num Integer index, nullable 销量排序数值(取下界):1w+ → 10000
commission_percent Float index, nullable 佣金比例,数值即百分数(1.4 = 1.4%);美团原值 140 ÷ 100
commission_amount_cents Integer nullable 预估佣金(分)
poi_name String(128) nullable 最近门店名([:128])
available_poi_num Integer nullable 可用门店数
delivery_distance_m Float nullable 距离(米);相对抓取时的城市默认点,对用户位置无意义,rec 接口置空不返
dedup_key String(64) index, NOT NULL 跨源去重键 = md5(brand_name|name|sell_price_cents)
raw JSON / JSONB NOT NULL, default {} 整条原始返回(避免漏字段重抓);PG 上为 JSONB
first_seen DateTime(tz) server_default now() 首次抓到
last_seen DateTime(tz) index, server_default now() 最近一轮抓到(过期清理依据)
updated_at DateTime(tz) server_default now(), onupdate now()

索引与约束

  • 唯一约束 uq_meituan_coupon_source_sign(source, product_view_sign)—— ETL upsert 的冲突目标。
  • 单列 index:sourcecity_idbrand_namesale_volume_numcommission_percentdedup_keylast_seen
  • 复合 index(meituan_coupon_feed_indexes 迁移,2026-07-23 加):
    • ix_meituan_coupon_city_dedup_sales(city_id, dedup_key, sale_volume_num DESC, commission_percent DESC)——「销量最高」。
    • ix_meituan_coupon_city_dedup_comm(city_id, dedup_key, commission_percent DESC)——「智能推荐」。
    • 列顺序对齐 WHERE city_id=? … ORDER BY dedup_key, <排序键> DESC,让 DISTINCT ON 顺着索引流式去重、免掉排序。
  • 字段核对(2026-06-10):模型 ↔ 迁移(meituan_coupon_table)↔ ETL 解析三处一致;解析截断宽度均 ≤ 列宽(name256 / brand128 / poi128 / head512 / sign128,dedup_key = md5 32 字符 ≤ 64)。
  • 索引评估修订(2026-07-23):此前判断「单城几千行 seq scan 即亚毫秒,复合索引留作放量时再加」——漏算了 raw。原查询用 select(MeituanCoupon) 做子查询,raw(JSONB,每行数 KB)被一起拖进 DISTINCT ON 与分页两轮排序,整城体量轻松推过 work_mem → 落盘外部归并,且每翻一页重来一次,是首页「滑到底越来越慢」的主因之一。现已双管齐下:接口侧改成「先在小列上排出本页 id,再按 id 回表取 raw」(api/v1/meituan.py_paged_dedup_ids),库侧补上上面两条复合索引。

过期清理策略

  • ETL 每轮(每小时)末尾按 last_seen 的 TTL 清理:DELETE WHERE last_seen < now() - prune_hours(默认 24h,--prune-hours 可调,0=不清)。
  • 仍在架的券每小时被刷新 last_seen,永不被清;只有下架 / sign 轮换的残留超 24h 宽限才删。宽限期用于吸收几次抓取失败。
  • 券自身到期无需单独处理:到期后美团不再返回 → 自然 last_seen 老化被清。
  • 护栏(2026-06-10):prune 仅在**本轮确有入库(total>0)**时执行。美团整体故障时本轮可能 0 入库(脚本不抛异常、只是抓回空),若仍照常 prune,连续故障会按 last_seen 把全表删空;加 total>0 护栏后,0 入库轮跳过清理并打日志。
  • 表只增量 upsert + 少量 delete,PG autovacuum 足以回收死元组,当前规模无需手动 VACUUM。

注意

  • 仅 PostgreSQL:ETL upsert(postgresql.insert(...).on_conflict_do_update)与读取的 DISTINCT ON 都是 PG 专用语法;库若是 sqlite,ETL 灌不进、rec/sales 接口报错。prod DATABASE_URL 必须 postgresql+psycopg://
  • dedup_key 不含城市:md5(brand|name|price),单城没问题;将来多城会把异城同品合并,需把 city_id 纳入 key。
  • product_view_sign 跨渠道会变,不能当商品全局唯一 id:存储按 (source, sign)、查询按 dedup_key