diff --git a/app/api/v1/meituan.py b/app/api/v1/meituan.py index 9d7510e..7cb1c4d 100644 --- a/app/api/v1/meituan.py +++ b/app/api/v1/meituan.py @@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, aliased from app.core.config import settings from app.db.session import get_db @@ -110,27 +110,30 @@ def feed(req: FeedRequest) -> FeedResponse: except MeituanCpsError: return [] - # 距离最近:拉齐全部榜单轮次,合并去重,后端在【完整池子】上全局按距离由近及远排,一次性返回。 - # (距离排序必须在完整池上做、不能逐页排——这正是之前放前端不合理的根因。) - if tab == "distance": + # 智能推荐 / 距离最近:拉齐全部榜单轮次(~60 条),后端处理后【一次性返回、不分页】。 + # (池子很小;逐页实时打美团反而让客户端翻页卡顿/滑不动——一次拉齐 → 前端加载一次、滚动顺畅。) + if tab in ("rec", "distance"): + wm_all: list[dict] = [] + dd_all: list[dict] = [] with ThreadPoolExecutor(max_workers=len(_TOPIC_ROUNDS) * 2) as pool: - futs = [] - for wm_topic, dd_topic in _TOPIC_ROUNDS: - futs.append(pool.submit(_fetch_topic, 1, None, wm_topic)) - futs.append(pool.submit(_fetch_topic, 2, 1, dd_topic)) - raws = [f.result() for f in futs] - seen: set[str] = set() - cards: list[CouponCard] = [] - for raw_list in raws: - for it in raw_list: - card = CouponCard.from_raw(it) - if card.product_view_sign and card.product_view_sign not in seen: - seen.add(card.product_view_sign) - cards.append(card) - cards.sort(key=lambda c: c.distance_meters if c.distance_meters is not None else float("inf")) + futs = [ + (pool.submit(_fetch_topic, 1, None, wm_topic), + pool.submit(_fetch_topic, 2, 1, dd_topic)) + for wm_topic, dd_topic in _TOPIC_ROUNDS + ] + for fw, fd in futs: + wm_all += fw.result() + dd_all += fd.result() + cards = _interleave(wm_all, dd_all) # from_raw + 去重 + 外卖:到店 2:1 交叉 + if tab == "rec": + # 智能推荐:去掉佣金率 < 3% + cards = [c for c in cards if _commission_pct(c) >= 3.0] + else: + # 距离最近:在完整池上全局按距离由近及远(无距离排最后) + cards.sort(key=lambda c: c.distance_meters if c.distance_meters is not None else float("inf")) return FeedResponse(items=cards, has_next=False, page=1) - # 智能推荐(rec,默认):沿用逐轮分页的混合 feed,后端过滤掉佣金率 < 3%。 + # 默认(老客户端不传 tab):沿用逐轮分页的混合 feed,不筛。 page_idx = req.page - 1 if page_idx >= len(_TOPIC_ROUNDS): return FeedResponse(items=[], has_next=False, page=req.page) @@ -142,8 +145,6 @@ def feed(req: FeedRequest) -> FeedResponse: waimai, daodian = f_wm.result(), f_dd.result() items = _interleave(waimai, daodian) - if tab == "rec": - items = [c for c in items if _commission_pct(c) >= 3.0] has_next = page_idx + 1 < len(_TOPIC_ROUNDS) return FeedResponse(items=items, has_next=has_next, page=req.page) @@ -172,32 +173,36 @@ def referral_link(req: ReferralLinkRequest) -> ReferralLinkResponse: @router.post("/top-sales", response_model=CouponListResponse, summary="销量最高(从离线库 meituan_coupon 按销量降序 + 跨源去重,不实时打美团)") def top_sales(req: TopSalesRequest, db: Session = Depends(get_db)) -> CouponListResponse: - # 只取有销量档的(美团很多券没销量,排不了);按销量降序、同销量再按佣金降序 - stmt = select(MeituanCoupon).where(MeituanCoupon.sale_volume_num.isnot(None)) + # 去重 + 排序 + 分页全在 SQL 做,每页只取并解析当前页 ~20 条。 + # (之前实现每翻一页都全表拉取 + 全量 from_raw 解析,翻页慢 → 客户端滑动卡顿/翻不动。) + # 1) DISTINCT ON (dedup_key):每个去重键(品牌|名|价)只留销量最高那条(同销量再按佣金) + base = select(MeituanCoupon).where(MeituanCoupon.sale_volume_num.isnot(None)) if req.platform is not None: - stmt = stmt.where(MeituanCoupon.platform == req.platform) - stmt = stmt.order_by( + base = base.where(MeituanCoupon.platform == req.platform) + deduped = base.distinct(MeituanCoupon.dedup_key).order_by( + MeituanCoupon.dedup_key, MeituanCoupon.sale_volume_num.desc(), MeituanCoupon.commission_percent.desc(), - ) - rows = db.execute(stmt).scalars().all() + ).subquery() - # 跨源去重(dedup_key = 品牌|名|价):按销量降序遍历,每个 dedup_key 只留第一条(=销量最高那条)。 - # 用 raw(整条原始返回)重建 CouponCard,字段与实时接口完全一致,前端无需改渲染。 - seen: set[str] = set() + # 2) 对去重结果按销量降序分页;多取 1 条判断 has_next,只对本页做 from_raw + m = aliased(MeituanCoupon, deduped) + start = (req.page - 1) * req.page_size + rows = db.execute( + select(m) + # 加 id 作稳定 tiebreaker:同销量同佣金的并列项排序确定,避免跨页重复/漏项 + .order_by(m.sale_volume_num.desc(), m.commission_percent.desc(), m.id) + .offset(start) + .limit(req.page_size + 1) + ).scalars().all() + + has_next = len(rows) > req.page_size cards: list[CouponCard] = [] - for row in rows: - if row.dedup_key in seen: - continue - seen.add(row.dedup_key) + for row in rows[:req.page_size]: try: card = CouponCard.from_raw(row.raw or {}) except Exception: # noqa: BLE001 continue if card.product_view_sign: cards.append(card) - - start = (req.page - 1) * req.page_size - page_items = cards[start:start + req.page_size] - has_next = start + req.page_size < len(cards) - return CouponListResponse(items=page_items, has_next=has_next, search_id=None) + return CouponListResponse(items=cards, has_next=has_next, search_id=None)