b995ee8ada
排序策略 (app/core/ranking.py 新建): - parse_sale_volume: "热销8.5万+" → 85000 整数销量 - get_distance_km: 距离统一(到店米 ÷1000), >50km 视为脏数据 - filter_items: 售价≤0 / 距离>8km / 脏数据剔除 - sort_by_sales: 按销量降序 - split_pages + shuffle_pages: 切页 + 页内 Fisher-Yates 打乱 - inject_billboard: 榜单商品 4 条/页注入前 5 页, 随机位置 - merge_category_pages: 外卖第K页 + 到店第K页合并后 shuffle Feed 接口重写 (app/api/v1/meituan.py): - 旧逻辑: 三页固定 listTopiId 主题轮, 美团给什么序就什么序 - 新逻辑: searchText="外卖"/"到店餐饮" + sortField=6 翻页 LBS 召回, 4 任务并发(外卖召回 / 到店召回 / 外卖榜单 / 到店榜单), 走完 ranking pipeline 后做坐标级内存缓存(5min TTL) - 翻页上限 5 页/品类(性价比最高: 3.4s 拿到 238 条/品类) - 直接调 _call 而非 query_coupon, 因为 searchId 翻页时不能传 pageNo 标签格式化 (app/schemas/meituan.py): - price_label: "比日常团购省3.5元" → "比团购省 3.5 元" - rank_label: "2小时北京外卖销量榜第1名" → "外卖榜第 1" - rating_label: "4.6分" → "点评 4.6 分" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
4.1 KiB
Python
144 lines
4.1 KiB
Python
"""首页 Feed 排序策略 (MVP 版)
|
|
|
|
LBS 召回 → 距离过滤 → 销量重排 → 分页 shuffle → 榜单加成 → 双品类同页交错。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
def parse_sale_volume(text: str | None) -> int:
|
|
"""'热销8.5万+' → 85000, '热销1k+' → 1000, None → 0"""
|
|
if not text:
|
|
return 0
|
|
m = re.search(r"([\d.]+)\s*(万|k)?", text, re.IGNORECASE)
|
|
if not m:
|
|
return 0
|
|
num = float(m.group(1))
|
|
unit = (m.group(2) or "").lower()
|
|
if unit == "万":
|
|
num *= 10000
|
|
elif unit == "k":
|
|
num *= 1000
|
|
return int(num)
|
|
|
|
|
|
def get_distance_km(item: dict[str, Any], is_daodian: bool) -> float | None:
|
|
"""提取距离(km)。外卖单位千米,到店单位米需÷1000。>50km 视为脏数据返回 None。"""
|
|
raw = (item.get("deliverablePoiInfo") or {}).get("deliveryDistance")
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
d = float(raw)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
if is_daodian:
|
|
d /= 1000
|
|
if d > 50:
|
|
return None
|
|
return d
|
|
|
|
|
|
def get_sell_price(item: dict[str, Any]) -> float | None:
|
|
raw = (item.get("couponPackDetail") or {}).get("sellPrice")
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
p = float(raw)
|
|
return p if p > 0 else None
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
def get_product_sign(item: dict[str, Any]) -> str:
|
|
cpd = item.get("couponPackDetail") or {}
|
|
return cpd.get("productViewSign") or cpd.get("skuViewId") or ""
|
|
|
|
|
|
def filter_items(
|
|
items: list[dict], is_daodian: bool, max_km: float = 8.0,
|
|
) -> list[dict]:
|
|
"""距离 ≤ max_km、售价 > 0、去脏数据。"""
|
|
result = []
|
|
for item in items:
|
|
if get_sell_price(item) is None:
|
|
continue
|
|
dist = get_distance_km(item, is_daodian)
|
|
if dist is None or dist > max_km:
|
|
continue
|
|
result.append(item)
|
|
return result
|
|
|
|
|
|
def dedup(items: list[dict]) -> list[dict]:
|
|
seen: set[str] = set()
|
|
result = []
|
|
for item in items:
|
|
sign = get_product_sign(item)
|
|
if sign and sign not in seen:
|
|
seen.add(sign)
|
|
result.append(item)
|
|
return result
|
|
|
|
|
|
def sort_by_sales(items: list[dict]) -> list[dict]:
|
|
def _key(item: dict) -> int:
|
|
vol = (item.get("couponPackDetail") or {}).get("saleVolume")
|
|
return parse_sale_volume(vol)
|
|
return sorted(items, key=_key, reverse=True)
|
|
|
|
|
|
def split_pages(items: list[dict], page_size: int = 20) -> list[list[dict]]:
|
|
if not items:
|
|
return []
|
|
return [items[i : i + page_size] for i in range(0, len(items), page_size)]
|
|
|
|
|
|
def shuffle_pages(pages: list[list[dict]]) -> list[list[dict]]:
|
|
"""每页内 Fisher-Yates shuffle,不跨页。"""
|
|
result = []
|
|
for page in pages:
|
|
shuffled = page[:]
|
|
random.shuffle(shuffled)
|
|
result.append(shuffled)
|
|
return result
|
|
|
|
|
|
def inject_billboard(
|
|
pages: list[list[dict]],
|
|
billboard: list[dict],
|
|
per_page: int = 4,
|
|
max_inject_pages: int = 5,
|
|
) -> list[list[dict]]:
|
|
"""将榜单商品分配到前 N 页,每页额外加 per_page 个,随机位置插入。"""
|
|
existing = {get_product_sign(it) for p in pages for it in p}
|
|
unique = [it for it in billboard if get_product_sign(it) not in existing]
|
|
|
|
idx = 0
|
|
for i in range(min(max_inject_pages, len(pages))):
|
|
batch = unique[idx : idx + per_page]
|
|
idx += per_page
|
|
for it in batch:
|
|
pages[i].insert(random.randint(0, len(pages[i])), it)
|
|
return pages
|
|
|
|
|
|
def merge_category_pages(
|
|
waimai_pages: list[list[dict]],
|
|
daodian_pages: list[list[dict]],
|
|
) -> list[list[dict]]:
|
|
"""同页合并 + shuffle,页数取两者最大值。"""
|
|
n = max(len(waimai_pages), len(daodian_pages))
|
|
result = []
|
|
for k in range(n):
|
|
merged: list[dict] = []
|
|
if k < len(waimai_pages):
|
|
merged.extend(waimai_pages[k])
|
|
if k < len(daodian_pages):
|
|
merged.extend(daodian_pages[k])
|
|
random.shuffle(merged)
|
|
result.append(merged)
|
|
return result
|