AutoRpa/rpa_selection/search_yaoex.py

202 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import base64
import time
from binascii import Error as BinasciiError
from dataclasses import dataclass
from typing import Any
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from http_util import post_json_with_retry
from mall_config import (
YAOEX_MALL_NAME,
YAOEX_PRICE_DECRYPT_BASE_KEY,
YAOEX_SEARCH_API_URL,
)
from rules import Candidate, normalize_vendor, parse_expiry_months
from util import normalize_text, parse_float_or_none
@dataclass
class YaoexAuth:
token: str
user_id: str
buyer_code: str
def build_yaoex_search_request(
auth: YaoexAuth, query_key: str
) -> tuple[dict[str, Any], dict[str, Any]]:
timestamp = str(int(time.time() * 1000))
common_part: dict[str, Any] = {
"traderName": "yaoex_pc",
"trader": "pc",
"closesignature": "yes",
"signature_method": "md5",
"signature": "",
"timestamp": timestamp,
"token": auth.token,
"userToken": auth.token,
}
form_payload = {
**common_part,
"userId": auth.user_id,
"roleId": "",
"userType": "",
"buyerCode": auth.buyer_code,
"nowPage": 1,
"per": 20,
"keyword": query_key,
"catSearchId": "",
"specs": "",
"factoryIds": "",
"sellerCodes": "",
"sellerFilterMode": 1,
"sortColumn": "default",
"sortMode": "default",
"ver": 1,
"stock_mode": 1,
"showExtendCard": "true",
"needDinnerPrice": "true",
"limitStart": "",
"limitEnd": "",
"deadLineStart": 12,
"deadLineEnd": "",
"filterDtos": "",
"showWholePurchase": "true",
}
return common_part, form_payload
def build_yaoex_price_decrypt_key(user_id: Any) -> bytes:
user_text = normalize_text(user_id)
if not user_text:
return YAOEX_PRICE_DECRYPT_BASE_KEY.encode("utf-8")
dynamic_key = YAOEX_PRICE_DECRYPT_BASE_KEY[:10] + user_text[:6].rjust(6, "0")
return dynamic_key.encode("utf-8")
def decrypt_yaoex_price(raw_price: Any, user_id: Any) -> Any:
if raw_price is None:
return ""
if isinstance(raw_price, (int, float)):
return raw_price
text = normalize_text(raw_price)
if not text:
return ""
try:
return float(text)
except ValueError:
pass
try:
cipher_bytes = base64.b64decode(text)
key_bytes = build_yaoex_price_decrypt_key(user_id)
plain_bytes = AES.new(key_bytes, AES.MODE_ECB).decrypt(cipher_bytes)
plain_text = unpad(plain_bytes, AES.block_size).decode("utf-8").strip()
except (BinasciiError, ValueError, UnicodeDecodeError):
return text
if plain_text == "":
return 0
try:
return float(plain_text)
except ValueError:
return plain_text
def query_yaoex_products(
session: requests.Session,
auth: YaoexAuth,
query_key: str,
) -> tuple[list[dict[str, Any]], str]:
params, payload = build_yaoex_search_request(auth, query_key)
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://mall.yaoex.com",
"Referer": "https://mall.yaoex.com/",
"X-Request-Agent": "Axios",
"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_json = post_json_with_retry(
session,
YAOEX_SEARCH_API_URL,
platform_name=YAOEX_MALL_NAME,
query_key=query_key,
params=params,
data=payload,
headers=headers,
)
data_section = payload_json.get("data") or {}
products = data_section.get("shopProducts") or []
if not isinstance(products, list):
raise RuntimeError(f"1号药城接口返回 shopProducts 结构异常:{type(products)}")
response_user_id = normalize_text(data_section.get("userId")) or auth.user_id
return products, response_user_id
def map_yaoex_products(products: list[dict[str, Any]], user_id: Any) -> list[Candidate]:
candidates: list[Candidate] = []
if products:
sample = products[0]
expiry_keys = {
key: sample.get(key)
for key in sample
if any(token in str(key).lower() for token in ("dead", "valid", "expir", "period", "effect"))
}
print(f"1号药城效期相关字段示例:{expiry_keys}")
for product in products:
vendor_raw = normalize_text(product.get("vendorName"))
expiry_raw = ""
for key in ("deadLine", "deadLineDesc", "validDate", "expireDate", "period"):
expiry_raw = normalize_text(product.get(key))
if expiry_raw:
break
raw_price = decrypt_yaoex_price(product.get("price", ""), user_id)
candidates.append(
Candidate(
mall_name=YAOEX_MALL_NAME,
sku_id=normalize_text(product.get("spuCode")),
name=normalize_text(product.get("shortName")),
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(product.get("stockCount")),
extra={"stockCountDesc": product.get("stockCountDesc", "")},
)
)
return candidates
def search_candidates(
auth: YaoexAuth,
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:
products, user_id = query_yaoex_products(own_session, auth, query_key)
mapped = map_yaoex_products(products, user_id)
finally:
if session is None:
own_session.close()
store[query_key] = mapped
return mapped