实现付款后订单监控:真实探测 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:
@@ -48,6 +48,14 @@ data/evidence/checkout-research-20260811/NOTES.md):
|
||||
这次真实提交**只此一单**,其余账号/商品/金额组合下的中间步骤分支(电话补录、
|
||||
非默认地址、3DS/OTP 拦截等)仍未被真实数据验证过,继续按原有猜测处理并保留
|
||||
CheckoutBlockedError 兜底,不做无限重试。
|
||||
- check_order_status(付款后监控的单次探测,循环轮询在 runner.py)**2026-08-13
|
||||
用真实订单号 306087-20260813-0863947697 实测过** order.my.rakuten.co.jp 的
|
||||
订单列表页与详情页:两者共用同一套「配送阶段进度条」组件,固定 4 阶段
|
||||
(ショップ→出荷→配達店→配達完了),当前阶段的 class 里带 `-active--` 中缀,
|
||||
详见 _ORDER_STEPPER_ITEM_PATTERN 上方注释。这次真实订单当时还停在「ショップ」
|
||||
(刚接单,未发货)这一阶段,「出荷」「配達完了」两个状态转换点没有被真实数据
|
||||
验证过,只是按进度条文案直译映射(_ORDER_STAGE_TO_STATE);取消/退款没有
|
||||
找到可靠信号,检测不到,遇到需要人工核对订单列表页。
|
||||
|
||||
**httpx 不能用于带账号的写操作**:Rakuten 对账号操作有 TLS/HTTP2 指纹校验,
|
||||
同一份 cookie Playwright 能用、httpx 不能。所以本模块全程使用 Playwright
|
||||
@@ -80,6 +88,7 @@ from app.shared.purchase_contract import (
|
||||
basket_domain_of,
|
||||
inventory_flag_for,
|
||||
)
|
||||
from app.shared.task_state import OrderState
|
||||
from app.trading.core import auth_site
|
||||
from app.trading.worker.models import LeaseTask
|
||||
|
||||
@@ -259,10 +268,32 @@ _PAYMENT_BLOCK_INDICATORS = (
|
||||
'input[autocomplete="one-time-code"]',
|
||||
)
|
||||
|
||||
_NOT_IMPLEMENTED_MSG = (
|
||||
"站点交互未实现:见 docs/order-gateway.md §10 与 "
|
||||
"project://jp-rakuten/checkout-flow-probe-findings。"
|
||||
# ---- 付款后监控:订单列表/详情页轮询(2026-08-13 用真实订单号
|
||||
# 306087-20260813-0863947697 实测过,见模块顶部说明)----
|
||||
# 详情页 URL 可以直接从 site_order_id 构造,不需要额外查询:shop_id 就是订单号
|
||||
# 第一段(本单验证 306087-20260813-... 对应 shop_id=306087)。
|
||||
_ORDER_DETAIL_URL_TEMPLATE = (
|
||||
"https://order.my.rakuten.co.jp/purchase-history/"
|
||||
"?order_number={order_number}&shop_id={shop_id}&act=detail_page_view"
|
||||
)
|
||||
# 订单列表页与详情页共用同一套「配送阶段进度条」组件:4 个固定阶段的 <li>,
|
||||
# 当前阶段比其余几个多一段形如 `item-shipping-active--{hash}` 的 class
|
||||
# (hash 是 CSS modules 编译产物,逐次构建会变;`-active--` 这个中缀是本模块
|
||||
# 唯一依赖的稳定信号)。用 re.findall 顺序返回 4 个 (class, 阶段文案) 二元组。
|
||||
_ORDER_STEPPER_ITEM_PATTERN = re.compile(
|
||||
r'<li class="([^"]*)">.*?<div class="title--2uGVi">([^<]*)</div></li>', re.DOTALL,
|
||||
)
|
||||
_ORDER_STEPPER_ACTIVE_MARKER = "-active--"
|
||||
# 「ショップ」(店铺已接单,未发货)阶段没有对应的 OrderState 取值——报告过
|
||||
# ORDERED/PAID 了,不需要 monitor 再报一次;「配達店」(配送中转)也没有单独
|
||||
# 状态,归入 SHIPPED。只有「出荷」→SHIPPED、「配達完了」→DELIVERED 两个转换点
|
||||
# 会触发 runner._monitor_order 的 report。真实订单当时仍停在「ショップ」阶段,
|
||||
# 这两个映射本身未经真实状态转换验证,只是按进度条文案直译。
|
||||
_ORDER_STAGE_TO_STATE: dict[str, OrderState] = {
|
||||
"出荷": OrderState.SHIPPED,
|
||||
"配達店": OrderState.SHIPPED,
|
||||
"配達完了": OrderState.DELIVERED,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -277,6 +308,25 @@ class CheckoutSummary:
|
||||
pay_deadline: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class OrderStatusSnapshot:
|
||||
"""订单列表/详情页单次探测结果(check_order_status 的返回值)
|
||||
|
||||
found=False 表示页面上没找到这个订单号——**不当错误处理**:站点自己说
|
||||
「ご注文の反映に10分ほどかかります」,下单后短时间内查不到是正常的,调用方
|
||||
(runner._monitor_order)应该继续下一轮轮询,不是放弃。
|
||||
stage_label 是真实站点用的阶段原文(如「出荷」),供日志/证据留原始信号;
|
||||
order_state 是映射到 OrderState 的结果,映射不到(比如仍在「ショップ」这个
|
||||
起始阶段,或者进度条没解析出来)时为 None——同样不当错误,只是「这次没有
|
||||
新状态可报」。html 是抓取到的整页内容,供调用方按需落证据。
|
||||
"""
|
||||
|
||||
found: bool
|
||||
stage_label: str | None = None
|
||||
order_state: OrderState | None = None
|
||||
html: str = ""
|
||||
|
||||
|
||||
class SiteInteractor:
|
||||
"""Rakuten 站点交互器:持有 Playwright 浏览器 context,复用账号 cookie
|
||||
|
||||
@@ -1381,9 +1431,48 @@ class SiteInteractor:
|
||||
finally:
|
||||
await page.close()
|
||||
|
||||
async def monitor(self, task: LeaseTask, site_order_id: str) -> None:
|
||||
"""付款后监控。**未实现**:订单列表页的真实结构没有实测过"""
|
||||
raise NotImplementedError(_NOT_IMPLEMENTED_MSG)
|
||||
async def check_order_status(self, site_order_id: str) -> OrderStatusSnapshot:
|
||||
"""付款后监控的单次探测:查一次订单详情页的配送阶段,不循环
|
||||
|
||||
循环轮询(间隔、次数上限、状态变化时 report)由
|
||||
`runner.WorkerRunner._monitor_order` 负责——本方法只做一次
|
||||
「导航 + 解析」,站点交互与轮询节奏解耦,也方便离线单测轮询逻辑
|
||||
(用桩替换本方法)与解析逻辑(`_parse_order_status`,纯函数)。
|
||||
|
||||
与 enter_checkout 不同,本方法开自己的临时 Page 并在返回前关闭——
|
||||
submit_order/pay 已经处理完并关闭了 `_checkout_pages` 里留存的会话,
|
||||
监控阶段没有需要跨调用复用的页面状态。
|
||||
|
||||
Returns:
|
||||
OrderStatusSnapshot;订单号暂时查不到、或进度条解析不出新阶段都
|
||||
**不算错误**(详见该 dataclass 文档),由调用方决定是否继续轮询。
|
||||
|
||||
Raises:
|
||||
NotLoggedInError: 登录态失效
|
||||
OrderOperationError: 订单详情页打开/渲染失败
|
||||
"""
|
||||
shop_id = site_order_id.split("-", 1)[0]
|
||||
url = _ORDER_DETAIL_URL_TEMPLATE.format(order_number=site_order_id, shop_id=shop_id)
|
||||
|
||||
async with self._lock:
|
||||
await self._auth_session.require_logged_in("rakuten")
|
||||
await self._refresh_context_if_stale()
|
||||
|
||||
page = await self._context.new_page()
|
||||
try:
|
||||
try:
|
||||
await page.goto(url, wait_until="domcontentloaded", timeout=30_000)
|
||||
await page.wait_for_timeout(2_000)
|
||||
except Exception as exc:
|
||||
raise OrderOperationError(
|
||||
f"订单详情页打开失败:site_order_id={site_order_id} "
|
||||
f"{type(exc).__name__}: {exc}"
|
||||
) from exc
|
||||
html = await page.content()
|
||||
finally:
|
||||
await page.close()
|
||||
|
||||
return _parse_order_status(html, site_order_id)
|
||||
|
||||
|
||||
# ---- 模块级辅助函数(纯函数,便于单测)----
|
||||
@@ -1531,3 +1620,25 @@ def _parse_checkout_summary(html: str) -> CheckoutSummary:
|
||||
site_order_id=site_order_id,
|
||||
pay_deadline=pay_deadline,
|
||||
)
|
||||
|
||||
|
||||
def _parse_order_status(html: str, site_order_id: str) -> OrderStatusSnapshot:
|
||||
"""订单列表/详情页解析核心逻辑(纯函数,供 check_order_status 调用,便于离线单测)
|
||||
|
||||
2026-08-13 用真实订单号 306087-20260813-0863947697 的订单详情页 HTML 验证过:
|
||||
见 _ORDER_STEPPER_ITEM_PATTERN 上方注释。找不到订单号、或进度条没解析出
|
||||
「当前阶段」都返回 found/order_state 相应地为 False/None,不抛错——这是
|
||||
「暂时没有新信息」而不是「站点交互失败」,抛错的语义留给导航/渲染失败。
|
||||
"""
|
||||
if site_order_id not in html:
|
||||
return OrderStatusSnapshot(found=False, html=html)
|
||||
|
||||
for cls, label in _ORDER_STEPPER_ITEM_PATTERN.findall(html):
|
||||
if _ORDER_STEPPER_ACTIVE_MARKER in cls:
|
||||
return OrderStatusSnapshot(
|
||||
found=True,
|
||||
stage_label=label,
|
||||
order_state=_ORDER_STAGE_TO_STATE.get(label),
|
||||
html=html,
|
||||
)
|
||||
return OrderStatusSnapshot(found=True, html=html)
|
||||
|
||||
Reference in New Issue
Block a user