feat(trading): 详情页派送到真实样本,补结构化配送状态并修 stepper 回归

用真账号实测爬取订单详情页(scripts/probe_order_detail.py,样本落盘
.probe/order_detail/),首次拿到 pageType="ph-detail" 的真实 __INITIAL_STATE__,
此前「详情页结构从未有样本、只原样透传」的缺口由此闭合:

- _parse_order_detail_status:新增,优先从 orderData.shippingList[].deliveryInfo
  .deliveryStatus 结构化枚举判配送阶段;映射遵循「只映射实测值」,目前仅
  CHECKING_ORDER,未识别枚举交回 stepper 兜底(防掐掉 SHIPPED/DELIVERED 上报)。
- _ORDER_STEPPER_ITEM_PATTERN:修回归——不锚定 <li class="item--3gWCU"> 时面包屑
  li 会抢走进度条第0项、导致 -active-- 永远判不出当前阶段(真实样本复现)。
- OrderStatusSnapshot 增 delivery_status 字段;query_runner order_detail 透传。
- docs/order-gateway.md §11 更新实测边界;新增详情页样本回溯测试。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
2026-08-16 23:04:59 +08:00
parent c03158488b
commit 3b53b8f28b
5 changed files with 479 additions and 26 deletions
+113
View File
@@ -39,6 +39,7 @@ from app.trading.worker.site_interact import (
_parse_checkout_summary,
_parse_initial_state,
_parse_order_list,
_parse_order_detail_status,
_parse_order_status,
parse_order_datetime,
)
@@ -334,6 +335,118 @@ def test_order_status_snapshot_defaults():
assert s.html == ""
# ---- _parse_order_status:2026-08-16 真实详情页样本暴露出面包屑干扰的回归 ----
# 真实详情页除了配送进度条,还有一套面包屑 <li>(class 同样含 CSS modules hash、
# label 也是「ショップ」且紧邻 title--2uGVi)。不锚定 li class 前缀时,findall
# 会先抓走面包屑、再把带 -active-- 的进度条第 0 项错位跳过,导致永远判不出当前
# 阶段。修复后锚定 item--3gWCU 前缀,下面用脚本 probe_order_detail.py 落盘的真实
# 片段回归防倒退回退化。
# 摘自 .probe/order_detail/00-detail-default.html 的可复现片段:
# 面包屑 li(应被排除)在前,真正的进度条第 0 项(active)夹在中间。
_REAL_STEPPER_HTML = (
'<html><body>'
'<div class="breadcrumbs-container-overflow-scroll--8DmdJ">'
'<li class="breadcrumb-item--2aqJe"><div class="title--2uGVi">ショップ</div></li>'
'</div>'
'<ul>'
'<li class="item--3gWCU item-0--3JtWc title-m--3FZT3 item-shipping-active--1mu0i">'
'<div class="default-icon-container">..</div><div class="title--2uGVi">ショップ</div></li>'
'<li class="item--3gWCU item-1--2Ehq7 title-m--3FZT3">'
'<div class="title--2uGVi">出荷</div></li>'
'<li class="item--3gWCU item-2--2hMQE title-m--3FZT3">'
'<div class="title--2uGVi">配達店</div></li>'
'<li class="item--3gWCU item-3--31OcV title-m--3FZT3">'
'<div class="title--2uGVi">配達完了</div></li>'
'</ul>'
f'<div>注文番号:{_REAL_ORDER_ID}</div>'
'</body></html>'
)
def test_parse_order_status_breadcrumb_sibling_does_not_shadow_active_item0():
"""真实详情页有面包屑 li,不得让它替代进度条 item-0 挡在 -active-- 判定前"""
snapshot = _parse_order_status(_REAL_STEPPER_HTML, _REAL_ORDER_ID)
assert snapshot.found is True
assert snapshot.stage_label == "ショップ"
assert snapshot.order_state is None
# ---- _parse_order_detail_status:2026-08-16 首次拿到详情页真实样本才有的路径 ----
# orderData.shippingList[].deliveryInfo.deliveryStatus 是站点结构化配送状态,比
# CSS 进度条稳。但映射遵循「只映射实测值」,当前只有 CHECKING_ORDER 一个样本。
def _detail_state(*, delivery_status: str | None, title: str | None, order_ok: bool = True) -> dict:
"""构造详情页 __INITIAL_STATE__ 最小 fixture(真实样本 .probe/order_detail)"""
return {
"pageType": "ph-detail",
"requestParams": {"orderNumber": _REAL_ORDER_ID if order_ok else "other-order"},
"orderData": {
"orderSummary": {"orderNumber": _REAL_ORDER_ID if order_ok else "other-order"},
"shippingList": [
{
"deliveryInfo": (
{"deliveryStatus": delivery_status, "deliveryStatusTitle": title}
if delivery_status
else {}
)
}
],
},
}
def test_parse_order_detail_status_uses_delivery_status_when_present():
"""结构化的 deliveryStatus 优先于 stepper,stage_label 取站点原文"""
snapshot = _parse_order_detail_status(
_detail_state(delivery_status="CHECKING_ORDER", title="ご注文確認中"), _REAL_ORDER_ID
)
assert snapshot is not None
assert snapshot.found is True
assert snapshot.delivery_status == "CHECKING_ORDER"
assert snapshot.stage_label == "ご注文確認中"
# 尚无实测映射的枚举值不猜 order_state(CHECKING_ORDER 对应「ショップ」阶段,
# 已报告过 ORDERED/PAID,不需要 monitor 再报,与 stepper 行为一致)
assert snapshot.order_state is None
def test_parse_order_detail_status_unknown_code_falls_back_to_none():
"""没实测过的 deliveryStatus 不接管——交回 stepper 兜底(避免掐掉 SHIPPED/DELIVERED 上报)
实测只认识 CHECKING_ORDER;一旦真实订单走到「出荷/配達完了」,其 deliveryStatus
字符串还是未知,此时若结构化路径返回 order_state=None 会把 monitor 的状态上报
掐掉。所以未识别的枚举值直接返回 None,由调用方降级到 stepper 的
_ORDER_STAGE_TO_STATE(后者早就能映射出荷/配達完了)。
"""
assert (
_parse_order_detail_status(
_detail_state(delivery_status="SOME_FUTURE_STATUS", title="出荷待ち"),
_REAL_ORDER_ID,
)
is None
)
def test_parse_order_detail_status_returns_none_wrong_order():
"""state 的 orderNumber 对不上所查订单时不得当 found(防 SPA 复用到别的订单)"""
assert (
_parse_order_detail_status(
_detail_state(delivery_status="CHECKING_ORDER", title="ご注文確認中", order_ok=False),
_REAL_ORDER_ID,
)
is None
)
def test_parse_order_detail_status_returns_none_when_no_order_data():
assert _parse_order_detail_status(None, _REAL_ORDER_ID) is None
assert (
_parse_order_detail_status(_detail_state(delivery_status=None, title=None), _REAL_ORDER_ID)
is None
)
# ---- check_order_status 在没启动 Playwright 时应失败 ----