实现付款后订单监控:真实探测 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:
@@ -76,10 +76,28 @@ class WorkerRunner:
|
||||
self._evidence = evidence
|
||||
self._site = site
|
||||
self._running = False
|
||||
# 付款后监控后台任务:task_id → asyncio.Task。与主循环解耦(不阻塞领下一单),
|
||||
# 详见 _spawn_monitor / _monitor_order。用 set 而非 list 是因为只需要成员管理,
|
||||
# 不需要顺序。
|
||||
self._monitor_tasks: set[asyncio.Task] = set()
|
||||
|
||||
def stop(self) -> None:
|
||||
self._running = False
|
||||
|
||||
async def cancel_monitors(self) -> None:
|
||||
"""服务关闭时调用:取消所有还在跑的付款后监控后台任务并等它们收尾
|
||||
|
||||
必须在 SiteInteractor.close() 之前调用——监控任务还在用同一个
|
||||
Playwright context,先关 context 再取消会导致监控任务在 await 到一半时
|
||||
踩上已关闭的资源,报一堆无意义的异常。
|
||||
"""
|
||||
tasks = list(self._monitor_tasks)
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
for t in tasks:
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await t
|
||||
|
||||
@property
|
||||
def worker_id(self) -> str:
|
||||
return self._settings.worker_id_effective
|
||||
@@ -311,11 +329,90 @@ class WorkerRunner:
|
||||
)
|
||||
await self._db.mark_finished(task.task_id, OrderState.PAID.value)
|
||||
|
||||
# 步骤 6:付款后监控(非阻塞,常驻轮询;当前未实现)
|
||||
try:
|
||||
await self._site.monitor(task, site_order_id)
|
||||
except NotImplementedError:
|
||||
logger.info("付款后监控未实现,跳过:task_id=%s", task.task_id)
|
||||
# 步骤 6:付款后监控——后台常驻轮询,不阻塞主循环领下一单(规格 §6)
|
||||
self._spawn_monitor(task, site_order_id)
|
||||
|
||||
def _spawn_monitor(self, task: LeaseTask, site_order_id: str) -> None:
|
||||
"""把 _monitor_order 起成独立的后台任务,登记到 _monitor_tasks 便于关服时收尾"""
|
||||
monitor_task = asyncio.create_task(
|
||||
self._monitor_order(task, site_order_id), name=f"monitor-{task.task_id}"
|
||||
)
|
||||
self._monitor_tasks.add(monitor_task)
|
||||
monitor_task.add_done_callback(self._monitor_tasks.discard)
|
||||
|
||||
async def _monitor_order(self, task: LeaseTask, site_order_id: str) -> None:
|
||||
"""付款后台轮询:订单配送阶段变化时继续 report(规格 §6「付款后监控」)
|
||||
|
||||
任务本身已经在 execute() 里上报过 succeeded 终态——这里的 report 都是
|
||||
`terminal=False` 的追加上报(规格 §4.5:任务已 terminal 的仍可上报,
|
||||
gateway 追加到 task_reports),监控本身的成败不影响已经完成的下单结果。
|
||||
|
||||
轮询间隔/次数上限见 settings.order_monitor_poll_interval_seconds /
|
||||
order_monitor_max_checks;达到上限仍未看到「配達完了」不算失败,只是
|
||||
停止追踪(记一条 warning)。单次探测异常(登录态失效、页面打不开)只记
|
||||
日志、下一轮重试,不能把异常抛出去——这是后台任务,没有人等着 catch 它,
|
||||
未捕获异常会被 asyncio 直接吞掉且只在垃圾回收时打一条难查的警告。
|
||||
"""
|
||||
last_state: OrderState | None = None
|
||||
step_no = 6
|
||||
interval = self._settings.order_monitor_poll_interval_seconds
|
||||
max_checks = self._settings.order_monitor_max_checks
|
||||
for attempt in range(1, max_checks + 1):
|
||||
await asyncio.sleep(interval)
|
||||
try:
|
||||
snapshot = await self._site.check_order_status(site_order_id)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"订单监控探测失败(第 %s/%s 次),下一轮继续重试:"
|
||||
"task_id=%s site_order_id=%s",
|
||||
attempt, max_checks, task.task_id, site_order_id,
|
||||
exc_info=True,
|
||||
)
|
||||
continue
|
||||
|
||||
if not snapshot.found:
|
||||
logger.info(
|
||||
"订单监控:第 %s/%s 次仍未在详情页找到订单号,继续轮询:task_id=%s",
|
||||
attempt, max_checks, task.task_id,
|
||||
)
|
||||
continue
|
||||
|
||||
if snapshot.order_state is None or snapshot.order_state == last_state:
|
||||
continue
|
||||
|
||||
last_state = snapshot.order_state
|
||||
step_name = f"monitor-{snapshot.order_state.value}"
|
||||
detail = f"订单监控:进度「{snapshot.stage_label}」→ {snapshot.order_state.value}"
|
||||
evidence_ref = self._evidence.write_step(
|
||||
task.task_id, step_no, step_name,
|
||||
html=snapshot.html,
|
||||
meta={
|
||||
"step": step_name,
|
||||
"state": snapshot.order_state.value,
|
||||
"stage_label": snapshot.stage_label,
|
||||
},
|
||||
)
|
||||
await self._db.index_evidence(task.task_id, step_no, step_name, evidence_ref)
|
||||
await self._db.record_event(
|
||||
task.task_id, snapshot.order_state.value,
|
||||
detail=detail, evidence_ref=evidence_ref,
|
||||
)
|
||||
await self._report_safe(
|
||||
task,
|
||||
state=snapshot.order_state,
|
||||
detail=detail,
|
||||
site_order_id=site_order_id,
|
||||
)
|
||||
step_no += 1
|
||||
|
||||
if snapshot.order_state == OrderState.DELIVERED:
|
||||
logger.info("订单监控:已送达,停止轮询:task_id=%s", task.task_id)
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"订单监控达到最大轮询次数 %s 仍未看到「配達完了」,停止追踪:task_id=%s",
|
||||
max_checks, task.task_id,
|
||||
)
|
||||
|
||||
def _enforce_amount_guard(self, task: LeaseTask, payable_yen: int) -> None:
|
||||
"""金额守卫:实际应付超过 intent.max_total_yen 或 RAKUTEN_ORDER_MAX_TOTAL_YEN 时拦截"""
|
||||
|
||||
Reference in New Issue
Block a user