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>
6.1 KiB
6.1 KiB
meituan_coupon — 美团 CPS 券本地缓存
模型
app/models/meituan_coupon.py· ETLscripts/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(PGON CONFLICT DO UPDATE),last_seen每轮刷新。 - R(读):
- rec(智能推荐):
WHERE commission_percent>=3.0→DISTINCT ON(dedup_key)取佣金最高 → 按sale_volume_num降序分页。 - sales(销量最高):
WHERE sale_volume_num IS NOT NULL→DISTINCT ON(dedup_key)取销量最高 → 按销量降序分页。
- rec(智能推荐):
- 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:
source、city_id、brand_name、sale_volume_num、commission_percent、dedup_key、last_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 接口报错。prodDATABASE_URL必须postgresql+psycopg://。 dedup_key不含城市:md5(brand|name|price),单城没问题;将来多城会把异城同品合并,需把city_id纳入 key。product_view_sign跨渠道会变,不能当商品全局唯一 id:存储按(source, sign)、查询按dedup_key。