feat: 加购班领表2按仓加入购物车,失败改status=2
选品成功行领取后按条形码对仓加车。成功盖章不插行;登录挂、库存、中包装只改该行失败状态。不推接口2。
This commit is contained in:
parent
abf26b4259
commit
0fa29ec9bd
22
AGENTS.md
22
AGENTS.md
|
|
@ -1,6 +1,6 @@
|
|||
# AutoRpa
|
||||
|
||||
洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)和**选品班次**(`rpa_selection/`):收件落 Oracle,请求里不跑选品;选品作业另起进程捞 `deal_flag=0`。合同页面在 `rpa_contract/`;加购 `rpa_cart/`、返利 `rpa_rebate/` 仍是占位。根上不再有 `选品流程/`、`state_center/`、`rpa_yisaiqi/`。
|
||||
洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)、**选品班次**(`rpa_selection/`)和**加购班次**(`rpa_cart/`):收件落 Oracle,请求里不跑选品;选品作业另起进程捞 `deal_flag=0`;加购另起进程领表 2 成功行。合同页面在 `rpa_contract/`;返利 `rpa_rebate/` 仍是占位。根上不再有 `选品流程/`、`state_center/`、`rpa_yisaiqi/`。
|
||||
|
||||
智力用在**定落点、想清楚对错**;速度和 token 靠下面这张地图定点打开,禁止用扫仓代替思考。改对应了,反工和消耗都会少。
|
||||
|
||||
|
|
@ -31,7 +31,8 @@
|
|||
| 节点入口 | `rpa_contract/contract_create_node.py`、`main.py` | 读到正式合同号 |
|
||||
| 浏览器适配 | `rpa_contract/apa_browser_adapter.py` | `rpa_contract/tests/test_apa_browser_adapter.py::点名` |
|
||||
| 文字验收 / 截图策略 | `rpa_contract/accept.py` | 只读 `ACCEPT.txt`,禁止读 png |
|
||||
| 加购 / 支付回传 | `rpa_cart/README.md` | 加购未做;1 号药城支付按 Excel 落表,不对加购行 |
|
||||
| 加购班次 | `rpa_cart/cart_job.py`、`cart_repo.py`、`cart_match.py` | `rpa_cart/tests/test_cart_job.py::test_job_adds_split_rows_without_insert` |
|
||||
| 支付回传 | `rpa_cart/job.py`、`settle.py` | 1 号药城支付按 Excel 落表,不对加购行 |
|
||||
| 返利班 | `rpa_rebate/README.md` | 占位,未实现 |
|
||||
| 四个口地图 | `rpa_oms/README.md`、根 `README.md` | — |
|
||||
| 节点业务规则(点名一节) | `docs/RPA订单中心接口开发口径_20260930.md` | 禁止通读 `docs/` |
|
||||
|
|
@ -83,6 +84,22 @@
|
|||
|
||||
验收:`cd rpa_selection` → `uv run pytest tests/test_pipeline.py::test_split_across_malls_is_success -q`(杭州 3 + 广东 7 = 计划 10)。
|
||||
|
||||
## 加购落点
|
||||
|
||||
班次作业在 `rpa_cart/`,不在选品请求里跑。领表 2 `operate_type=1 AND status=1 AND push_status=0 AND update_user_name=系统管理员`。登录 1 号药城 / 药帮忙,按条形码再搜,对上 `supplier_name`,按行加车。
|
||||
|
||||
| 步 | 函数 | 要点 |
|
||||
| --- | --- | --- |
|
||||
| 领取 | `claim_pending` | 盖 `加购中`;崩溃停在这戳,改回 `系统管理员` 再跑 |
|
||||
| 对仓 | `match_offer` | 条形码结果对商家全称;同仓留低价 |
|
||||
| 加车 | `run_cart_batch` + 平台 `add_*_cart` | 改数量加入购物车。无 `sku_id`,必须再搜 |
|
||||
| 成功 | `mark_success` | 不插行,`update_user_name=加购班`,`status` 仍 1 |
|
||||
| 失败 | `mark_fail` | 该行 `status=2` + 原因。登录挂写系统故障;库存/中包装/对不上仓写业务原因 |
|
||||
|
||||
同一 `omsId` 拆两仓要加齐;一行失败只改那一行。不推接口 2,不清空购物车,不碰合同、表 1。接口 2 仍捞全部 `push_status=0`,加购班跑完之前不要组装。
|
||||
|
||||
验收:`cd rpa_cart` → `uv run pytest tests/test_cart_job.py::test_job_adds_split_rows_without_insert -q`(杭州 3 + 广东 7,不插新行)。
|
||||
|
||||
## CONTRACT_CREATE 落点
|
||||
|
||||
页面脚本在 `rpa_contract/`。`run_contract_create_flow` 串起来;修一步只打开对应函数。从表 3 合单、写表 4 尚未接。
|
||||
|
|
@ -124,6 +141,7 @@
|
|||
- 接口2:点名 `rpa_oms/tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope`。
|
||||
- 接口3:点名 `rpa_oms/tests/test_purchase_contract_receive.py` 或 `test_oracle_contract_repo.py`。
|
||||
- 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
||||
- 加购:点名 `rpa_cart/tests/test_cart_job.py::test_job_adds_split_rows_without_insert` 或 `test_job_marks_out_of_stock`。
|
||||
- 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
|
||||
- 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,37 @@
|
|||
|
||||
| 班次 | 状态 |
|
||||
| --- | --- |
|
||||
| 加购 `operateType=1` | 未做。选品已落表 2 |
|
||||
| 加购 `operateType=1` | 领表 2 选品成功行,按仓加进购物车。成功盖章,失败改 `status=2` |
|
||||
| 支付回传 `operateType=2` | **1 号药城已做落表**。不推接口 2 |
|
||||
|
||||
## 加购
|
||||
|
||||
选品已经把「哪个仓、加几件」写成表 2:`operate_type=1`、`status=1`、`push_status=0`、`update_user_name=系统管理员`。本班读这些行,登录 1 号药城 / 药帮忙,按条形码再搜一次,对上 `supplier_name`,改数量,加入购物车。同一 `omsId` 拆两仓、两平台要加齐。
|
||||
|
||||
成功不插新行,只把 `update_user_name` 改成 `加购班`。加不进去(登录挂、库存没了、中包装加不进、对不上仓)改该行 `status=2`,写失败原因。领取时先盖 `加购中`;中途崩溃停在这一戳,改回 `系统管理员` 再跑。
|
||||
|
||||
不推订单中心,不清空购物车,不碰合同。表 2 没有 `sku_id`,加购必须再搜条形码。选完到加购之间库存可能变,已知。
|
||||
|
||||
```bash
|
||||
cd rpa_cart
|
||||
uv sync --python 3.12
|
||||
uv run python main.py cart
|
||||
uv run python main.py cart --limit 20
|
||||
uv run python main.py cart --platform yaoex
|
||||
```
|
||||
|
||||
账号用 `rpa_selection/.env` 的 `YAOEX_*` / `YBM_*`,库用 `RPA_ORACLE_*`。
|
||||
|
||||
验收(内存、不连库):
|
||||
|
||||
```bash
|
||||
uv run pytest tests/test_cart_job.py::test_job_adds_split_rows_without_insert -q
|
||||
```
|
||||
|
||||
杭州 3 + 广东 7 两行都加车,表 2 仍两行。失败点名 `test_job_marks_out_of_stock`、`test_job_marks_login_fail_as_system_error`。
|
||||
|
||||
接口 2 仍捞全部 `push_status=0`。加购班跑完之前不要组装推送。
|
||||
|
||||
## 1 号药城支付落表
|
||||
|
||||
从买家中心导出已付订单,按导出行字段写入表 2 `operate_type=2`。不对加购行。购物车和支付单没有强关联,`oms_id` 表上 NOT NULL,先填 `0`,等人后选。
|
||||
|
|
@ -31,7 +59,7 @@ uv run pytest -v
|
|||
|
||||
## Windows 真库
|
||||
|
||||
Mac 打不到 `192.168.12.244`,**插入表 2 在 Windows 上测**。导出/解析在 Mac 已经跑过,Win 上要测的是登录、导出、落表。
|
||||
Mac 打不到 `192.168.12.244`,**改表 2 在 Windows 上测**。支付导出/解析在 Mac 已经跑过;Win 上要测登录、导出、落表,以及加购登录和对仓加车。
|
||||
|
||||
Python 3.12 + Instant Client(位数一致)+ Chrome。密码不要写进仓库。把本包和 `rpa_selection/.env` 一起带过去。
|
||||
|
||||
|
|
@ -49,7 +77,8 @@ $env:RPA_ORACLE_PASSWORD = "这里填密码"
|
|||
$env:RPA_ORACLE_THICK = "1"
|
||||
$env:RPA_ORACLE_CLIENT_LIB = "C:\oracle\instantclient"
|
||||
|
||||
uv run pytest -v
|
||||
uv run pytest tests/test_cart_job.py::test_job_adds_split_rows_without_insert -q
|
||||
uv run python main.py cart
|
||||
# 国庆当天可能没单,用 9-30 有数
|
||||
uv run python main.py --dry-run --day 2026-09-30 --days 0
|
||||
uv run python main.py --day 2026-09-30 --days 0
|
||||
|
|
@ -74,4 +103,4 @@ ORDER BY fid DESC;
|
|||
|
||||
## 本期不做
|
||||
|
||||
真加购、清空购物车、药帮忙支付、接口 2 推送。
|
||||
清空购物车、药帮忙支付、接口 2 推送。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
"""加车结果。HTTP 客户端把商城回包收成这三种。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
KIND_OK = "ok"
|
||||
KIND_BUSINESS = "business"
|
||||
KIND_SYSTEM = "system"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddOutcome:
|
||||
kind: str
|
||||
reason: str = ""
|
||||
|
||||
|
||||
def classify_mall_message(text: str) -> AddOutcome:
|
||||
msg = (text or "").strip()
|
||||
if not msg:
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="加购失败")
|
||||
if any(token in msg for token in ("成功", "已加入")):
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
if any(token in msg for token in ("中包装", "倍数", "起订")):
|
||||
reason = msg if "中包装" in msg else f"中包装加不进:{msg}"
|
||||
return AddOutcome(kind=KIND_BUSINESS, reason=reason)
|
||||
if any(token in msg for token in ("库存", "无货", "缺货")):
|
||||
return AddOutcome(kind=KIND_BUSINESS, reason="库存不足")
|
||||
return AddOutcome(kind=KIND_BUSINESS, reason=msg)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
"""加购班次:领表 2 选品成功行 → 再搜条形码对仓 → 加车。成功盖章,失败改 status=2。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from cart_add import ( # noqa: F401
|
||||
KIND_BUSINESS,
|
||||
KIND_OK,
|
||||
KIND_SYSTEM,
|
||||
AddOutcome,
|
||||
)
|
||||
from cart_match import CartOffer, match_offer, middle_pack, qty_fits_pack
|
||||
from cart_repo import CartRow, clip_reason
|
||||
|
||||
Searcher = Callable[[str], list[CartOffer]]
|
||||
Adder = Callable[[CartOffer, int], AddOutcome]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CartSummary:
|
||||
claimed: int = 0
|
||||
added: int = 0
|
||||
failed: int = 0
|
||||
|
||||
|
||||
def run_cart_batch(
|
||||
repo,
|
||||
*,
|
||||
searchers: dict[int, Searcher],
|
||||
adders: dict[int, Adder],
|
||||
login_errors: dict[int, str],
|
||||
limit: int | None = None,
|
||||
platform_types: list[int] | None = None,
|
||||
) -> CartSummary:
|
||||
rows = repo.claim_pending(limit=limit, platform_types=platform_types)
|
||||
summary = CartSummary(claimed=len(rows))
|
||||
for row in rows:
|
||||
reason = _add_one(row, searchers, adders, login_errors)
|
||||
if reason is None:
|
||||
repo.mark_success(row.fid)
|
||||
summary.added += 1
|
||||
else:
|
||||
repo.mark_fail(row.fid, reason)
|
||||
summary.failed += 1
|
||||
return summary
|
||||
|
||||
|
||||
def _add_one(
|
||||
row: CartRow,
|
||||
searchers: dict[int, Searcher],
|
||||
adders: dict[int, Adder],
|
||||
login_errors: dict[int, str],
|
||||
) -> str | None:
|
||||
platform = row.platform_type
|
||||
if platform in login_errors:
|
||||
return _system(login_errors[platform])
|
||||
searcher = searchers.get(platform)
|
||||
if searcher is None:
|
||||
return _system("未登录该平台")
|
||||
adder = adders.get(platform)
|
||||
if adder is None:
|
||||
return _system("未配置加车")
|
||||
bar_code = (row.bar_code or "").strip()
|
||||
if not bar_code:
|
||||
return "缺少条形码"
|
||||
qty = int(row.purchase_qty)
|
||||
if qty <= 0:
|
||||
return "采购数量无效"
|
||||
try:
|
||||
offers = searcher(bar_code)
|
||||
except Exception as exc:
|
||||
return _system(f"查询失败:{exc}")
|
||||
offer = match_offer(offers, row.supplier_name)
|
||||
if offer is None:
|
||||
return "未找到对应仓"
|
||||
if offer.stock is not None and offer.stock < qty:
|
||||
return "库存不足"
|
||||
pack = middle_pack(offer)
|
||||
if not qty_fits_pack(qty, pack):
|
||||
return f"中包装加不进:数量须为{pack}的倍数"
|
||||
try:
|
||||
outcome = adder(offer, qty)
|
||||
except Exception as exc:
|
||||
return _system(f"加购失败:{exc}")
|
||||
if outcome.kind == KIND_OK:
|
||||
return None
|
||||
reason = (outcome.reason or "加购失败").strip()
|
||||
if outcome.kind == KIND_SYSTEM:
|
||||
return _system(reason)
|
||||
return clip_reason(reason)
|
||||
|
||||
|
||||
def _system(detail: str) -> str:
|
||||
text = (detail or "未知错误").strip()
|
||||
if text.startswith("系统故障"):
|
||||
return clip_reason(text)
|
||||
return clip_reason(f"系统故障:{text}")
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""按供应商名对上搜到的仓。无 sku_id 时靠条形码结果 + 商家全称。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
SELF_OPERATED_PREFIX = "[自营]"
|
||||
PACK_KEYS = ("middlePackNum", "midPackQty", "packNum", "packQty")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CartOffer:
|
||||
sku_id: str
|
||||
vendor_raw: str
|
||||
vendor_norm: str
|
||||
stock: float | None
|
||||
price: float | None = None
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def normalize_supplier(raw: str) -> str:
|
||||
text = (raw or "").replace("\u3000", " ").strip()
|
||||
if text.startswith(SELF_OPERATED_PREFIX):
|
||||
text = text[len(SELF_OPERATED_PREFIX) :]
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
def match_offer(offers: list[CartOffer], supplier_name: str) -> CartOffer | None:
|
||||
target = normalize_supplier(supplier_name)
|
||||
if not target:
|
||||
return None
|
||||
matched = [
|
||||
offer
|
||||
for offer in offers
|
||||
if normalize_supplier(offer.vendor_raw) == target
|
||||
or normalize_supplier(offer.vendor_norm) == target
|
||||
]
|
||||
if not matched:
|
||||
return None
|
||||
return min(matched, key=_offer_rank)
|
||||
|
||||
|
||||
def middle_pack(offer: CartOffer) -> int | None:
|
||||
raw = offer.extra.get("raw") if isinstance(offer.extra.get("raw"), dict) else offer.extra
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
for key in PACK_KEYS:
|
||||
value = _positive_int(raw.get(key))
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def qty_fits_pack(qty: int, pack: int | None) -> bool:
|
||||
if pack is None or pack <= 1:
|
||||
return True
|
||||
return qty % pack == 0
|
||||
|
||||
|
||||
def _offer_rank(offer: CartOffer) -> tuple[int, float, float]:
|
||||
price = offer.price
|
||||
stock = offer.stock
|
||||
return (
|
||||
0 if price is not None else 1,
|
||||
float(price) if price is not None else 0.0,
|
||||
-float(stock) if stock is not None else 0.0,
|
||||
)
|
||||
|
||||
|
||||
def _positive_int(value: object) -> int | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
number = int(float(str(value)))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if number <= 0:
|
||||
return None
|
||||
return number
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
"""表 2 加购行:领取、成功盖章、失败改状态。成功不插新行。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from db import RepoUnavailable, connect_from_env, oracle_dsn
|
||||
|
||||
OPERATE_TYPE_ADD_CART = 1
|
||||
STATUS_OK = 1
|
||||
STATUS_FAIL = 2
|
||||
PUSH_NOT_SENT = 0
|
||||
STAMP_SELECTION = "系统管理员"
|
||||
STAMP_CLAIMING = "加购中"
|
||||
STAMP_DONE = "加购班"
|
||||
FAIL_REASON_MAX = 500
|
||||
|
||||
_CLAIM_SQL = """
|
||||
UPDATE rpa_cart_settlement_goods_data
|
||||
SET update_user_name = :stamp,
|
||||
update_time = SYSDATE
|
||||
WHERE fid = :fid
|
||||
AND operate_type = 1
|
||||
AND status = 1
|
||||
AND push_status = 0
|
||||
AND NVL(update_user_name, :selection_stamp) = :selection_stamp
|
||||
"""
|
||||
|
||||
_SUCCESS_SQL = """
|
||||
UPDATE rpa_cart_settlement_goods_data
|
||||
SET update_user_name = :stamp,
|
||||
fail_reason = NULL,
|
||||
update_time = SYSDATE
|
||||
WHERE fid = :fid
|
||||
AND update_user_name = :claim_stamp
|
||||
"""
|
||||
|
||||
_FAIL_SQL = """
|
||||
UPDATE rpa_cart_settlement_goods_data
|
||||
SET status = :status,
|
||||
fail_reason = :fail_reason,
|
||||
update_user_name = :stamp,
|
||||
update_time = SYSDATE
|
||||
WHERE fid = :fid
|
||||
AND update_user_name = :claim_stamp
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CartRow:
|
||||
fid: int
|
||||
oms_id: int
|
||||
platform_type: int
|
||||
supplier_name: str
|
||||
bar_code: str
|
||||
purchase_qty: int
|
||||
status: int
|
||||
fail_reason: str | None
|
||||
push_status: int
|
||||
update_user_name: str
|
||||
operate_type: int = OPERATE_TYPE_ADD_CART
|
||||
|
||||
|
||||
def clip_reason(text: str) -> str:
|
||||
text = (text or "").strip()
|
||||
if len(text) <= FAIL_REASON_MAX:
|
||||
return text
|
||||
return text[: FAIL_REASON_MAX - 1] + "…"
|
||||
|
||||
|
||||
def _is_pending(row: dict[str, Any]) -> bool:
|
||||
stamp = row.get("update_user_name") or STAMP_SELECTION
|
||||
return (
|
||||
int(row.get("operate_type") or 0) == OPERATE_TYPE_ADD_CART
|
||||
and int(row.get("status") or 0) == STATUS_OK
|
||||
and int(row.get("push_status") or 0) == PUSH_NOT_SENT
|
||||
and stamp == STAMP_SELECTION
|
||||
)
|
||||
|
||||
|
||||
def _row_from_dict(data: dict[str, Any]) -> CartRow:
|
||||
qty = data.get("purchase_qty")
|
||||
return CartRow(
|
||||
fid=int(data["fid"]),
|
||||
oms_id=int(data["oms_id"]),
|
||||
platform_type=int(data["platform_type"]),
|
||||
supplier_name=str(data.get("supplier_name") or ""),
|
||||
bar_code=str(data.get("bar_code") or ""),
|
||||
purchase_qty=int(qty) if qty is not None else 0,
|
||||
status=int(data.get("status") or 0),
|
||||
fail_reason=data.get("fail_reason"),
|
||||
push_status=int(data.get("push_status") or 0),
|
||||
update_user_name=str(data.get("update_user_name") or ""),
|
||||
operate_type=int(data.get("operate_type") or OPERATE_TYPE_ADD_CART),
|
||||
)
|
||||
|
||||
|
||||
class MemoryCartRepo:
|
||||
def __init__(self, rows: list[dict[str, Any]]):
|
||||
self.rows: dict[int, dict[str, Any]] = {int(row["fid"]): dict(row) for row in rows}
|
||||
|
||||
def claim_pending(
|
||||
self, limit: int | None = None, platform_types: list[int] | None = None
|
||||
) -> list[CartRow]:
|
||||
pending = sorted(
|
||||
(row for row in self.rows.values() if _is_pending(row)),
|
||||
key=lambda row: (int(row["oms_id"]), int(row["fid"])),
|
||||
)
|
||||
if platform_types is not None:
|
||||
allowed = set(platform_types)
|
||||
pending = [row for row in pending if int(row["platform_type"]) in allowed]
|
||||
if limit is not None:
|
||||
pending = pending[:limit]
|
||||
claimed: list[CartRow] = []
|
||||
for row in pending:
|
||||
row["update_user_name"] = STAMP_CLAIMING
|
||||
claimed.append(_row_from_dict(row))
|
||||
return claimed
|
||||
|
||||
def mark_success(self, fid: int) -> None:
|
||||
row = self.rows[fid]
|
||||
row["update_user_name"] = STAMP_DONE
|
||||
row["fail_reason"] = None
|
||||
|
||||
def mark_fail(self, fid: int, reason: str) -> None:
|
||||
row = self.rows[fid]
|
||||
row["status"] = STATUS_FAIL
|
||||
row["fail_reason"] = clip_reason(reason)
|
||||
row["update_user_name"] = STAMP_DONE
|
||||
|
||||
|
||||
class OracleCartRepo:
|
||||
def __init__(self, connect: Callable):
|
||||
self._connect = connect
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OracleCartRepo":
|
||||
oracle_dsn()
|
||||
return cls(connect=connect_from_env)
|
||||
|
||||
def claim_pending(
|
||||
self, limit: int | None = None, platform_types: list[int] | None = None
|
||||
) -> list[CartRow]:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
sql, params = _pending_sql(limit=limit, platform_types=platform_types)
|
||||
cur.execute(sql, params)
|
||||
fetched = [_row_from_cursor(cur, rec) for rec in cur.fetchall()]
|
||||
claimed: list[CartRow] = []
|
||||
for row in fetched:
|
||||
cur.execute(
|
||||
_CLAIM_SQL,
|
||||
{
|
||||
"fid": row.fid,
|
||||
"stamp": STAMP_CLAIMING,
|
||||
"selection_stamp": STAMP_SELECTION,
|
||||
},
|
||||
)
|
||||
if cur.rowcount == 1:
|
||||
row.update_user_name = STAMP_CLAIMING
|
||||
claimed.append(row)
|
||||
conn.commit()
|
||||
return claimed
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def mark_success(self, fid: int) -> None:
|
||||
self._execute(
|
||||
_SUCCESS_SQL,
|
||||
{"fid": int(fid), "stamp": STAMP_DONE, "claim_stamp": STAMP_CLAIMING},
|
||||
)
|
||||
|
||||
def mark_fail(self, fid: int, reason: str) -> None:
|
||||
self._execute(
|
||||
_FAIL_SQL,
|
||||
{
|
||||
"fid": int(fid),
|
||||
"status": STATUS_FAIL,
|
||||
"fail_reason": clip_reason(reason),
|
||||
"stamp": STAMP_DONE,
|
||||
"claim_stamp": STAMP_CLAIMING,
|
||||
},
|
||||
)
|
||||
|
||||
def _execute(self, sql: str, params: dict[str, Any]) -> None:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _row_from_cursor(cur, rec) -> CartRow:
|
||||
cols = [item[0].lower() for item in cur.description]
|
||||
return _row_from_dict(dict(zip(cols, rec)))
|
||||
|
||||
|
||||
def _pending_sql(
|
||||
*, limit: int | None, platform_types: list[int] | None
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
params: dict[str, Any] = {"selection_stamp": STAMP_SELECTION}
|
||||
filters = [
|
||||
"operate_type = 1",
|
||||
"status = 1",
|
||||
"push_status = 0",
|
||||
"NVL(update_user_name, :selection_stamp) = :selection_stamp",
|
||||
]
|
||||
if platform_types is not None:
|
||||
if not platform_types:
|
||||
filters.append("1 = 0")
|
||||
else:
|
||||
keys: list[str] = []
|
||||
for index, platform in enumerate(platform_types):
|
||||
key = f"p{index}"
|
||||
keys.append(f":{key}")
|
||||
params[key] = int(platform)
|
||||
filters.append(f"platform_type IN ({', '.join(keys)})")
|
||||
sql = f"""
|
||||
SELECT fid, oms_id, platform_type, supplier_name, bar_code,
|
||||
purchase_qty, status, fail_reason, push_status, update_user_name,
|
||||
operate_type
|
||||
FROM rpa_cart_settlement_goods_data
|
||||
WHERE {' AND '.join(filters)}
|
||||
ORDER BY oms_id, fid
|
||||
"""
|
||||
if limit is not None:
|
||||
params["limit"] = int(limit)
|
||||
sql = f"SELECT * FROM (\n{sql}\n) WHERE ROWNUM <= :limit"
|
||||
return sql, params
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"""再搜条形码,保留原始商品字段给加车用。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from cart_match import CartOffer
|
||||
from yaoex_login import SELECTION_ROOT
|
||||
|
||||
if str(SELECTION_ROOT) not in sys.path:
|
||||
sys.path.append(str(SELECTION_ROOT))
|
||||
|
||||
from search_yaoex import map_yaoex_products, query_yaoex_products # noqa: E402
|
||||
from search_ybm import map_ybm_products, query_ybm_products # noqa: E402
|
||||
|
||||
|
||||
def search_yaoex_offers(
|
||||
auth,
|
||||
query_key: str,
|
||||
session: requests.Session | None = None,
|
||||
cache: dict[str, list[CartOffer]] | None = None,
|
||||
) -> list[CartOffer]:
|
||||
store = cache if cache is not None else {}
|
||||
if query_key in store:
|
||||
return store[query_key]
|
||||
own = session is None
|
||||
client = session or requests.Session()
|
||||
try:
|
||||
products, user_id = query_yaoex_products(client, auth, query_key)
|
||||
candidates = map_yaoex_products(products, user_id)
|
||||
offers = [
|
||||
_from_candidate(candidate, raw)
|
||||
for candidate, raw in zip(candidates, products)
|
||||
]
|
||||
finally:
|
||||
if own:
|
||||
client.close()
|
||||
store[query_key] = offers
|
||||
return offers
|
||||
|
||||
|
||||
def search_ybm_offers(
|
||||
cookies: dict[str, str],
|
||||
query_key: str,
|
||||
session: requests.Session | None = None,
|
||||
cache: dict[str, list[CartOffer]] | None = None,
|
||||
) -> list[CartOffer]:
|
||||
store = cache if cache is not None else {}
|
||||
if query_key in store:
|
||||
return store[query_key]
|
||||
own = session is None
|
||||
client = session or requests.Session()
|
||||
try:
|
||||
if own:
|
||||
for name, value in cookies.items():
|
||||
client.cookies.set(name, value)
|
||||
rows = query_ybm_products(client, query_key)
|
||||
candidates = map_ybm_products(rows)
|
||||
offers = [
|
||||
_from_candidate(candidate, _ybm_raw(row))
|
||||
for candidate, row in zip(candidates, rows)
|
||||
]
|
||||
finally:
|
||||
if own:
|
||||
client.close()
|
||||
store[query_key] = offers
|
||||
return offers
|
||||
|
||||
|
||||
def _ybm_raw(row: dict[str, Any]) -> dict[str, Any]:
|
||||
product = row.get("productInfo")
|
||||
raw = dict(product) if isinstance(product, dict) else dict(row)
|
||||
if not raw.get("skuId"):
|
||||
raw["skuId"] = row.get("skuId")
|
||||
return raw
|
||||
|
||||
|
||||
def _from_candidate(candidate, raw: dict[str, Any]) -> CartOffer:
|
||||
extra = dict(candidate.extra)
|
||||
extra["raw"] = raw
|
||||
return CartOffer(
|
||||
sku_id=candidate.sku_id,
|
||||
vendor_raw=candidate.vendor_raw,
|
||||
vendor_norm=candidate.vendor_norm,
|
||||
stock=candidate.stock,
|
||||
price=candidate.price,
|
||||
extra=extra,
|
||||
)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
"""1 号药城加入购物车。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from cart_add import KIND_OK, KIND_SYSTEM, AddOutcome, classify_mall_message
|
||||
from cart_match import CartOffer
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
GATEWAY = "https://gateway-b2b.fangkuaiyi.com"
|
||||
YAOEX_ADD_URL = f"{GATEWAY}/small/shopCart/addShopCart"
|
||||
|
||||
|
||||
def add_yaoex_cart(auth, offer: CartOffer, qty: int, *, session: requests.Session | None = None) -> AddOutcome:
|
||||
ids = _ids(offer)
|
||||
if not ids["skuId"] and not ids["spuCode"]:
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="缺少 sku")
|
||||
payload = _form(
|
||||
auth,
|
||||
{
|
||||
"skuId": ids["skuId"],
|
||||
"spuCode": ids["spuCode"],
|
||||
"sellerCode": ids["sellerCode"],
|
||||
"amount": int(qty),
|
||||
},
|
||||
)
|
||||
own = session is None
|
||||
client = session or requests.Session()
|
||||
try:
|
||||
response = client.post(
|
||||
YAOEX_ADD_URL,
|
||||
data=payload,
|
||||
headers=_headers(),
|
||||
timeout=30,
|
||||
verify=False,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return _outcome_from_json(response.json())
|
||||
except Exception as exc:
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason=str(exc))
|
||||
finally:
|
||||
if own:
|
||||
client.close()
|
||||
|
||||
|
||||
def _ids(offer: CartOffer) -> dict[str, str]:
|
||||
raw = offer.extra.get("raw") if isinstance(offer.extra.get("raw"), dict) else offer.extra
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
sku = str(raw.get("skuId") or raw.get("id") or offer.sku_id or "")
|
||||
spu = str(raw.get("spuCode") or offer.sku_id or "")
|
||||
seller = str(raw.get("sellerCode") or raw.get("vendorCode") or raw.get("shopCode") or "")
|
||||
return {"skuId": sku, "spuCode": spu, "sellerCode": seller}
|
||||
|
||||
|
||||
def _form(auth, extra: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = {
|
||||
"traderName": "yaoex_pc",
|
||||
"trader": "pc",
|
||||
"closesignature": "yes",
|
||||
"signature_method": "md5",
|
||||
"signature": "****",
|
||||
"timestamp": str(int(time.time() * 1000)),
|
||||
"token": auth.token,
|
||||
"userToken": auth.token,
|
||||
"userId": getattr(auth, "user_id", ""),
|
||||
"buyerCode": getattr(auth, "buyer_code", ""),
|
||||
}
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _headers() -> dict[str, str]:
|
||||
return {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-Request-Agent": "Axios",
|
||||
"traderName": "yaoex_pc",
|
||||
"trader": "pc",
|
||||
"Origin": "https://mall.yaoex.com",
|
||||
"Referer": "https://mall.yaoex.com/v2/cart/",
|
||||
}
|
||||
|
||||
|
||||
def _outcome_from_json(payload: object) -> AddOutcome:
|
||||
if not isinstance(payload, dict):
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="加购返回异常")
|
||||
msg = str(payload.get("msg") or payload.get("message") or payload.get("errorMsg") or "")
|
||||
code = payload.get("code")
|
||||
success = payload.get("success")
|
||||
if success is True or code in (0, "0", 200, "200"):
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
if msg:
|
||||
return classify_mall_message(msg)
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="加购失败")
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
"""药帮忙加入购物车。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from cart_add import KIND_OK, KIND_SYSTEM, AddOutcome, classify_mall_message
|
||||
from cart_match import CartOffer
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
YBM_ADD_URL = "https://www.ybm100.com/new-front/cart/add-product"
|
||||
|
||||
|
||||
def add_ybm_cart(
|
||||
cookies: dict[str, str],
|
||||
offer: CartOffer,
|
||||
qty: int,
|
||||
*,
|
||||
session: requests.Session | None = None,
|
||||
) -> AddOutcome:
|
||||
sku = _sku_id(offer)
|
||||
if not sku:
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="缺少 sku")
|
||||
own = session is None
|
||||
client = session or requests.Session()
|
||||
try:
|
||||
if own:
|
||||
for name, value in cookies.items():
|
||||
client.cookies.set(name, value)
|
||||
response = client.post(
|
||||
YBM_ADD_URL,
|
||||
json={"skuId": sku, "skuNum": int(qty)},
|
||||
headers=_headers(),
|
||||
timeout=30,
|
||||
verify=False,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return _outcome_from_json(response.json())
|
||||
except Exception as exc:
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason=str(exc))
|
||||
finally:
|
||||
if own:
|
||||
client.close()
|
||||
|
||||
|
||||
def _sku_id(offer: CartOffer) -> str:
|
||||
raw = offer.extra.get("raw") if isinstance(offer.extra.get("raw"), dict) else offer.extra
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
return str(raw.get("skuId") or offer.sku_id or "")
|
||||
|
||||
|
||||
def _headers() -> dict[str, str]:
|
||||
return {
|
||||
"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"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _outcome_from_json(payload: object) -> AddOutcome:
|
||||
if not isinstance(payload, dict):
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="加购返回异常")
|
||||
msg = str(payload.get("msg") or payload.get("message") or "")
|
||||
success = payload.get("success")
|
||||
code = payload.get("code")
|
||||
if success is True or code in (0, "0", 200, "200"):
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
if msg:
|
||||
return classify_mall_message(msg)
|
||||
return AddOutcome(kind=KIND_SYSTEM, reason="加购失败")
|
||||
164
rpa_cart/main.py
164
rpa_cart/main.py
|
|
@ -1,4 +1,4 @@
|
|||
"""1 号药城支付落表:导出当天已付订单 → 写入表 2 operate_type=2。不对加购行。"""
|
||||
"""加购班与 1 号药城支付落表。默认支付;`cart` 子命令加购。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
|
@ -6,17 +6,44 @@ import tempfile
|
|||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from db import RepoUnavailable
|
||||
from job import run_yaoex_pay
|
||||
from parse_xls import parse_order_xls
|
||||
from repo import EmptyPayRepo, OraclePayRepo
|
||||
from yaoex_client import day_range, export_orders
|
||||
from yaoex_login import load_cart_dotenv, resolve_yaoex_auth
|
||||
from yaoex_login import (
|
||||
BrowserRuntime,
|
||||
load_cart_dotenv,
|
||||
resolve_yaoex_auth,
|
||||
resolve_ybm_cookies,
|
||||
)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
YAOEX_PLATFORM_TYPE = 66
|
||||
YBM_PLATFORM_TYPE = 48
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _parse_pay_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="1 号药城支付落表")
|
||||
parser.add_argument("--xls", type=Path, default=None, help="已导出的订单明细 xls,跳过商城导出")
|
||||
parser.add_argument(
|
||||
|
|
@ -33,8 +60,31 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _parse_cart_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="加购班:领表 2 选品成功行,按仓加进购物车")
|
||||
parser.add_argument("--limit", type=int, default=None, help="本班最多领取 N 行")
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=["all", "yaoex", "ybm"],
|
||||
default="all",
|
||||
help="all=两平台都跑",
|
||||
)
|
||||
parser.add_argument("--fresh-login", action="store_true", help="忽略登录缓存")
|
||||
parser.add_argument("--secrets", type=Path, default=None)
|
||||
parser.add_argument("--yaoex-auth", type=Path, default=None)
|
||||
parser.add_argument("--ybm-cookies", type=Path, default=None)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
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(args, repo=repo)
|
||||
|
||||
|
||||
def main_pay(args: argparse.Namespace, repo=None) -> None:
|
||||
load_cart_dotenv()
|
||||
xls_path = args.xls
|
||||
tmp_xls: Path | None = None
|
||||
|
|
@ -87,5 +137,115 @@ def main(argv: list[str] | None = None, repo=None) -> None:
|
|||
)
|
||||
|
||||
|
||||
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(
|
||||
secrets_path=args.secrets,
|
||||
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(
|
||||
secrets_path=args.secrets,
|
||||
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:
|
||||
|
||||
def search_yaoex(query_key: str):
|
||||
return search_yaoex_offers(
|
||||
yaoex_auth, query_key, session=yaoex_session, cache=yaoex_cache
|
||||
)
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "rpa-cart"
|
||||
version = "0.1.0"
|
||||
description = "洁达加购与支付回传。表 2 → 接口 2。本期先做 1 号药城支付落表。"
|
||||
description = "洁达加购与支付回传。表 2 加购班 + 1 号药城支付落表。不推接口 2。"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"oracledb>=2.4.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
from cart_add import KIND_BUSINESS, KIND_OK, KIND_SYSTEM, classify_mall_message
|
||||
from cart_match import CartOffer
|
||||
from cart_yaoex import YAOEX_ADD_URL, add_yaoex_cart
|
||||
from cart_ybm import YBM_ADD_URL, add_ybm_cart
|
||||
|
||||
|
||||
class FakeAuth:
|
||||
token = "tok"
|
||||
user_id = "u1"
|
||||
buyer_code = "b1"
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, json_data=None, status_code=200, text=""):
|
||||
self._json = json_data
|
||||
self.status_code = status_code
|
||||
self.text = text or str(json_data)
|
||||
self.headers = {"content-type": "application/json"}
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise RuntimeError(f"HTTP {self.status_code}")
|
||||
|
||||
def json(self):
|
||||
if self._json is None:
|
||||
raise ValueError("not json")
|
||||
return self._json
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, response: FakeResponse):
|
||||
self.response = response
|
||||
self.calls: list[dict] = []
|
||||
|
||||
def post(self, url, data=None, json=None, headers=None, timeout=None, verify=None, params=None):
|
||||
self.calls.append(
|
||||
{
|
||||
"url": url,
|
||||
"data": data,
|
||||
"json": json,
|
||||
"headers": headers,
|
||||
"params": params,
|
||||
}
|
||||
)
|
||||
return self.response
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
|
||||
def _offer(**overrides) -> CartOffer:
|
||||
extra = overrides.pop(
|
||||
"extra",
|
||||
{
|
||||
"raw": {
|
||||
"skuId": "SKU66",
|
||||
"spuCode": "SPU66",
|
||||
"sellerCode": "GD01",
|
||||
}
|
||||
},
|
||||
)
|
||||
data = dict(
|
||||
sku_id="SPU66",
|
||||
vendor_raw="广东壹号药业有限公司",
|
||||
vendor_norm="广东壹号药业有限公司",
|
||||
stock=20.0,
|
||||
price=90.0,
|
||||
extra=extra,
|
||||
)
|
||||
data.update(overrides)
|
||||
return CartOffer(**data)
|
||||
|
||||
|
||||
def test_classify_pack_and_stock_and_ok():
|
||||
pack = classify_mall_message("加入失败,数量须为中包装倍数")
|
||||
assert pack.kind == KIND_BUSINESS
|
||||
assert "中包装" in pack.reason
|
||||
stock = classify_mall_message("库存不足")
|
||||
assert stock.kind == KIND_BUSINESS
|
||||
assert stock.reason == "库存不足"
|
||||
ok = classify_mall_message("加入购物车成功")
|
||||
assert ok.kind == KIND_OK
|
||||
|
||||
|
||||
def test_yaoex_add_posts_token_and_qty():
|
||||
session = FakeSession(FakeResponse({"code": 0, "msg": "成功"}))
|
||||
outcome = add_yaoex_cart(FakeAuth(), _offer(), 7, session=session)
|
||||
assert outcome.kind == KIND_OK
|
||||
assert session.calls[0]["url"] == YAOEX_ADD_URL
|
||||
payload = session.calls[0]["data"]
|
||||
assert payload["token"] == "tok"
|
||||
assert payload["userToken"] == "tok"
|
||||
assert str(payload["amount"]) == "7"
|
||||
assert payload["skuId"] == "SKU66"
|
||||
assert payload["spuCode"] == "SPU66"
|
||||
assert payload["sellerCode"] == "GD01"
|
||||
|
||||
|
||||
def test_yaoex_add_classifies_pack_error():
|
||||
session = FakeSession(FakeResponse({"code": 1, "msg": "数量必须是中包装的倍数"}))
|
||||
outcome = add_yaoex_cart(FakeAuth(), _offer(), 7, session=session)
|
||||
assert outcome.kind == KIND_BUSINESS
|
||||
assert "中包装" in outcome.reason
|
||||
|
||||
|
||||
def test_ybm_add_posts_sku_and_qty():
|
||||
session = FakeSession(FakeResponse({"success": True, "msg": "成功"}))
|
||||
offer = _offer(sku_id="SKU48", extra={"raw": {"skuId": "SKU48"}})
|
||||
outcome = add_ybm_cart({"SESSION": "c1"}, offer, 3, session=session)
|
||||
assert outcome.kind == KIND_OK
|
||||
assert session.calls[0]["url"] == YBM_ADD_URL
|
||||
body = session.calls[0]["json"]
|
||||
assert body["skuId"] == "SKU48"
|
||||
assert body["skuNum"] == 3
|
||||
|
||||
|
||||
def test_ybm_http_error_is_system_fail():
|
||||
session = FakeSession(FakeResponse(json_data=None, status_code=500, text="bad"))
|
||||
outcome = add_ybm_cart({}, _offer(extra={"raw": {"skuId": "X"}}), 1, session=session)
|
||||
assert outcome.kind == KIND_SYSTEM
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
from cart_job import KIND_BUSINESS, KIND_OK, KIND_SYSTEM, AddOutcome, run_cart_batch
|
||||
from cart_match import CartOffer
|
||||
from cart_repo import STAMP_DONE, STAMP_SELECTION, MemoryCartRepo
|
||||
|
||||
|
||||
HANGZHOU = "杭州小药药自营旗舰店"
|
||||
GUANGDONG = "广东壹号药业有限公司"
|
||||
YBM = 48
|
||||
YAOEX = 66
|
||||
|
||||
|
||||
def _row(**overrides) -> dict:
|
||||
data = {
|
||||
"fid": 1,
|
||||
"oms_id": 2026100201,
|
||||
"platform_type": YAOEX,
|
||||
"supplier_name": GUANGDONG,
|
||||
"bar_code": "06901234567890",
|
||||
"purchase_qty": 7,
|
||||
"status": 1,
|
||||
"fail_reason": None,
|
||||
"push_status": 0,
|
||||
"update_user_name": STAMP_SELECTION,
|
||||
"operate_type": 1,
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def _offer(vendor: str, **overrides) -> CartOffer:
|
||||
data = dict(
|
||||
sku_id="S-" + vendor[:2],
|
||||
vendor_raw=vendor,
|
||||
vendor_norm=vendor,
|
||||
stock=20.0,
|
||||
price=90.0,
|
||||
)
|
||||
data.update(overrides)
|
||||
return CartOffer(**data)
|
||||
|
||||
|
||||
def test_job_adds_split_rows_without_insert():
|
||||
repo = MemoryCartRepo(
|
||||
[
|
||||
_row(fid=1, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
|
||||
_row(fid=2, platform_type=YAOEX, supplier_name=GUANGDONG, purchase_qty=7),
|
||||
]
|
||||
)
|
||||
added: list[tuple[int, str, int]] = []
|
||||
|
||||
def search_ybm(bar_code: str):
|
||||
assert bar_code == "06901234567890"
|
||||
return [_offer(HANGZHOU, sku_id="HZ", stock=5.0)]
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
assert bar_code == "06901234567890"
|
||||
return [_offer(GUANGDONG, sku_id="GD", stock=20.0)]
|
||||
|
||||
def add_ybm(offer: CartOffer, qty: int):
|
||||
added.append((YBM, offer.sku_id, qty))
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
|
||||
def add_yaoex(offer: CartOffer, qty: int):
|
||||
added.append((YAOEX, offer.sku_id, qty))
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YBM: search_ybm, YAOEX: search_yaoex},
|
||||
adders={YBM: add_ybm, YAOEX: add_yaoex},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.claimed == 2
|
||||
assert summary.added == 2
|
||||
assert summary.failed == 0
|
||||
assert added == [(YBM, "HZ", 3), (YAOEX, "GD", 7)]
|
||||
assert list(repo.rows) == [1, 2]
|
||||
assert repo.rows[1]["status"] == 1
|
||||
assert repo.rows[2]["status"] == 1
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
assert repo.rows[2]["update_user_name"] == STAMP_DONE
|
||||
assert repo.rows[1]["push_status"] == 0
|
||||
|
||||
|
||||
def test_job_marks_out_of_stock():
|
||||
repo = MemoryCartRepo([_row(purchase_qty=10)])
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
return [_offer(GUANGDONG, stock=4.0)]
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.added == 0
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["status"] == 2
|
||||
assert repo.rows[1]["fail_reason"] == "库存不足"
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
|
||||
|
||||
def test_job_marks_login_fail_as_system_error():
|
||||
repo = MemoryCartRepo(
|
||||
[
|
||||
_row(fid=1, platform_type=YAOEX),
|
||||
_row(fid=2, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
|
||||
]
|
||||
)
|
||||
ybm_added = []
|
||||
|
||||
def search_ybm(bar_code: str):
|
||||
return [_offer(HANGZHOU)]
|
||||
|
||||
def add_ybm(offer: CartOffer, qty: int):
|
||||
ybm_added.append(qty)
|
||||
return AddOutcome(kind=KIND_OK)
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YBM: search_ybm},
|
||||
adders={YBM: add_ybm},
|
||||
login_errors={YAOEX: "账号密码错误"},
|
||||
)
|
||||
assert summary.added == 1
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["status"] == 2
|
||||
assert repo.rows[1]["fail_reason"].startswith("系统故障:")
|
||||
assert "账号密码错误" in repo.rows[1]["fail_reason"]
|
||||
assert repo.rows[2]["status"] == 1
|
||||
assert repo.rows[2]["update_user_name"] == STAMP_DONE
|
||||
assert ybm_added == [3]
|
||||
|
||||
|
||||
def test_job_skips_already_stamped_rows():
|
||||
repo = MemoryCartRepo([_row(update_user_name=STAMP_DONE)])
|
||||
called = []
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
called.append(bar_code)
|
||||
return [_offer(GUANGDONG)]
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.claimed == 0
|
||||
assert called == []
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
|
||||
|
||||
def test_job_marks_missing_warehouse():
|
||||
repo = MemoryCartRepo([_row()])
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
return [_offer(HANGZHOU)]
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["status"] == 2
|
||||
assert repo.rows[1]["fail_reason"] == "未找到对应仓"
|
||||
|
||||
|
||||
def test_job_marks_pack_reject():
|
||||
repo = MemoryCartRepo([_row(purchase_qty=7)])
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
return [_offer(GUANGDONG, extra={"raw": {"middlePackNum": 10}})]
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["status"] == 2
|
||||
assert repo.rows[1]["fail_reason"].startswith("中包装加不进")
|
||||
|
||||
|
||||
def test_job_records_mall_add_failure():
|
||||
repo = MemoryCartRepo([_row()])
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
return [_offer(GUANGDONG)]
|
||||
|
||||
def add_yaoex(offer: CartOffer, qty: int):
|
||||
return AddOutcome(kind=KIND_BUSINESS, reason="中包装加不进:数量须为10的倍数")
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: add_yaoex},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["fail_reason"] == "中包装加不进:数量须为10的倍数"
|
||||
|
||||
|
||||
def test_job_platform_filter_leaves_other_mall_pending():
|
||||
repo = MemoryCartRepo(
|
||||
[
|
||||
_row(fid=1, platform_type=YAOEX),
|
||||
_row(fid=2, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
|
||||
]
|
||||
)
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
return [_offer(GUANGDONG)]
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
platform_types=[YAOEX],
|
||||
)
|
||||
assert summary.claimed == 1
|
||||
assert summary.added == 1
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
assert repo.rows[2]["update_user_name"] == STAMP_SELECTION
|
||||
assert repo.rows[2]["status"] == 1
|
||||
|
||||
|
||||
def test_job_search_error_is_system_fail():
|
||||
repo = MemoryCartRepo([_row()])
|
||||
|
||||
def search_yaoex(bar_code: str):
|
||||
raise RuntimeError("网关 502")
|
||||
|
||||
summary = run_cart_batch(
|
||||
repo,
|
||||
searchers={YAOEX: search_yaoex},
|
||||
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
|
||||
login_errors={},
|
||||
)
|
||||
assert summary.failed == 1
|
||||
assert repo.rows[1]["fail_reason"].startswith("系统故障:")
|
||||
assert "网关 502" in repo.rows[1]["fail_reason"]
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
from cart_match import CartOffer, match_offer, middle_pack, qty_fits_pack
|
||||
|
||||
|
||||
def _offer(**overrides) -> CartOffer:
|
||||
data = dict(
|
||||
sku_id="S1",
|
||||
vendor_raw="广东壹号药业有限公司",
|
||||
vendor_norm="广东壹号药业有限公司",
|
||||
stock=20.0,
|
||||
price=90.0,
|
||||
)
|
||||
data.update(overrides)
|
||||
return CartOffer(**data)
|
||||
|
||||
|
||||
def test_matches_supplier_name_exact():
|
||||
picked = match_offer(
|
||||
[_offer(vendor_raw="杭州小药药自营旗舰店", vendor_norm="杭州小药药自营旗舰店"), _offer()],
|
||||
"广东壹号药业有限公司",
|
||||
)
|
||||
assert picked is not None
|
||||
assert picked.sku_id == "S1"
|
||||
assert picked.vendor_raw == "广东壹号药业有限公司"
|
||||
|
||||
|
||||
def test_matches_supplier_name_stripping_self_operated_prefix():
|
||||
picked = match_offer(
|
||||
[_offer(vendor_raw="[自营] 广东壹号药业有限公司", vendor_norm="广东壹号药业有限公司")],
|
||||
"广东壹号药业有限公司",
|
||||
)
|
||||
assert picked is not None
|
||||
assert picked.vendor_norm == "广东壹号药业有限公司"
|
||||
|
||||
|
||||
def test_same_supplier_keeps_cheaper():
|
||||
picked = match_offer(
|
||||
[
|
||||
_offer(sku_id="DEAR", price=94.0, stock=100.0),
|
||||
_offer(sku_id="CHEAP", price=90.0, stock=5.0),
|
||||
],
|
||||
"广东壹号药业有限公司",
|
||||
)
|
||||
assert picked is not None
|
||||
assert picked.sku_id == "CHEAP"
|
||||
|
||||
|
||||
def test_missing_supplier_returns_none():
|
||||
assert match_offer([_offer()], "杭州小药药自营旗舰店") is None
|
||||
|
||||
|
||||
def test_qty_not_multiple_of_middle_pack():
|
||||
offer = _offer(extra={"raw": {"middlePackNum": 10}})
|
||||
assert middle_pack(offer) == 10
|
||||
assert qty_fits_pack(7, 10) is False
|
||||
assert qty_fits_pack(10, 10) is True
|
||||
assert qty_fits_pack(7, None) is True
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
from cart_repo import (
|
||||
STAMP_CLAIMING,
|
||||
STAMP_DONE,
|
||||
STAMP_SELECTION,
|
||||
MemoryCartRepo,
|
||||
OracleCartRepo,
|
||||
)
|
||||
|
||||
|
||||
def _row(**overrides) -> dict:
|
||||
data = {
|
||||
"fid": 1,
|
||||
"oms_id": 2026100201,
|
||||
"platform_type": 66,
|
||||
"supplier_name": "广东壹号药业有限公司",
|
||||
"bar_code": "06901234567890",
|
||||
"purchase_qty": 7,
|
||||
"status": 1,
|
||||
"fail_reason": None,
|
||||
"push_status": 0,
|
||||
"update_user_name": STAMP_SELECTION,
|
||||
"operate_type": 1,
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def test_claim_pending_stamps_processing_and_skips_done():
|
||||
repo = MemoryCartRepo(
|
||||
[
|
||||
_row(),
|
||||
_row(fid=2, oms_id=2026100202, update_user_name=STAMP_DONE),
|
||||
_row(fid=3, oms_id=2026100203, status=2, update_user_name=STAMP_SELECTION),
|
||||
_row(fid=4, oms_id=2026100204, operate_type=2),
|
||||
]
|
||||
)
|
||||
claimed = repo.claim_pending()
|
||||
assert [row.fid for row in claimed] == [1]
|
||||
assert claimed[0].update_user_name == STAMP_CLAIMING
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_CLAIMING
|
||||
assert repo.rows[2]["update_user_name"] == STAMP_DONE
|
||||
assert repo.rows[3]["status"] == 2
|
||||
|
||||
|
||||
def test_mark_success_does_not_insert_or_change_status():
|
||||
repo = MemoryCartRepo([_row()])
|
||||
repo.claim_pending()
|
||||
repo.mark_success(1)
|
||||
assert list(repo.rows) == [1]
|
||||
assert repo.rows[1]["status"] == 1
|
||||
assert repo.rows[1]["fail_reason"] is None
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
assert repo.rows[1]["push_status"] == 0
|
||||
|
||||
|
||||
def test_mark_fail_sets_status_and_reason():
|
||||
repo = MemoryCartRepo([_row()])
|
||||
repo.claim_pending()
|
||||
repo.mark_fail(1, "库存不足")
|
||||
assert list(repo.rows) == [1]
|
||||
assert repo.rows[1]["status"] == 2
|
||||
assert repo.rows[1]["fail_reason"] == "库存不足"
|
||||
assert repo.rows[1]["update_user_name"] == STAMP_DONE
|
||||
|
||||
|
||||
def test_oracle_claim_updates_stamp():
|
||||
fetched = [
|
||||
(
|
||||
11,
|
||||
2026100201,
|
||||
66,
|
||||
"广东壹号药业有限公司",
|
||||
"0690",
|
||||
7,
|
||||
1,
|
||||
None,
|
||||
0,
|
||||
STAMP_SELECTION,
|
||||
1,
|
||||
)
|
||||
]
|
||||
executed: list[tuple[str, dict]] = []
|
||||
|
||||
class FakeCursor:
|
||||
description = [
|
||||
("FID",),
|
||||
("OMS_ID",),
|
||||
("PLATFORM_TYPE",),
|
||||
("SUPPLIER_NAME",),
|
||||
("BAR_CODE",),
|
||||
("PURCHASE_QTY",),
|
||||
("STATUS",),
|
||||
("FAIL_REASON",),
|
||||
("PUSH_STATUS",),
|
||||
("UPDATE_USER_NAME",),
|
||||
("OPERATE_TYPE",),
|
||||
]
|
||||
rowcount = 1
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def execute(self, sql, params=None):
|
||||
executed.append((sql, params or {}))
|
||||
|
||||
def fetchall(self):
|
||||
return fetched
|
||||
|
||||
class FakeConn:
|
||||
committed = False
|
||||
|
||||
def cursor(self):
|
||||
return FakeCursor()
|
||||
|
||||
def commit(self):
|
||||
self.committed = True
|
||||
|
||||
def rollback(self):
|
||||
raise AssertionError("不应 rollback")
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
conn = FakeConn()
|
||||
repo = OracleCartRepo(connect=lambda: conn)
|
||||
claimed = repo.claim_pending()
|
||||
assert conn.committed is True
|
||||
assert len(claimed) == 1
|
||||
assert claimed[0].fid == 11
|
||||
assert claimed[0].update_user_name == STAMP_CLAIMING
|
||||
assert "update_user_name" in executed[1][0].lower()
|
||||
assert executed[1][1]["fid"] == 11
|
||||
assert executed[1][1]["stamp"] == STAMP_CLAIMING
|
||||
|
|
@ -6,9 +6,18 @@ def test_parse_args_defaults():
|
|||
assert args.days == 0
|
||||
assert args.dry_run is False
|
||||
assert args.xls is None
|
||||
assert args.command == "pay"
|
||||
|
||||
|
||||
def test_parse_args_xls_dry_run():
|
||||
args = parse_args(["--xls", "/tmp/orders.xls", "--dry-run"])
|
||||
assert args.dry_run is True
|
||||
assert str(args.xls).endswith("orders.xls")
|
||||
assert args.command == "pay"
|
||||
|
||||
|
||||
def test_parse_args_cart_subcommand():
|
||||
args = parse_args(["cart", "--limit", "5", "--platform", "yaoex"])
|
||||
assert args.command == "cart"
|
||||
assert args.limit == 5
|
||||
assert args.platform == "yaoex"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""复用 rpa_selection 的 1 号药城登录。"""
|
||||
"""复用 rpa_selection 的 1 号药城 / 药帮忙登录。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
|
@ -9,12 +9,16 @@ if str(SELECTION_ROOT) not in sys.path:
|
|||
sys.path.append(str(SELECTION_ROOT))
|
||||
|
||||
from auth_login import ( # noqa: E402
|
||||
DEFAULT_YBM_COOKIE_PATH,
|
||||
DEFAULT_YAOEX_AUTH_PATH,
|
||||
BrowserRuntime,
|
||||
load_cookie_file,
|
||||
load_dotenv,
|
||||
load_secrets,
|
||||
load_yaoex_auth_file,
|
||||
login_yaoex,
|
||||
login_ybm,
|
||||
save_cookie_file,
|
||||
save_yaoex_auth,
|
||||
)
|
||||
from search_yaoex import YaoexAuth # noqa: E402
|
||||
|
|
@ -49,3 +53,29 @@ def resolve_yaoex_auth(
|
|||
finally:
|
||||
if own:
|
||||
session.close()
|
||||
|
||||
|
||||
def resolve_ybm_cookies(
|
||||
*,
|
||||
secrets_path: Path | None = None,
|
||||
cookie_path: Path | None = None,
|
||||
fresh: bool = False,
|
||||
runtime: BrowserRuntime | None = None,
|
||||
) -> dict[str, str]:
|
||||
cached = cookie_path or DEFAULT_YBM_COOKIE_PATH
|
||||
if not fresh and cached.exists():
|
||||
cookies = load_cookie_file(cached)
|
||||
if cookies:
|
||||
return cookies
|
||||
secrets = load_secrets(secrets_path)
|
||||
if not secrets["ybm"].get("username"):
|
||||
raise SystemExit("缺少药帮忙账号。配置 rpa_selection/.env 或环境变量 YBM_USERNAME / YBM_PASSWORD。")
|
||||
own = runtime is None
|
||||
session = runtime or BrowserRuntime(keep=False)
|
||||
try:
|
||||
cookies = login_ybm(secrets_path, runtime=session)
|
||||
save_cookie_file(cookies, cached)
|
||||
return cookies
|
||||
finally:
|
||||
if own:
|
||||
session.close()
|
||||
|
|
|
|||
Loading…
Reference in New Issue