# 加购班与 1 号药城支付落表。默认支付;`cart` 子命令加购。 from __future__ import annotations import argparse from datetime import date, datetime from pathlib import Path import requests from rpa_oracle import RepoUnavailable from job import run_yaoex_pay from parse_xls import parse_order_xls from repo import OraclePayRepo from yaoex_client import day_range, export_orders from yaoex_login import ( BrowserRuntime, load_cart_dotenv, resolve_yaoex_auth, resolve_ybm_cookies, ) PROJECT_ROOT = Path(__file__).resolve().parent EXPORT_DIR = PROJECT_ROOT / "exports" / "yaoex" YAOEX_PLATFORM_TYPE = 66 YBM_PLATFORM_TYPE = 48 DEFAULT_CART_LIMIT = 20 # 导出文件按订单日放进固定目录。同一秒再导一次加序号,不盖掉上一份。 def pay_export_path( begin: str, end: str, exported_at: datetime, root: Path = EXPORT_DIR, ) -> Path: start_day = begin[:10] end_day = end[:10] folder_name = start_day if start_day == end_day else f"{start_day}_{end_day}" directory = root / folder_name stamp = exported_at.strftime("%H%M%S") path = directory / f"{stamp}.xls" suffix = 2 while path.exists(): path = directory / f"{stamp}_{suffix}.xls" suffix += 1 return path # 区分支付落表和加购。没写子命令时走支付。 def parse_args(argv: list[str] | None = None) -> argparse.Namespace: argv = list(argv) if argv is not None else None if argv is None: import sys argv = sys.argv[1:] command = "pay" if argv and argv[0] in {"pay", "cart"}: command = argv.pop(0) if command == "cart": args = _parse_cart_args(argv) args.command = "cart" return args args = _parse_pay_args(argv) args.command = "pay" return args # 支付落表没有额外参数:登录、导出当天、写表 2。 def _parse_pay_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description="1 号药城支付落表:登录、导出当天、解析、写库") return parser.parse_args(argv) def _parse_cart_args(argv: list[str]) -> argparse.Namespace: """解析加购参数:领取条数、平台、是否忽略登录缓存。 不传 --limit 时用 20,不再把空值当成全部领走。 """ parser = argparse.ArgumentParser(description="加购班:领表 2 选品成功行,按仓加进购物车") parser.add_argument( "--limit", type=int, default=DEFAULT_CART_LIMIT, help="本班最多领取 N 行,默认 20", ) parser.add_argument( "--platform", choices=["all", "yaoex", "ybm"], default="all", help="all=两平台都跑", ) parser.add_argument("--fresh-login", action="store_true", help="忽略登录缓存") parser.add_argument("--yaoex-auth", type=Path, default=None) parser.add_argument("--ybm-cookies", type=Path, default=None) return parser.parse_args(argv) # 入口。cart 子命令加购,否则跑 1 号药城支付落表。 def main(argv: list[str] | None = None, repo=None) -> None: args = parse_args(argv) if args.command == "cart": main_cart(args, repo=repo) return main_pay(repo=repo) # 登录 1 号药城,导出当天订单,已支付行写入表 2。不对加购行。 def main_pay(repo=None) -> None: load_cart_dotenv() try: auth = resolve_yaoex_auth() token = auth.token print("1号药城认证就绪。") except SystemExit: raise except Exception as exc: print(f"1号药城登录失败:{exc}") raise SystemExit(2) from exc end = date.today() begin, end_ts = day_range(end, 0) print(f"导出 {begin} ~ {end_ts}") content = export_orders(token, begin=begin, end=end_ts) xls_path = pay_export_path(begin, end_ts, datetime.now()) xls_path.parent.mkdir(parents=True, exist_ok=True) xls_path.write_bytes(content) print(f"导出文件 {xls_path} ({len(content)} bytes)") lines = parse_order_xls(xls_path) print(f"导出行 {len(lines)}") if repo is None: try: repo = OraclePayRepo.from_env() except RepoUnavailable as exc: print(f"数据库不可用:{exc}") raise SystemExit(2) from exc summary = run_yaoex_pay(repo, lines) print( "落表 " f"inserted={summary.inserted} " f"skipped_status={summary.skipped_status} " f"skipped_dup={summary.skipped_dup} " f"unmatched={summary.unmatched} " f"reasons={summary.unmatched_reasons}" ) # 领表 2 选品成功行,按条形码再搜并对上商家后加车。一行失败只改那一行。 def main_cart(args: argparse.Namespace, repo=None) -> None: from cart_job import run_cart_batch from cart_repo import OracleCartRepo from cart_search import search_yaoex_offers, search_ybm_offers from cart_yaoex import add_yaoex_cart from cart_ybm import add_ybm_cart load_cart_dotenv() if repo is None: try: repo = OracleCartRepo.from_env() except RepoUnavailable as exc: print(f"数据库不可用:{exc}") raise SystemExit(2) from exc want_yaoex = args.platform in {"all", "yaoex"} want_ybm = args.platform in {"all", "ybm"} login_errors: dict[int, str] = {} searchers = {} adders = {} yaoex_auth = None ybm_cookies = None runtime = BrowserRuntime(keep=False) yaoex_session = requests.Session() ybm_session = requests.Session() yaoex_cache: dict = {} ybm_cache: dict = {} try: if want_yaoex: try: yaoex_auth = resolve_yaoex_auth( auth_path=args.yaoex_auth, fresh=args.fresh_login, runtime=runtime, ) print("1号药城认证就绪。") except SystemExit: raise except Exception as exc: login_errors[YAOEX_PLATFORM_TYPE] = str(exc) print(f"1号药城登录失败,本平台中止:{exc}") if want_ybm: try: ybm_cookies = resolve_ybm_cookies( cookie_path=args.ybm_cookies, fresh=args.fresh_login, runtime=runtime, ) print("药帮忙认证就绪。") except SystemExit: raise except Exception as exc: login_errors[YBM_PLATFORM_TYPE] = str(exc) print(f"药帮忙登录失败,本平台中止:{exc}") if ybm_cookies: for name, value in ybm_cookies.items(): ybm_session.cookies.set(name, value) if yaoex_auth is not None: # 用本班已登录的 1 号药城会话按条形码再搜。 def search_yaoex(query_key: str): return search_yaoex_offers( yaoex_auth, query_key, session=yaoex_session, cache=yaoex_cache ) # 把对上的仓按数量加入 1 号药城购物车。 def add_yaoex(offer, qty: int): return add_yaoex_cart(yaoex_auth, offer, qty, session=yaoex_session) searchers[YAOEX_PLATFORM_TYPE] = search_yaoex adders[YAOEX_PLATFORM_TYPE] = add_yaoex if ybm_cookies is not None: # 用本班已登录的药帮忙会话按条形码再搜。 def search_ybm(query_key: str): return search_ybm_offers( ybm_cookies, query_key, session=ybm_session, cache=ybm_cache ) # 把对上的仓按数量加入药帮忙购物车。 def add_ybm(offer, qty: int): return add_ybm_cart(ybm_cookies, offer, qty, session=ybm_session) searchers[YBM_PLATFORM_TYPE] = search_ybm adders[YBM_PLATFORM_TYPE] = add_ybm platform_types: list[int] = [] if want_yaoex: platform_types.append(YAOEX_PLATFORM_TYPE) if want_ybm: platform_types.append(YBM_PLATFORM_TYPE) summary = run_cart_batch( repo, searchers=searchers, adders=adders, login_errors=login_errors, limit=args.limit, platform_types=platform_types, ) print( f"领取={summary.claimed} 加购成功={summary.added} 失败={summary.failed}" ) print("未推订单中心,未清空购物车。") finally: yaoex_session.close() ybm_session.close() runtime.close() if __name__ == "__main__": main()