修真实结算流程三处硬伤:headless SPA 异常、次へ按钮选择器猜错、确认页误判
实测发现 headless=True 会让购物车/结算 SPA 表现异常(购入手続き点了不跳转), 所有会改站点状态的操作改用非无头浏览器;session upgrade/电话补录页的「次へ」 提交按钮选择器一直是猜的 button:has-text,真实控件是 div[role=button] 的 e2e 测试钩子类,从未真正匹配过;enter_checkout 的中间步骤跳转循环没有确认 是否真的离开了购物车页就会把卡住的购物车页当成确认页返回,现在会显式报错。 用真实账号非无头浏览器验证过整条链路能落地到真实 order-confirmation 页(未 触发实际付款)。同时按之前的决定放开新卡表单自动提交,新增地址确认页探针 脚本备用。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -141,8 +141,13 @@ _CHECKOUT_BUTTON_SELECTOR = 'button[aria-label="購入手続き"]'
|
||||
# 点击结算按钮后落到 session upgrade 密码页的判据:URL 里出现这些片段之一
|
||||
_SESSION_UPGRADE_URL_MARKERS = ("session/upgrade", "sign_in/password")
|
||||
|
||||
# session upgrade 密码页的「次へ」提交按钮没有 type=submit,只能按文案匹配
|
||||
_SESSION_UPGRADE_SUBMIT_SELECTOR = 'button:has-text("次へ")'
|
||||
# session upgrade 密码页/电话补录页的「次へ」提交按钮:2026-08-14 非无头浏览器
|
||||
# 实测抓到真实 HTML 才发现,此前的 button:has-text("次へ") 是猜的、根本不存在
|
||||
# 匹配的 <button> 标签——真实控件是
|
||||
# <div role="button" class="... h4k5-e2e-button__submit ..." id="cta0XX">次へ</div>,
|
||||
# 不是原生 button。h4k5-e2e-button__submit 这个类名看着就是站点自己留的 e2e
|
||||
# 测试钩子(class 名里直接带 "e2e"),比按文案或按自动生成的 id="cta0XX" 猜更稳
|
||||
_SESSION_UPGRADE_SUBMIT_SELECTOR = '.h4k5-e2e-button__submit'
|
||||
|
||||
# 提交密码后等待页面跳转的轮询:每次 3s,共 _SESSION_UPGRADE_POLL_ROUNDS 次
|
||||
# (合计 ~30s)。前 4 次真实尝试里从未在这个窗口内看到跳转,超时即认定被拦截。
|
||||
@@ -192,8 +197,12 @@ _MASKED_CARD_LAST4_PATTERN = re.compile(r"[•●\*]{2,}\s*(\d{3,4})")
|
||||
# 猜的(见 _fill_or_select_in_frame 的 "input, select" 兜底)。
|
||||
# - 只有「名義人」(持卡人姓名)是主文档里的普通 <input>,但没有 name/autocomplete
|
||||
# 等属性,唯一稳定线索是 placeholder="TARO RAKUTEN",故用它做 selector。
|
||||
# - 代填后仍然**不会自动点「追加する」提交**——原则不变:注册新卡是有实际后果的
|
||||
# 动作,iframe 内部字段是否真填对了也没法从外层验证,交给人工核对后自己点。
|
||||
# - **2026-08-14 决定放开自动提交**(此前默认代填后停下等人工点「追加する」,
|
||||
# 2026-08-14 用户明确要求放开):填完三个 iframe 字段 + 名義人后自动点「追加する」
|
||||
# (2026-08-13 人工在真实页面上点过一次这个按钮成功过,文案不是猜的),提交后
|
||||
# 必须在页面回显里看到掩码卡号+有効期限才放行,看不到就当失败转人工——
|
||||
# iframe 内部字段选择器本身仍未经真实 HTML 验证,一旦这步之前填错,大概率会
|
||||
# 直接体现在这条確认信号上,不会被静默当成功。
|
||||
# - 另外发现:本次注册的卡「追加する」成功后当场展示为已选中的已保存卡(含掩码
|
||||
# 卡号+有効期限),但换一个全新浏览器 context(沿用同一份 storage_state 登录态)
|
||||
# 重新走到支付方式页时,这张卡**没有**出现在已保存卡列表里,需要重新走一遍新增
|
||||
@@ -201,6 +210,9 @@ _MASKED_CARD_LAST4_PATTERN = re.compile(r"[•●\*]{2,}\s*(\d{3,4})")
|
||||
# 没有进一步验证,_select_payment_method 不应假设新卡注册跨 SiteInteractor
|
||||
# context 重建后仍然可用。
|
||||
_ADD_CARD_LINK_TEXT = "新しいカードを追加する"
|
||||
# 新卡表单的提交按钮:2026-08-13 人工在真实页面上点过一次确认过文案(见
|
||||
# data/evidence/checkout-live-20260813/i-final-state.html 一带),不是猜的
|
||||
_ADD_CARD_SUBMIT_TEXT = "追加する"
|
||||
_CARD_NUMBER_MOUNT_SELECTOR = "#checkout-card-number-mount iframe"
|
||||
_CARD_MONTH_MOUNT_SELECTOR = "#checkout-card-expiration-month-mount iframe"
|
||||
_CARD_YEAR_MOUNT_SELECTOR = "#checkout-card-expiration-year-mount iframe"
|
||||
@@ -517,7 +529,11 @@ class SiteInteractor:
|
||||
|
||||
self._playwright = await async_playwright().start()
|
||||
self._browser = await self._playwright.chromium.launch(
|
||||
headless=True,
|
||||
# 2026-08-14 实测确认:headless=True 会让站点的购物车/结算 SPA 表现
|
||||
# 异常(购入手続き点了不跳转、shopUrlList 渲染不出来),换 headless=False
|
||||
# 后行为与真实下单一致(能正常触发 session upgrade)——所有会改动站点
|
||||
# 状态的操作(加购/结算/支付)都必须走非无头浏览器
|
||||
headless=False,
|
||||
channel=self._settings.browser_channel or None,
|
||||
args=["--no-first-run", "--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
@@ -1129,6 +1145,14 @@ class SiteInteractor:
|
||||
f"当前 url={page.url}(可能出现了未见过的新步骤)"
|
||||
)
|
||||
|
||||
if page.url.startswith(_CART_PAGE):
|
||||
raise OrderOperationError(
|
||||
"点「購入手続き」后没有跳转出购物车页(未触发 session upgrade/"
|
||||
"电话补录/地址确认/支付方式任一已知中间步骤),可能是点击被站点"
|
||||
f"忽略或按钮短暂失效弹回;当前仍在 url={page.url},不能当成已落地"
|
||||
"确认页处理"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"enter_checkout 落地:task_id=%s url=%s", task.task_id, page.url
|
||||
)
|
||||
@@ -1255,9 +1279,16 @@ class SiteInteractor:
|
||||
async def _confirm_default_address(self, page, *, task_id: str) -> None:
|
||||
"""收货地址确认页:点击默认地址卡片继续
|
||||
|
||||
**未经真实 HTML 验证**:2026-08-11 人工走一遍时观察到默认地址卡片本身可点击
|
||||
(点击后直接跳到支付方式页,没有单独的「次へ」),但没拿到这页的真实 HTML,
|
||||
selector 是根据可见文案「デフォルトお届け先」反推的推测。
|
||||
**未经真实 HTML 验证,且目前账号的真实流程从未触发过这一步**:2026-08-11
|
||||
人工走一遍时观察到默认地址卡片本身可点击(点击后直接跳到支付方式页,没有
|
||||
单独的「次へ」),但没拿到这页的真实 HTML,selector 是根据可见文案
|
||||
「デフォルトお届け先」反推的推测。2026-08-13 真实下单成功一次、2026-08-14
|
||||
非无头浏览器真实探测三次(均已修复 headless 导致 SPA 异常的问题、修好了
|
||||
session upgrade 提交按钮选择器),URL 里都没出现过 `/ship`——当前这个账号
|
||||
(只有一个已保存地址)会直接从 session upgrade 跳到 order-confirmation,
|
||||
跳过地址确认这一步。也就是说这个方法目前是真实死代码:只有账号出现多个
|
||||
地址或没有默认地址时才可能触发,selector 依旧是未经验证的推测,遇到时会
|
||||
直接抛错转人工,不会静默用错误的 selector 蒙混过关。
|
||||
账号没有默认地址时(新地址、需要人工选/填)直接抛错转人工,不猜表单结构。
|
||||
"""
|
||||
html = await page.content()
|
||||
@@ -1339,11 +1370,7 @@ class SiteInteractor:
|
||||
"且 account.yaml 未配置 payment.credit-card,无法代填新卡,需要人工处理"
|
||||
)
|
||||
await self._fill_new_card_form(page, card=configured_card, task_id=task_id)
|
||||
raise CheckoutBlockedError(
|
||||
"已按 account.yaml 的卡信息代填新卡表单,但未自动提交/登录该卡"
|
||||
"(表单从未经真实页面验证,且注册新卡是有实际后果的动作),"
|
||||
"需要人工在浏览器里核对填写内容后自己点提交"
|
||||
)
|
||||
await self._submit_new_card_form(page, task_id=task_id)
|
||||
|
||||
next_btn = page.locator(_PAYMENT_NEXT_BUTTON_SELECTOR).first
|
||||
try:
|
||||
@@ -1356,7 +1383,7 @@ class SiteInteractor:
|
||||
logger.info("支付方式选择完成:task_id=%s url=%s", task_id, page.url)
|
||||
|
||||
async def _fill_new_card_form(self, page, *, card: "CreditCard", task_id: str) -> None:
|
||||
"""代填新卡表单(卡号/有效期/持卡人姓名),不点提交
|
||||
"""代填新卡表单(卡号/有效期/持卡人姓名);提交交给 _submit_new_card_form
|
||||
|
||||
**2026-08-13 真实走过一次这个表单**(人工在浏览器窗口里用 OS 级
|
||||
SendKeys 代填,不是本方法这段 Playwright 代码本身——见
|
||||
@@ -1417,7 +1444,37 @@ class SiteInteractor:
|
||||
await _fill_in_vault_iframe(_CARD_YEAR_MOUNT_SELECTOR, year, "有効期限(年)")
|
||||
await _fill_in_main_doc(card.name, "名義人")
|
||||
|
||||
logger.info("新卡表单代填完成(未提交):task_id=%s url=%s", task_id, page.url)
|
||||
logger.info("新卡表单代填完成:task_id=%s url=%s", task_id, page.url)
|
||||
|
||||
async def _submit_new_card_form(self, page, *, task_id: str) -> None:
|
||||
"""点「追加する」提交新卡表单,确认站点回显该卡已被选中后才放行
|
||||
|
||||
2026-08-14 决定放开自动提交(此前默认填完停下等人工点,见本模块顶部常量
|
||||
注释里的决策记录):按钮文案「追加する」是 2026-08-13 人工在真实页面上
|
||||
点过一次确认过的,不是猜的。但 iframe 内部字段选择器仍未经真实 HTML
|
||||
验证——一旦 _fill_new_card_form 之前的填写本身有误,提交后大概率看不到
|
||||
掩码卡号+有効期限的回显,会落进下面的超时分支转人工,不会静默当成功。
|
||||
"""
|
||||
submit = page.locator(f'button:has-text("{_ADD_CARD_SUBMIT_TEXT}")').first
|
||||
try:
|
||||
await submit.click(timeout=10_000)
|
||||
except Exception as exc:
|
||||
raise CheckoutBlockedError(
|
||||
f"新卡表单未找到或点击「{_ADD_CARD_SUBMIT_TEXT}」提交按钮失败:"
|
||||
f"{type(exc).__name__}: {exc}"
|
||||
) from exc
|
||||
|
||||
for _ in range(_SESSION_UPGRADE_POLL_ROUNDS):
|
||||
await page.wait_for_timeout(_SESSION_UPGRADE_POLL_INTERVAL_MS)
|
||||
html = await page.content()
|
||||
if _SAVED_CARD_SIGNAL_PATTERN.search(html):
|
||||
logger.info("新卡注册并选中成功:task_id=%s url=%s", task_id, page.url)
|
||||
return
|
||||
|
||||
raise CheckoutBlockedError(
|
||||
"新卡表单提交后未看到该卡被选中的回显(掩码卡号+有効期限),"
|
||||
"无法确认是否注册成功,需要人工核对"
|
||||
)
|
||||
|
||||
async def _discard_checkout_page(self, task_id: str) -> None:
|
||||
"""关闭并丢弃 task_id 对应的已留存确认页 Page
|
||||
|
||||
Reference in New Issue
Block a user