diff --git a/AGENTS.md b/AGENTS.md index 3f22b67..bdc6534 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ | 节点入口 | `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` | 占位,未实现 | +| 加购 / 支付回传 | `rpa_cart/README.md` | 加购未做;1 号药城支付按 Excel 落表,不对加购行 | | 返利班 | `rpa_rebate/README.md` | 占位,未实现 | | 四个口地图 | `rpa_oms/README.md`、根 `README.md` | — | | 节点业务规则(点名一节) | `docs/RPA订单中心接口开发口径_20260930.md` | 禁止通读 `docs/` | diff --git a/rpa_cart/README.md b/rpa_cart/README.md index 0a18446..2bd4da7 100644 --- a/rpa_cart/README.md +++ b/rpa_cart/README.md @@ -9,7 +9,7 @@ ## 1 号药城支付落表 -从买家中心导出已付订单,按条码+商家对加购成功行,写入表 2 `operate_type=2`。 +从买家中心导出已付订单,按导出行字段写入表 2 `operate_type=2`。不对加购行。购物车和支付单没有强关联,`oms_id` 表上 NOT NULL,先填 `0`,等人后选。 ```bash cd rpa_cart @@ -23,7 +23,7 @@ uv run python main.py --day 2026-09-30 --days 0 过滤:只落 待发货 / 待收货 / 已完成。单价用折后单价,总价 `round(折后单价×数量, 4)`。`pay_order_no` 用商城订单号,`pay_order_dtl_id` 用详情接口 `orderDetailId`。 -幂等:同一 `pay_order_no + pay_order_dtl_id` 不重复插入。对不上加购行或拉不到细单号的留下,不猜 `oms_id`。 +幂等:同一 `pay_order_no + pay_order_dtl_id` 不重复插入。细单号拉不到的留下,不猜。 ```bash uv run pytest -v @@ -31,7 +31,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` 一起带过去。 @@ -55,7 +55,7 @@ uv run python main.py --dry-run --day 2026-09-30 --days 0 uv run python main.py --day 2026-09-30 --days 0 ``` -看控制台 `inserted / unmatched`。对不上加购行是 `NO_CART_ROW`(表 2 还没有对应 `operate_type=1, status=1, platform=66` 的行)。细单号拉不到是 `NO_DTL_ID`。 +看控制台 `inserted / unmatched`。细单号拉不到是 `NO_DTL_ID`。 查库: diff --git a/rpa_cart/job.py b/rpa_cart/job.py index 28c2b30..07f0f58 100644 --- a/rpa_cart/job.py +++ b/rpa_cart/job.py @@ -29,11 +29,7 @@ def run_yaoex_pay( dry_run: bool = False, ) -> PaySummary: attached = attach_detail_ids(lines, details) - result = match_pay_lines( - attached, - repo.load_cart_candidates(), - repo.load_existing_pay_keys(), - ) + result = match_pay_lines(attached, repo.load_existing_pay_keys()) if result.rows and not dry_run: repo.save_pay_rows(result.rows) reasons: dict[str, int] = {} diff --git a/rpa_cart/main.py b/rpa_cart/main.py index 5873daf..168b2de 100644 --- a/rpa_cart/main.py +++ b/rpa_cart/main.py @@ -1,4 +1,4 @@ -"""1 号药城支付落表:导出当天已付订单 → 对加购行 → 写入表 2 operate_type=2。""" +"""1 号药城支付落表:导出当天已付订单 → 写入表 2 operate_type=2。不对加购行。""" from __future__ import annotations import argparse @@ -27,7 +27,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: help="导出日期跨度,0=仅当天。与 --xls 互斥时仍用于无 xls 的导出", ) parser.add_argument("--day", type=str, default=None, help="结束日期 YYYY-MM-DD,默认今天") - parser.add_argument("--dry-run", action="store_true", help="对码但不写库") + parser.add_argument("--dry-run", action="store_true", help="解析但不写库") 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) @@ -91,7 +91,7 @@ def main(argv: list[str] | None = None, repo=None) -> None: if not args.dry_run: print(f"数据库不可用:{exc}") raise SystemExit(2) from exc - print(f"无库,dry-run 只解析不对加购行:{exc}") + print(f"无库,dry-run 只解析:{exc}") repo = EmptyPayRepo() summary = run_yaoex_pay(repo, lines, details, dry_run=args.dry_run) diff --git a/rpa_cart/repo.py b/rpa_cart/repo.py index e8afbcd..d202607 100644 --- a/rpa_cart/repo.py +++ b/rpa_cart/repo.py @@ -1,21 +1,11 @@ -"""表 2:读加购成功行,写支付行。""" +"""表 2:写支付行。""" from __future__ import annotations from collections.abc import Callable from typing import Any from db import CREATE_USER_NAME, RepoUnavailable, connect_from_env, oracle_dsn -from settle import CartCandidate, YAOEX_PLATFORM_TYPE - -_SELECT_CART = """ -SELECT fid, oms_id, platform_type, supplier_name, bar_code, - purchase_qty, NVL(org_code, '') AS org_code, multi_platform_flag -FROM rpa_cart_settlement_goods_data -WHERE operate_type = 1 - AND status = 1 - AND platform_type = :platform_type -ORDER BY fid -""" +from settle import YAOEX_PLATFORM_TYPE _SELECT_PAY_KEYS = """ SELECT pay_order_no, pay_order_dtl_id @@ -60,9 +50,6 @@ _PAY_BIND_KEYS = ( class EmptyPayRepo: - def load_cart_candidates(self) -> list[CartCandidate]: - return [] - def load_existing_pay_keys(self) -> set[tuple[str, str]]: return set() @@ -79,17 +66,6 @@ class OraclePayRepo: oracle_dsn() return cls(connect=connect_from_env) - def load_cart_candidates(self) -> list[CartCandidate]: - conn = self._connect() - try: - with conn.cursor() as cur: - cur.execute(_SELECT_CART, {"platform_type": YAOEX_PLATFORM_TYPE}) - return [_cart_from_rec(cur, rec) for rec in cur.fetchall()] - except Exception as exc: - raise RepoUnavailable(str(exc)) from exc - finally: - conn.close() - def load_existing_pay_keys(self) -> set[tuple[str, str]]: conn = self._connect() try: @@ -124,17 +100,3 @@ class OraclePayRepo: raise RepoUnavailable(str(exc)) from exc finally: conn.close() - - -def _cart_from_rec(cur, rec) -> CartCandidate: - cols = [item[0].lower() for item in cur.description] - data = dict(zip(cols, rec)) - return CartCandidate( - fid=int(data["fid"]), - oms_id=int(data["oms_id"]), - supplier_name=str(data.get("supplier_name") or ""), - bar_code=str(data.get("bar_code") or ""), - purchase_qty=int(data.get("purchase_qty") or 0), - org_code=str(data.get("org_code") or ""), - multi_platform_flag=int(data.get("multi_platform_flag") or 0), - ) diff --git a/rpa_cart/settle.py b/rpa_cart/settle.py index 30191a8..412eaaa 100644 --- a/rpa_cart/settle.py +++ b/rpa_cart/settle.py @@ -1,4 +1,4 @@ -"""1 号药城导出行 → 表 2 operate_type=2。""" +"""1 号药城导出行 → 表 2 operate_type=2。不对加购行。""" from __future__ import annotations from dataclasses import dataclass, replace @@ -8,6 +8,7 @@ from typing import Any YAOEX_PLATFORM_TYPE = 66 OPERATE_TYPE_PAY = 2 PUSH_NOT_SENT = 0 +UNASSIGNED_OMS_ID = 0 PAID_STATUSES = {"待发货", "待收货", "已完成"} @@ -32,17 +33,6 @@ class MallLine: pay_order_dtl_id: str = "" -@dataclass(frozen=True) -class CartCandidate: - fid: int - oms_id: int - supplier_name: str - bar_code: str - purchase_qty: int - org_code: str - multi_platform_flag: int - - @dataclass class MatchResult: rows: list[dict[str, Any]] @@ -73,11 +63,9 @@ def attach_detail_ids( def match_pay_lines( lines: list[MallLine], - cart_rows: list[CartCandidate], existing_keys: set[tuple[str, str]], ) -> MatchResult: result = MatchResult(rows=[], unmatched=[], skipped_dup=[], skipped_status=[]) - unused = list(cart_rows) seen = set(existing_keys) for line in lines: if line.status not in PAID_STATUSES: @@ -90,22 +78,17 @@ def match_pay_lines( if key in seen: result.skipped_dup.append(line) continue - cart = _pick_cart(unused, line) - if cart is None: - result.unmatched.append((line, "NO_CART_ROW")) - continue - unused.remove(cart) seen.add(key) - result.rows.append(build_pay_row(line, cart)) + result.rows.append(build_pay_row(line)) return result -def build_pay_row(line: MallLine, cart: CartCandidate) -> dict[str, Any]: +def build_pay_row(line: MallLine) -> dict[str, Any]: price = money4(line.pay_price) qty = int(line.qty) return { "operate_type": OPERATE_TYPE_PAY, - "oms_id": cart.oms_id, + "oms_id": UNASSIGNED_OMS_ID, "platform_type": YAOEX_PLATFORM_TYPE, "supplier_name": line.supplier_name, "bar_code": line.bar_code, @@ -117,24 +100,11 @@ def build_pay_row(line: MallLine, cart: CartCandidate) -> dict[str, Any]: "pay_order_no": line.order_no, "pay_order_dtl_id": line.pay_order_dtl_id, "push_status": PUSH_NOT_SENT, - "org_code": cart.org_code or None, - "multi_platform_flag": cart.multi_platform_flag, + "org_code": None, + "multi_platform_flag": 0, } -def _pick_cart(unused: list[CartCandidate], line: MallLine) -> CartCandidate | None: - matched = [ - row - for row in unused - if row.bar_code == line.bar_code and row.supplier_name == line.supplier_name - ] - if not matched: - return None - equal_qty = [row for row in matched if row.purchase_qty == line.qty] - pool = equal_qty or matched - return sorted(pool, key=lambda row: row.fid)[0] - - def _find_product(pool: list[dict[str, Any]], line: MallLine) -> int | None: for idx, product in enumerate(pool): qty = int(Decimal(str(product.get("quantity") or 0))) diff --git a/rpa_cart/tests/test_job.py b/rpa_cart/tests/test_job.py index 4ec4a6d..988e2ff 100644 --- a/rpa_cart/tests/test_job.py +++ b/rpa_cart/tests/test_job.py @@ -1,18 +1,14 @@ from decimal import Decimal from job import run_yaoex_pay -from settle import CartCandidate, MallLine +from settle import MallLine class MemoryPayRepo: - def __init__(self, carts: list[CartCandidate], existing=None): - self.carts = carts + def __init__(self, existing=None): self.existing = existing or set() self.saved: list[dict] = [] - def load_cart_candidates(self): - return list(self.carts) - def load_existing_pay_keys(self): return set(self.existing) @@ -40,20 +36,8 @@ def _line(**overrides) -> MallLine: return MallLine(**data) -def test_job_attaches_dtl_matches_cart_and_inserts(): - repo = MemoryPayRepo( - [ - CartCandidate( - fid=1, - oms_id=2026100201, - supplier_name="广东壹号药业有限公司", - bar_code="6921665004933", - purchase_qty=10, - org_code="901", - multi_platform_flag=0, - ) - ] - ) +def test_job_attaches_dtl_and_inserts_without_cart(): + repo = MemoryPayRepo() details = { "ZQD1": { "productList": [ @@ -68,25 +52,14 @@ def test_job_attaches_dtl_matches_cart_and_inserts(): } summary = run_yaoex_pay(repo, [_line()], details) assert summary.inserted == 1 - assert repo.saved[0]["oms_id"] == 2026100201 + assert summary.unmatched == 0 + assert repo.saved[0]["oms_id"] == 0 assert repo.saved[0]["pay_order_dtl_id"] == "881" assert repo.saved[0]["purchase_total"] == Decimal("98.5000") def test_job_dry_run_does_not_insert(): - repo = MemoryPayRepo( - [ - CartCandidate( - fid=1, - oms_id=1, - supplier_name="广东壹号药业有限公司", - bar_code="6921665004933", - purchase_qty=10, - org_code="901", - multi_platform_flag=0, - ) - ] - ) + repo = MemoryPayRepo() details = { "ZQD1": { "productList": [ diff --git a/rpa_cart/tests/test_repo.py b/rpa_cart/tests/test_repo.py index 8520a2f..df9c39e 100644 --- a/rpa_cart/tests/test_repo.py +++ b/rpa_cart/tests/test_repo.py @@ -1,53 +1,6 @@ from repo import OraclePayRepo -def test_load_cart_candidates_filters_yaoex_add_cart_success(): - fetched = [ - (1, 11, 66, "广东壹号药业有限公司", "6921", 10, "901", 0), - ] - sql_holder: dict[str, str] = {} - - class FakeCursor: - description = [ - ("FID",), - ("OMS_ID",), - ("PLATFORM_TYPE",), - ("SUPPLIER_NAME",), - ("BAR_CODE",), - ("PURCHASE_QTY",), - ("ORG_CODE",), - ("MULTI_PLATFORM_FLAG",), - ] - - def __enter__(self): - return self - - def __exit__(self, *args): - return False - - def execute(self, sql, params=None): - sql_holder["sql"] = sql - sql_holder["params"] = params or {} - - def fetchall(self): - return fetched - - class FakeConn: - def cursor(self): - return FakeCursor() - - def close(self): - pass - - repo = OraclePayRepo(connect=lambda: FakeConn()) - rows = repo.load_cart_candidates() - assert len(rows) == 1 - assert rows[0].oms_id == 11 - assert rows[0].bar_code == "6921" - assert "operate_type = 1" in sql_holder["sql"].lower() - assert sql_holder["params"]["platform_type"] == 66 - - def test_save_pay_rows_inserts_operate_type_2(): executed: list[tuple[str, dict]] = [] @@ -86,7 +39,7 @@ def test_save_pay_rows_inserts_operate_type_2(): [ { "operate_type": 2, - "oms_id": 11, + "oms_id": 0, "platform_type": 66, "supplier_name": "广东壹号药业有限公司", "bar_code": "6921", @@ -98,7 +51,7 @@ def test_save_pay_rows_inserts_operate_type_2(): "pay_order_no": "ZQD1", "pay_order_dtl_id": "881", "push_status": 0, - "org_code": "901", + "org_code": None, "multi_platform_flag": 0, } ] @@ -106,6 +59,7 @@ def test_save_pay_rows_inserts_operate_type_2(): assert conn.committed is True assert "rpa_cart_settlement_goods_data" in executed[0][0].lower() assert executed[0][1]["operate_type"] == 2 + assert executed[0][1]["oms_id"] == 0 assert executed[0][1]["pay_order_no"] == "ZQD1" assert executed[0][1]["pay_order_dtl_id"] == "881" diff --git a/rpa_cart/tests/test_settle.py b/rpa_cart/tests/test_settle.py index a0c0403..d704858 100644 --- a/rpa_cart/tests/test_settle.py +++ b/rpa_cart/tests/test_settle.py @@ -2,7 +2,6 @@ from decimal import Decimal from settle import ( PAID_STATUSES, - CartCandidate, MallLine, attach_detail_ids, build_pay_row, @@ -31,20 +30,6 @@ def _line(**overrides) -> MallLine: return MallLine(**data) -def _cart(**overrides) -> CartCandidate: - data = dict( - fid=1, - oms_id=2026100201, - supplier_name="广东壹号药业有限公司", - bar_code="6921665004933", - purchase_qty=200, - org_code="901", - multi_platform_flag=0, - ) - data.update(overrides) - return CartCandidate(**data) - - def test_money4_rounds_half_up(): assert money4("65.49") == Decimal("65.4900") assert money4(Decimal("9.85") * 200) == Decimal("1970.0000") @@ -58,12 +43,7 @@ def test_drops_cancelled_and_keeps_paid_statuses(): _line(status="已完成", order_no="P3"), _line(status="退款/退换货", order_no="R1"), ] - carts = [ - _cart(fid=1, oms_id=1), - _cart(fid=2, oms_id=2), - _cart(fid=3, oms_id=3), - ] - result = match_pay_lines(lines, carts, existing_keys=set()) + result = match_pay_lines(lines, existing_keys=set()) kept = {row["pay_order_no"] for row in result.rows} assert kept == {"P1", "P2", "P3"} assert {line.order_no for line in result.skipped_status} == {"C1", "R1"} @@ -71,7 +51,7 @@ def test_drops_cancelled_and_keeps_paid_statuses(): def test_purchase_total_uses_discounted_price_times_qty(): - result = match_pay_lines([_line()], [_cart()], existing_keys=set()) + result = match_pay_lines([_line()], existing_keys=set()) assert len(result.rows) == 1 row = result.rows[0] assert row["purchase_price"] == Decimal("9.8500") @@ -82,56 +62,39 @@ def test_purchase_total_uses_discounted_price_times_qty(): assert row["status"] is None assert row["pay_order_no"] == "ZQD1" assert row["pay_order_dtl_id"] == "881" - assert row["oms_id"] == 2026100201 - assert row["org_code"] == "901" + assert row["oms_id"] == 0 + assert row["org_code"] is None + assert row["multi_platform_flag"] == 0 -def test_match_prefers_equal_qty_then_earlier_fid(): - carts = [ - _cart(fid=10, oms_id=1, purchase_qty=50), - _cart(fid=20, oms_id=2, purchase_qty=200), - _cart(fid=5, oms_id=3, purchase_qty=200), - ] - result = match_pay_lines([_line()], carts, existing_keys=set()) - assert result.rows[0]["oms_id"] == 3 +def test_inserts_excel_line_without_cart_match(): + result = match_pay_lines([_line(bar_code="999")], existing_keys=set()) + assert len(result.rows) == 1 + assert result.rows[0]["bar_code"] == "999" + assert result.rows[0]["oms_id"] == 0 + assert result.unmatched == [] -def test_same_barcode_two_lines_consume_distinct_cart_rows(): - carts = [ - _cart(fid=1, oms_id=11, purchase_qty=10), - _cart(fid=2, oms_id=22, purchase_qty=50), - ] +def test_same_barcode_two_lines_both_insert(): lines = [ _line(order_no="A", qty=10, pay_price=Decimal("53.40"), pay_order_dtl_id="1"), _line(order_no="A", qty=50, pay_price=Decimal("55.21"), pay_order_dtl_id="2"), ] - result = match_pay_lines(lines, carts, existing_keys=set()) - oms = sorted(row["oms_id"] for row in result.rows) - assert oms == [11, 22] + result = match_pay_lines(lines, existing_keys=set()) + assert [row["pay_order_dtl_id"] for row in result.rows] == ["1", "2"] + assert all(row["oms_id"] == 0 for row in result.rows) def test_skips_existing_pay_order_and_dtl(): - result = match_pay_lines( - [_line()], - [_cart()], - existing_keys={("ZQD1", "881")}, - ) + result = match_pay_lines([_line()], existing_keys={("ZQD1", "881")}) assert result.rows == [] assert len(result.skipped_dup) == 1 -def test_unmatched_without_cart_or_dtl_is_not_inserted(): - no_cart = match_pay_lines([_line(bar_code="999")], [_cart()], existing_keys=set()) - assert no_cart.rows == [] - assert no_cart.unmatched[0][1] == "NO_CART_ROW" - - no_dtl = match_pay_lines( - [_line(pay_order_dtl_id="")], - [_cart()], - existing_keys=set(), - ) - assert no_dtl.rows == [] - assert no_dtl.unmatched[0][1] == "NO_DTL_ID" +def test_unmatched_without_dtl_is_not_inserted(): + result = match_pay_lines([_line(pay_order_dtl_id="")], existing_keys=set()) + assert result.rows == [] + assert result.unmatched[0][1] == "NO_DTL_ID" def test_attach_detail_ids_matches_qty_and_pay_price(): @@ -161,9 +124,11 @@ def test_attach_detail_ids_matches_qty_and_pay_price(): assert [line.pay_order_dtl_id for line in attached] == ["1001", "1002"] -def test_build_pay_row_copies_multi_platform_flag(): - row = build_pay_row(_line(), _cart(multi_platform_flag=1)) - assert row["multi_platform_flag"] == 1 +def test_build_pay_row_from_excel_fields(): + row = build_pay_row(_line()) + assert row["oms_id"] == 0 + assert row["org_code"] is None + assert row["multi_platform_flag"] == 0 assert row["supplier_name"] == "广东壹号药业有限公司" assert row["bar_code"] == "6921665004933" assert row["push_status"] == 0