# 选品班次入口:登录 1 号药城 / 药帮忙,按条形码搜,凑齐后按本次 sku 加车。不推接口 2。 from __future__ import annotations import argparse import sys import time from pathlib import Path import requests from auth_login import ( DEFAULT_YAOEX_AUTH_PATH, DEFAULT_YBM_COOKIE_PATH, BrowserRuntime, load_cookie_file, load_yaoex_auth_file, login_yaoex, login_ybm, save_cookie_file, save_yaoex_auth, ) from rpa_oracle import RepoUnavailable, load_job_env from job import run_batch from mall_config import ( YAOEX_MALL_NAME, YAOEX_PLATFORM_TYPE, YBM_MALL_NAME, YBM_PLATFORM_TYPE, ) from repo import OracleSelectRepo from rules import Candidate from search_yaoex import YaoexAuth from search_yaoex import search_candidates as search_yaoex_candidates from search_ybm import search_candidates as search_ybm_candidates DEFAULT_CLAIM_LIMIT = 50 _CART_ROOT = Path(__file__).resolve().parent.parent / "rpa_cart" def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """解析本班领取条数、跑哪个平台、是否用登录缓存。 不传 --limit 时用 50,不再把空值当成全部领走。 """ parser = argparse.ArgumentParser( description="选品班次:捞表1 deal_flag=0,按条形码搜平台,结果写入表2。" ) parser.add_argument( "--limit", type=int, default=DEFAULT_CLAIM_LIMIT, help="本班最多领取 N 条,默认 50", ) parser.add_argument( "--platform", choices=["all", "yaoex", "ybm"], default="all", help="all=两平台都跑", ) parser.add_argument( "--yaoex-auth", type=Path, default=None, help="已登录的 1号药城认证 JSON(token/user_id/buyer_code),跳过浏览器", ) parser.add_argument( "--ybm-cookies", type=Path, default=None, help="已登录的药帮忙 cookie JSON,跳过浏览器", ) parser.add_argument( "--keep-browser", action="store_true", help="登录后不关 Chrome,搜完再留一段时间方便查看", ) parser.add_argument( "--hold-seconds", type=int, default=180, help="--keep-browser 时窗口再留多少秒,默认 180", ) parser.add_argument( "--fresh-login", action="store_true", help="忽略本地登录缓存,强制重新打开浏览器", ) return parser.parse_args(argv) # 把命令行平台收成要登录的商城名。all 是两个都跑。 def _platforms(choice: str) -> list[str]: if choice == "yaoex": return [YAOEX_MALL_NAME] if choice == "ybm": return [YBM_MALL_NAME] return [YAOEX_MALL_NAME, YBM_MALL_NAME] # 登录两个商城,领表 1 deal_flag=0 选品并写表 2。一边登录失败只停那一边。 def main(argv: list[str] | None = None, repo=None) -> None: args = parse_args(argv) load_job_env(Path(__file__).resolve().parent) flow_start = time.perf_counter() if repo is None: try: repo = OracleSelectRepo.from_env() except RepoUnavailable as exc: print(f"数据库不可用:{exc}") raise SystemExit(2) from exc yaoex_auth: YaoexAuth | None = None ybm_cookies: dict[str, str] | None = None login_errors: dict[str, str] = {} runtime = BrowserRuntime(keep=args.keep_browser, hold_seconds=args.hold_seconds) platforms = _platforms(args.platform) try: if YAOEX_MALL_NAME in platforms: try: cached = args.yaoex_auth or ( None if args.fresh_login else DEFAULT_YAOEX_AUTH_PATH ) if cached and cached.exists(): yaoex_auth = load_yaoex_auth_file(cached) print(f"1号药城使用缓存登录:{cached}") else: yaoex_auth = login_yaoex(runtime=runtime) save_yaoex_auth(yaoex_auth) print(f"1号药城登录已缓存:{DEFAULT_YAOEX_AUTH_PATH}") print("1号药城认证就绪。") except SystemExit: raise except Exception as exc: login_errors[YAOEX_MALL_NAME] = str(exc) print(f"1号药城登录失败,本平台中止:{exc}") if YBM_MALL_NAME in platforms: try: cached = args.ybm_cookies or ( None if args.fresh_login else DEFAULT_YBM_COOKIE_PATH ) if cached and cached.exists(): ybm_cookies = load_cookie_file(cached) print(f"药帮忙使用缓存登录:{cached}") else: ybm_cookies = login_ybm(runtime=runtime) save_cookie_file(ybm_cookies) print(f"药帮忙登录已缓存:{DEFAULT_YBM_COOKIE_PATH}") print("药帮忙认证就绪。") except SystemExit: raise except Exception as exc: login_errors[YBM_MALL_NAME] = str(exc) print(f"药帮忙登录失败,本平台中止:{exc}") yaoex_session = requests.Session() ybm_session = requests.Session() yaoex_cache: dict[str, list[Candidate]] = {} ybm_cache: dict[str, list[Candidate]] = {} if ybm_cookies: for name, value in ybm_cookies.items(): ybm_session.cookies.set(name, value) searchers = {} if yaoex_auth is not None: # 用本班已登录的 1 号药城会话按条形码搜。同一条码走缓存。 def search_yaoex(query_key: str) -> list[Candidate]: assert yaoex_auth is not None return search_yaoex_candidates( yaoex_auth, query_key, session=yaoex_session, cache=yaoex_cache ) searchers[YAOEX_MALL_NAME] = search_yaoex if ybm_cookies is not None: # 用本班已登录的药帮忙会话按条形码搜。同一条码走缓存。 def search_ybm(query_key: str) -> list[Candidate]: return search_ybm_candidates( ybm_cookies, query_key, session=ybm_session, cache=ybm_cache ) searchers[YBM_MALL_NAME] = search_ybm adders, login_by_type = _cart_adders( platforms, yaoex_auth=yaoex_auth, ybm_cookies=ybm_cookies, yaoex_session=yaoex_session, ybm_session=ybm_session, login_errors=login_errors, ) cart_finish = _cart_finish(repo, adders, login_by_type) try: summary = run_batch( repo, platforms=platforms, searchers=searchers, login_errors=login_errors, limit=args.limit, cart_finish=cart_finish, ) finally: yaoex_session.close() ybm_session.close() elapsed = time.perf_counter() - flow_start print( f"领取={summary.claimed} ADDED={summary.added} " f"SKIPPED={summary.skipped} FAILED={summary.failed} " f"加购成功={summary.cart_added} 加购失败={summary.cart_failed} " f"用时 {elapsed:.2f} 秒" ) print("已写入 rpa_cart_settlement_goods_data,push_status=0,未推订单中心。") finally: runtime.close() # 把加购包放进导入路径。选品目录仍排在前面,避免拿错同名模块。 def _ensure_cart_path() -> None: if _CART_ROOT.is_dir() and str(_CART_ROOT) not in sys.path: sys.path.append(str(_CART_ROOT)) # 用本班已登录的会话加车。登录失败的平台不配加车函数。 def _cart_adders( platforms: list[str], *, yaoex_auth, ybm_cookies, yaoex_session: requests.Session, ybm_session: requests.Session, login_errors: dict[str, str], ) -> tuple[dict, dict[int, str]]: _ensure_cart_path() from cart_yaoex import add_yaoex_cart from cart_ybm import add_ybm_cart adders: dict = {} if yaoex_auth is not None and YAOEX_MALL_NAME in platforms: # 按选品拆定的 sku 加入 1 号药城购物车。 def add_yaoex(offer, qty: int): return add_yaoex_cart(yaoex_auth, offer, qty, session=yaoex_session) adders[YAOEX_PLATFORM_TYPE] = add_yaoex if ybm_cookies is not None and YBM_MALL_NAME in platforms: # 按选品拆定的 sku 加入药帮忙购物车。 def add_ybm(offer, qty: int): return add_ybm_cart(ybm_cookies, offer, qty, session=ybm_session) adders[YBM_PLATFORM_TYPE] = add_ybm login_by_type: dict[int, str] = {} if YAOEX_MALL_NAME in login_errors: login_by_type[YAOEX_PLATFORM_TYPE] = login_errors[YAOEX_MALL_NAME] if YBM_MALL_NAME in login_errors: login_by_type[YBM_PLATFORM_TYPE] = login_errors[YBM_MALL_NAME] return adders, login_by_type # 内存选品仓库上的盖章。字段和加购班的成功、失败、放回一致。 class _MemoryCartStamp: # 盖章写回选品仓库里刚插入的表 2 行。 def __init__(self, select_repo): self._repo = select_repo self.shift_errors: list = [] # 成功只改戳,status 保持 1。 def mark_success(self, fid: int) -> None: row = self._row(fid) row["update_user_name"] = "加购班" row["fail_reason"] = None # 失败改 status=2,戳改成加购班。 def mark_fail(self, fid: int, reason: str) -> None: row = self._row(fid) row["status"] = 2 row["fail_reason"] = reason row["update_user_name"] = "加购班" # 登录或查询失败放回待领。status 不动。 def mark_release(self, fid: int, reason: str) -> None: row = self._row(fid) row["update_user_name"] = "系统管理员" row["fail_reason"] = reason # 记下未捕获异常原文。不改表 2。 def save_shift_errors(self, items) -> None: self.shift_errors.extend(items) # 按刚插入的 fid 找表 2 行。找不到则 KeyError。 def _row(self, fid: int) -> dict: for row in self._repo.cart: if int(row.get("fid") or 0) == int(fid): return row raise KeyError(fid) # 真库和内存选品仓库都能盖加购章。真库复用选品这条池,避免再开一条连接池。 def _stamp_repo(select_repo): if isinstance(select_repo, OracleSelectRepo): _ensure_cart_path() from cart_repo import OracleCartRepo return OracleCartRepo(select_repo._pool) return _MemoryCartStamp(select_repo) # 落库之后按拆定的行加车。返回加购成功行数和失败行数。 def _cart_finish(select_repo, adders: dict, login_by_type: dict[int, str]): _ensure_cart_path() from cart_job import add_known_lines # 同一单的拆量行一次加完。 def finish(lines): summary = add_known_lines( _stamp_repo(select_repo), lines, adders=adders, login_errors=login_by_type, ) return summary.added, summary.failed return finish if __name__ == "__main__": main()