# 药帮忙按条形码搜索,收成选品候选。无货的价格记 0,不参与拆量。 from __future__ import annotations from typing import Any import requests from http_util import post_json_with_retry from mall_config import YBM_MALL_NAME, YBM_SEARCH_API_URL, YBM_SORT_STRATEGY from rules import Candidate, normalize_vendor, parse_expiry_months from util import normalize_text, parse_float_or_none, parse_int_or_none # 拼药帮忙搜索体。关键字是条形码,只要效期超过一年的。 def build_ybm_search_payload(query_key: str) -> dict[str, Any]: return { "queryWord": query_key, "searchScene": 1, "nearEffect": "", "minPrice": "", "maxPrice": "", "drugClassificationsStr": "", "isThirdCompany": 0, "isEffectPeriodOverOneYear": 1, "sortStrategy": YBM_SORT_STRATEGY, } # 取平台价。price 空了再用 fob。 def pick_ybm_platform_price(product: dict[str, Any]) -> Any: price = product.get("price") if normalize_text(price) != "": return price fob = product.get("fob") if normalize_text(fob) != "": return fob return "" # 按条形码查药帮忙,返回商品行。接口声明失败就报错,不当成没搜到。 def query_ybm_products( session: requests.Session, query_key: str, ) -> list[dict[str, Any]]: headers = { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "Origin": "https://www.ybm100.com", "Referer": "https://www.ybm100.com/new/", "X-Requested-With": "XMLHttpRequest", "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36" ), } payload = build_ybm_search_payload(query_key) payload_json = post_json_with_retry( session, YBM_SEARCH_API_URL, platform_name=YBM_MALL_NAME, query_key=query_key, json=payload, headers=headers, ) if not payload_json.get("success"): error_msg = normalize_text(payload_json.get("msg")) or "未知错误" raise RuntimeError(f"药帮忙查询失败:{error_msg}") data_section = payload_json.get("data") or {} rows = data_section.get("rows") or [] if not isinstance(rows, list): raise RuntimeError(f"药帮忙接口返回 data.rows 结构异常:{type(rows)}") return rows # 把药帮忙商品收成候选。库存为 0 时价格记 0,后面拆量不会吃它。 def map_ybm_products(rows: list[dict[str, Any]]) -> list[Candidate]: candidates: list[Candidate] = [] for row in rows: product = row.get("productInfo") or {} available_qty = product.get("availableQty", "") available_num = parse_int_or_none(available_qty) no_stock = available_num == 0 or normalize_text(available_qty) == "0" raw_price = 0 if no_stock else pick_ybm_platform_price(product) vendor_raw = normalize_text(product.get("shopName")) expiry_raw = normalize_text(product.get("nearEffect")) raw = dict(product) if isinstance(product, dict) else {} if not raw.get("skuId") and row.get("skuId"): raw["skuId"] = row.get("skuId") candidates.append( Candidate( mall_name=YBM_MALL_NAME, sku_id=normalize_text(product.get("skuId", row.get("skuId", ""))), name=normalize_text(product.get("showName")), spec=normalize_text(product.get("spec")), price=parse_float_or_none(raw_price), expiry_raw=expiry_raw, expiry_months=parse_expiry_months(expiry_raw), vendor_raw=vendor_raw, vendor_norm=normalize_vendor(vendor_raw), stock=parse_float_or_none(available_qty), extra={ "库存说明": "无货" if no_stock else "有货", "raw": raw, }, ) ) return candidates # 按条形码搜药帮忙。同一查询键本班只打一次接口。 def search_candidates( cookies: dict[str, str], query_key: str, session: requests.Session | None = None, cache: dict[str, list[Candidate]] | None = None, ) -> list[Candidate]: store = cache if cache is not None else {} if query_key in store: return store[query_key] own_session = session or requests.Session() try: if session is None: for name, value in cookies.items(): own_session.cookies.set(name, value) rows = query_ybm_products(own_session, query_key) mapped = map_ybm_products(rows) finally: if session is None: own_session.close() store[query_key] = mapped return mapped