实现付款后订单监控:真实探测 order.my.rakuten.co.jp 配送阶段

用真实订单号 306087-20260813-0863947697 探测 order.my.rakuten.co.jp(订单列表/
详情页),拿到真实 DOM 结构后实现 SiteInteractor.check_order_status:

- 详情页 URL 可直接从 site_order_id 构造(shop_id 是订单号第一段)
- 配送阶段用「进度条」组件的 4 个固定阶段(ショップ/出荷/配達店/配達完了),
  当前阶段的 class 带 -active-- 中缀,映射到 OrderState.SHIPPED/DELIVERED
- 查不到订单号、进度条解析不出新阶段都不算错误,交给轮询循环继续重试

runner.WorkerRunner 新增 _monitor_order 后台轮询:付款成功上报后以
asyncio.create_task 起后台任务(不阻塞主循环领下一单,因为 SiteInteractor 的
Playwright 操作全程持锁串行化),状态变化时用 terminal=False 追加 report;
新增 cancel_monitors() 在服务关闭时于 SiteInteractor.close() 之前收尾。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 23:01:35 +08:00
co-authored by Claude Sonnet 5
parent 5f7921b788
commit 51e2538438
8 changed files with 450 additions and 18 deletions
+74 -4
View File
@@ -17,13 +17,16 @@ import pytest
from app.shared.errors import CartOperationError, InvalidRequestError, OrderOperationError
from app.trading.worker.models import LeaseTask
from app.shared.task_state import OrderState
from app.trading.worker.site_interact import (
CheckoutSummary,
OrderStatusSnapshot,
SiteInteractor,
_extract_error_message,
_extract_purchase_fields,
_parse_checkout_summary,
_parse_initial_state,
_parse_order_status,
)
FIXTURES = Path(__file__).parent / "fixtures"
@@ -250,13 +253,80 @@ def test_site_interactor_construction_does_not_require_playwright():
assert site._per_task_state == {}
# ---- monitor 仍未实现,抛 NotImplementedError ----
# ---- _parse_order_status:付款后监控的解析核心,2026-08-13 用真实订单号
# 306087-20260813-0863947697 的详情页 HTML 验证过(见 _ORDER_STEPPER_ITEM_PATTERN
# 上方注释)。下面 fixture 里的 4 个 <li> 是从真实页面摘录的进度条结构原样保留,
# 只是把每个 class 里易变的 CSS modules hash 换成了固定占位,不影响所依赖的
# `-active--` 中缀。
_REAL_ORDER_ID = "306087-20260813-0863947697"
async def test_monitor_unimplemented():
def _stepper_html(*, active_stage: str | None, order_id: str = _REAL_ORDER_ID) -> str:
"""构造「订单号 + 4 阶段进度条」的最小 fixture,active_stage 指定哪一阶带 -active-- class"""
stages = ["ショップ", "出荷", "配達店", "配達完了"]
items = []
for i, stage in enumerate(stages):
active_cls = " item-shipping-active--1mu0i" if stage == active_stage else ""
items.append(
f'<li class="item--3gWCU item-{i}--hash title-m--3FZT3{active_cls}">'
f'<div class="title--2uGVi">{stage}</div></li>'
)
return f"<div>注文番号:{order_id}</div><ul>{''.join(items)}</ul>"
def test_parse_order_status_not_found_when_order_id_missing():
snapshot = _parse_order_status(_stepper_html(active_stage="ショップ", order_id="999999-x"), _REAL_ORDER_ID)
assert snapshot.found is False
assert snapshot.order_state is None
def test_parse_order_status_shop_stage_has_no_order_state_mapping():
"""「ショップ」(已接单未发货)阶段没有对应的 OrderState——已经在 ORDERED/PAID 报过了"""
snapshot = _parse_order_status(_stepper_html(active_stage="ショップ"), _REAL_ORDER_ID)
assert snapshot.found is True
assert snapshot.stage_label == "ショップ"
assert snapshot.order_state is None
def test_parse_order_status_shipped_stage_maps_to_shipped():
snapshot = _parse_order_status(_stepper_html(active_stage="出荷"), _REAL_ORDER_ID)
assert snapshot.order_state == OrderState.SHIPPED
def test_parse_order_status_depot_stage_also_maps_to_shipped():
"""「配達店」(配送网点中转)没有单独状态,归入 SHIPPED"""
snapshot = _parse_order_status(_stepper_html(active_stage="配達店"), _REAL_ORDER_ID)
assert snapshot.order_state == OrderState.SHIPPED
def test_parse_order_status_delivered_stage_maps_to_delivered():
snapshot = _parse_order_status(_stepper_html(active_stage="配達完了"), _REAL_ORDER_ID)
assert snapshot.order_state == OrderState.DELIVERED
def test_parse_order_status_no_active_marker_returns_found_without_state():
"""进度条 4 项都没有 -active-- class(页面结构变了/解析不出):found 但 order_state=None"""
snapshot = _parse_order_status(_stepper_html(active_stage=None), _REAL_ORDER_ID)
assert snapshot.found is True
assert snapshot.order_state is None
assert snapshot.stage_label is None
def test_order_status_snapshot_defaults():
s = OrderStatusSnapshot(found=False)
assert s.stage_label is None
assert s.order_state is None
assert s.html == ""
# ---- check_order_status 在没启动 Playwright 时应失败 ----
async def test_check_order_status_without_start_raises():
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
with pytest.raises(NotImplementedError):
await site.monitor(_make_task(), "ord-1")
with pytest.raises((AttributeError, TypeError)):
await site.check_order_status(_REAL_ORDER_ID)
# ---- submit_order / pay:没有 enter_checkout 留存的确认页会话时应报错,不静默成功 ----