"""美团 CPS 券列表 + 换链。 不需要登录,客户端传经纬度即可。 """ from __future__ import annotations import logging from concurrent.futures import ThreadPoolExecutor, as_completed from fastapi import APIRouter, HTTPException from app.core.config import settings from app.integrations.meituan import MeituanCpsError, get_referral_link, query_coupon from app.schemas.meituan import ( CouponCard, CouponListRequest, CouponListResponse, FeedRequest, FeedResponse, ReferralLinkRequest, ReferralLinkResponse, ) logger = logging.getLogger("shagua.meituan") router = APIRouter(prefix="/api/v1/meituan", tags=["meituan-cps"]) @router.post("/coupons", response_model=CouponListResponse, summary="券列表(为您推荐)") def list_coupons(req: CouponListRequest) -> CouponListResponse: if not settings.mt_cps_configured: return CouponListResponse(items=[], has_next=False, search_id=None) logger.info("[coupons] lon=%.6f lat=%.6f topic=%s", req.longitude, req.latitude, req.list_topic_id) try: raw = query_coupon( longitude=req.longitude, latitude=req.latitude, platform=req.platform, biz_line=req.biz_line, list_topic_id=req.list_topic_id, search_text=req.keyword, search_id=req.search_id, sort_field=req.sort_field, page_no=req.page, page_size=req.page_size, ) except MeituanCpsError as e: logger.error("query_coupon failed: %s", e) raise HTTPException(status_code=502, detail=f"meituan: {e}") from e items = [CouponCard.from_raw(it) for it in (raw.get("data") or [])] return CouponListResponse( items=items, has_next=raw.get("hasNext", False), search_id=raw.get("searchId"), ) _TOPIC_ROUNDS = [ (3, 3), # 爆款筛选 (2, 2), # 今日必推 (1, 5), # 精选 + 限时筛选 ] def _interleave(waimai: list[dict], daodian: list[dict]) -> list[CouponCard]: items: list[CouponCard] = [] seen: set[str] = set() i = j = 0 while i < len(waimai) or j < len(daodian): for _ in range(2): if i < len(waimai): card = CouponCard.from_raw(waimai[i]); i += 1 if card.product_view_sign not in seen: seen.add(card.product_view_sign); items.append(card) if j < len(daodian): card = CouponCard.from_raw(daodian[j]); j += 1 if card.product_view_sign not in seen: seen.add(card.product_view_sign); items.append(card) return items @router.post("/feed", response_model=FeedResponse, summary="混合feed(外卖+到店交叉, 无限流)") def feed(req: FeedRequest) -> FeedResponse: if not settings.mt_cps_configured: return FeedResponse(items=[], has_next=False, page=req.page) page_idx = req.page - 1 lon, lat = req.longitude, req.latitude logger.info("[feed] page=%s lon=%.6f lat=%.6f", req.page, lon, lat) def _fetch_topic(platform: int, biz_line: int | None, topic: int) -> list[dict]: try: return (query_coupon( longitude=lon, latitude=lat, platform=platform, biz_line=biz_line, list_topic_id=topic, page_size=20, ).get("data") or []) except MeituanCpsError: return [] if page_idx >= len(_TOPIC_ROUNDS): return FeedResponse(items=[], has_next=False, page=req.page) wm_topic, dd_topic = _TOPIC_ROUNDS[page_idx] with ThreadPoolExecutor(max_workers=2) as pool: f_wm = pool.submit(_fetch_topic, 1, None, wm_topic) f_dd = pool.submit(_fetch_topic, 2, 1, dd_topic) waimai, daodian = f_wm.result(), f_dd.result() items = _interleave(waimai, daodian) has_next = page_idx + 1 < len(_TOPIC_ROUNDS) return FeedResponse(items=items, has_next=has_next, page=req.page) @router.post("/referral-link", response_model=ReferralLinkResponse, summary="换取推广链接(点抢时调)") def referral_link(req: ReferralLinkRequest) -> ReferralLinkResponse: if not settings.mt_cps_configured: return ReferralLinkResponse(link="", link_map={}) try: raw = get_referral_link( product_view_sign=req.product_view_sign, platform=req.platform, biz_line=req.biz_line, sid=req.sid, link_type_list=req.link_type_list, ) except MeituanCpsError as e: logger.error("get_referral_link failed: %s", e) raise HTTPException(status_code=502, detail=f"meituan: {e}") from e link_map = raw.get("referralLinkMap") or {} link = raw.get("data") or link_map.get("1") or link_map.get("3") or next(iter(link_map.values()), "") return ReferralLinkResponse(link=link, link_map=link_map)