docs: import wiki at afc651f75a3a

ila
2026-08-07 16:37:06 +08:00
parent f29d5ae7ad
commit bc0202ab83
+386
@@ -0,0 +1,386 @@
<!-- docs-wiki-sync:docs/tasks/T-264.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
> 同步来源:[`docs/tasks/T-264.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-264.md) · commit `afc651f75a3a`
---
id: T-264
title: 停止以购买按钮作为规格入口并识别原生订单确认页
phase: 2
deps:
- T-263
status: DOING
created: 2026-07-31
context_ref: 3b04fb0
work_branch: null
write_paths:
- docs/tasks/T-264.md
- docs/current-state.md
- android-buyer/app/src/main/**
- android-buyer/app/src/test/**
---
## 问题 / 背景
T-263 上机后,第 2 轮**首次成功进入候选采集**(`cards=3 distinct=2 attempts=2`,
`payTextSuppressed=1` 证明文本抑制生效)。但随后自动化**走进了真实的订单确认页并卡死
在那里**,第 3 轮开 App 即被拦。
设备取证显示当时页面内容为:收件人姓名、掩码手机号、详细收货地址、
`已选:#心相印 新品【4包 400抽】…`、数量 `1`、`已包邮`、`仅1次¥0.01`、`¥8.01`,以及
`使用#微信支付,更换先用后付可0元下单`。**这是一个原生订单确认页**(147 节点、
26 条文本、**0 个 WebView**),商品是纸巾——图片搜索结果中混入的非服装品类。
### 因果链
**第一处缺口:找不到规格入口时会去点购买按钮**
`BuyerAccessibilityService.openPinduoduoSpecifications()`:
```kotlin
val target = directTargets.singleOrNull() // 选择规格/颜色分类/已选:…
?: uniqueClickableTargets(... endsWith("免拼购买")).singleOrNull() // 兜底点购买
?: return@withPinduoduoRoot false
```
纸巾没有颜色/尺码规格,详情页缺少安全入口文本(或存在多个导致 `singleOrNull()` 为
null),于是落入兜底分支点击「免拼购买」。正常商品点击后会先弹出规格面板,但该商品
为 `仅1次¥0.01` 促销且启用先用后付,**点击后直接跳转到订单确认页,跳过了规格面板**。
**第二处缺口:该订单确认页识别不出来**
`PinduoduoPageClassifier` 的 `hasOrderConfirmation` 三个判据全部落空:
| 判据 | 实际 |
| --- | --- |
| `hasCheckoutRoot`(`order_checkout`) | 未命中 |
| 存在文本恰为「确认订单」 | 页面无此文本 |
| 含「收货地址」+ 两个支付方式标记 + 「实付款¥」前缀 | 只有地址内容而无「收货地址」标签;价格为「¥8.01」而非「实付款¥…」 |
因此 `page != ORDER_CONFIRMATION`。而 `awaitSpecificationEntry()` 的判断顺序是:
```kotlin
if (page == SPECIFICATION_PANEL) return PANEL
if (page == ORDER_CONFIRMATION) return ORDER_CONFIRMATION // 走不到
safetyResult(snapshot)?.let { return FAILED(it) } // 落到这里
```
页面认不出来,**既有的 `returnFromOrderConfirmation()` 退出路径根本没有机会执行**——
这正是「卡在那里出不来」的直接原因,也解释了 `orderConfirmRedirect=0`。
最终由「微信支付」促销横幅触发 `PAYMENT_BOUNDARY` 拦下。**当前防线只剩这一条促销
文案**,而该文案随时可能变更。
### 风险评估
自动化在非预期路径下到达了订单确认页,页面上是 `仅1次¥0.01` 促销价并启用先用后付。
虽然从未点击提交、安全边界未被突破,但**距离产生一笔真实订单只差一次点击**,且
拦截依赖的是一条营销文案而非稳定特征。
## 关联需求与交互
- 功能:F-005、F-007
- 用户故事:US-004、US-006
- 交互:不新增采购操作
- 架构/API:沿用既有诊断机制,**后端零改动**
## 行为契约
### 一、不得以购买类控件作为规格入口
移除 `openPinduoduoSpecifications()` 中以「免拼购买」为兜底点击目标的分支。
找不到安全规格入口(`SAFE_ENTRY_TEXTS` / `SAFE_ENTRY_PREFIXES`)时**直接返回 false**,
由既有 `specOpenFailed` 路径跳过该候选,**不得点击任何购买、下单、拼单、支付语义的
控件**。
新增诊断计数 `specEntryAbsent`,记录因缺少安全入口而放弃的候选数,用于评估本次收紧
造成的覆盖损失。若真机上该值偏高,说明有相当比例的正常商品依赖该兜底,需另开任务以
更安全的方式补回,**但不得退回点击购买按钮**。
### 二、识别原生订单确认页
`hasOrderConfirmation` **追加**一个判据(现有三个判据一律保留):
同时满足即判定为订单确认页:
1. 存在掩码手机号形态的文本(如 `\d{3}\*{4}\d{4}`);
2. 存在以 `已选` 开头的文本;
3. 存在数量或运费语义(如纯数字数量、`已包邮`、`运费`)。
依据:掩码手机号仅出现在涉及收件信息的页面,商品详情页不会出现;三者同时出现足以与
商品详情页、规格弹层区分。
**隐私硬约束**:仅做**形态匹配**用于判定,**绝不记录**收件人姓名、手机号(含掩码
形式)、详细地址或任何原文,不得进入诊断 message、事件、日志或截图元数据。依据业务
规则 29。
识别成功后,既有 `awaitSpecificationEntry()` 的 `ORDER_CONFIRMATION` 分支即可生效,
调用既有 `returnFromOrderConfirmation()` 安全退出,解决「卡住出不来」。
### 三、记录支付边界触发时的页面类型
T-262 的归因只记了命中哪个标记,未记当时页面类型,导致本次必须手动 dump 才能定位。
`PaymentBoundaryAttribution` 追加页面类型,诊断 message 追加:
```
payPage=<PinduoduoPage 枚举名>
```
取值为本项目自定义枚举的名称(ASCII,如 `UNKNOWN`、`PRODUCT_DETAIL`、
`ORDER_CONFIRMATION`),**不是页面内容**,因此可以记录。未触发支付边界时记 `NONE`。
### 四、安全边界一律不放松
- 不得削弱 `PAYMENT_BOUNDARY` 的任何判据:T-263 结构判据、`hasCheckoutRoot`、
`paymentMarkers` 文本判据及其抑制规则全部保持现状。
- 不得点击提交订单、立即支付、免密支付、先用后付或任何扣款语义控件。
- 退出订单确认页只允许使用既有 `returnFromOrderConfirmation()`,且沿用其现有的有界
次数与失败即 `Blocked(PAYMENT_BOUNDARY)` 行为。
- 登录、验证码、风控三类安全停止的判定与优先级完全不变。
### 五、既有诊断不变
T-254/T-255/T-257/T-260/T-261/T-262/T-263 既有诊断键的顺序、名称与取值**逐字节不变**,
新键一律追加在末尾。
## 方案
1. `openPinduoduoSpecifications()` 删除「免拼购买」兜底分支,缺少安全入口时返回 false。
2. 候选自动化统计 `specEntryAbsent`,并入诊断快照与 message 尾部。
3. `PinduoduoPageClassifier` 为 `hasOrderConfirmation` 追加原生结算页判据;掩码手机号
等只做形态匹配,不留存原文。
4. `PaymentBoundaryAttribution` 追加页面类型字段,诊断追加 `payPage` 键。
5. 单测覆盖:无安全入口时不点击任何购买控件且计数递增;原生结算页样本被识别为
`ORDER_CONFIRMATION`;商品详情页与规格弹层**不得**被新判据误判;掩码号码不出现在
任何诊断输出中;`payPage` 取值正确且未触发时为 `NONE`;既有支付边界判据未削弱。
## 验收要点
- [ ] 详情页无安全规格入口时返回 false,且**不点击任何购买/下单/支付语义控件**,有
单测证明。
- [ ] `specEntryAbsent` 计数正确并出现在诊断 message。
- [ ] 用真机取证的原生结算页特征构造夹具,被识别为 `ORDER_CONFIRMATION`。
- [ ] 商品详情页、规格弹层、搜索结果页**不被**新判据误判为订单确认页,有回归测试。
- [ ] 掩码手机号、收件人、地址不出现在诊断 message、事件或日志中,有单测证明。
- [ ] `payPage` 出现在诊断 message,未触发支付边界时为 `NONE`。
- [ ] T-263 结构判据、`hasCheckoutRoot`、`paymentMarkers` 及抑制规则未被削弱,有回归
测试。
- [ ] 登录、验证码、风控判定与优先级未变。
- [ ] 既有诊断键顺序、名称、取值不变。
- [ ] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。
- [ ] 真机验证:同一任务重跑时,遇到无颜色/尺码规格的商品**不再进入订单确认页**;
若仍进入,应能被识别并自动退出而非卡死;从 Admin 读取 `specEntryAbsent` 与
`payPage` 记入执行记录。
## 边界
- **不放松任何支付边界判据**,只增加订单确认页的识别能力。
- 不点击提交订单、立即支付、先用后付或任何扣款语义控件。
- 不改 `returnFromOrderConfirmation()` 的既有实现与有界次数。
- 不改登录、验证码、风控的检测逻辑与优先级。
- 不改 T-261 清理逻辑、T-262 归因既有五键、T-263 结构判据与抑制名单。
- 不改 T-256 提交门槛、T-258 后端校验、T-259 安全停止补报、T-260 节奏配置。
- 不改候选卡片识别、签名算法、跨轮去重、价格解析或三轮预算。
- 不改后端代码、数据库、API 契约或 Admin 模板。
- 不处理「图片搜索结果混入非目标品类」本身——那需要在候选筛选阶段引入品类判断,
另行开任务。
- 不处理第 1 轮 `containers=550` 却 0 卡片的识别问题,另行开任务。
## 执行记录
开工后记录修改、命令、结果、环境、决策和 blocker。
### 2026-07-31 实现与自动化验证
环境:WSL 无 Android SDK/JDK,全部构建与单测经
`/mnt/c/Windows/System32/cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat <目标> --no-daemon --console=plain"`
在 Windows 侧执行;未运行 `init.ps1`。
#### 改动文件
- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoCandidateModels.kt`:
新增 `PinduoduoSpecificationOpenOutcome` 枚举(`OPENED`/`ENTRY_ABSENT`/
`UNAVAILABLE`),`PinduoduoCandidateDriver.openSpecifications()` 的返回类型
由 `Boolean` 改为该枚举,用于向候选自动化区分「找不到唯一安全入口」与其它
不可用情形。仅此接口改动;`PinduoduoOrderDryRunDriver`(下单 dry-run 专用
的独立接口,定义于 `PinduoduoOrderDryRunAutomation.kt`)保持
`openSpecifications(): Boolean` 不变,未受影响。
- `android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityService.kt`:
`openPinduoduoSpecifications()` 删除「找不到 `directTargets.singleOrNull()`
就点击以『免拼购买』结尾的兜底控件」分支(连同私有常量
`SPECIFICATION_PURCHASE_ENTRY`),改为纯投影——新增
`openPinduoduoSpecificationsDetailed(): PinduoduoSpecificationOpenOutcome`
作为唯一的点击实现:未验证到商品详情页时返回 `UNAVAILABLE`;
`directTargets.singleOrNull()` 为空(找不到唯一安全入口,含零个或多个两种
情形)时返回 `ENTRY_ABSENT`,**不再有任何兜底点击**;点击成功返回
`OPENED`,点击本身失败返回 `UNAVAILABLE`。`openPinduoduoSpecifications():
Boolean` 改为 `openPinduoduoSpecificationsDetailed() ==
PinduoduoSpecificationOpenOutcome.OPENED` 的布尔投影,供既有下单 dry-run
驱动(经 `BuyerAccessibilityBridge.openSpecifications()`)复用同一份点击
逻辑,不重复点击、不改变其行为。
- `android-buyer/app/src/main/java/com/roubao/autopilot/accessibility/BuyerAccessibilityBridge.kt`:
新增 `openSpecificationsDetailed(): PinduoduoSpecificationOpenOutcome`,
专供候选驱动使用;既有 `openSpecifications(): Boolean`(下单 dry-run 驱动
使用)逐字节未动。
- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/AndroidPinduoduoCandidateDriver.kt`:
`openSpecifications()` 改为调用 `BuyerAccessibilityBridge.openSpecificationsDetailed()`
并保留既有的动作节奏停顿(`actionPacer.pause()`),返回值改为枚举。
- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoCandidateAutomation.kt`:
新增会话内累计字段 `specEntryAbsentCount`(`reset()` 一并清零);打开规格
处的 `if (!driver.openSpecifications())` 改为对三态枚举的 `when`——
`OPENED` 正常继续;`ENTRY_ABSENT` 与 `UNAVAILABLE` 都仍使
`specificationOpenFailedCount`(既有 `specOpenFailed` 口径)递增并触发既有
`returnToResultsAfterUnsupportedCandidate()` 安全返回,`ENTRY_ABSENT` 额外
使 `specEntryAbsentCount` 递增;`diagnostics()` 追加
`specEntryAbsent = specEntryAbsentCount`。除新增分支外,候选采集的其余判定
/重试/清理逻辑未动。
- `android-buyer/app/src/main/java/com/roubao/autopilot/pinduoduo/PinduoduoPageClassifier.kt`:
- `hasOrderConfirmation` 追加第四个判据 `hasNativeOrderConfirmation`(既有
三个判据逐字节未动,新判据用 `||` 追加):同时满足「存在掩码手机号形态
文本(`\d{3}\*{4}\d{4}`,新增私有常量 `MASKED_PHONE_PATTERN`)」「存在以
`已选` 开头的文本」「存在数量或运费语义(纯数字文本、`已包邮`、包含
`运费`)」三者才成立;只产出布尔量,不留存任何原文。
- `computePaymentBoundaryAttribution()` 追加入参 `page: PinduoduoPage`,
写入 `PaymentBoundaryAttribution.page = page.name`(本项目自定义枚举的
ASCII 名称,不是页面内容);`classify()` 调用处相应传入已计算好的
`page` 局部变量,未改变 `page` 本身的判定逻辑(仅追加一次赋值读取,不重
排既有计算顺序之外的任何语句)。
- `android-buyer/app/src/main/java/com/roubao/autopilot/workflow/WorkflowModels.kt`:
`PaymentBoundaryAttribution` 追加字段 `page: String = "NONE"`(末尾追加,
既有八个字段——含 T-263 追加的三个——顺序/含义未动),`init` 增加
`PAGE_NAME_PATTERN = Regex("^[A-Z_]+$")` 校验。选择 `String` 而非直接引用
`PinduoduoPage` 类型,是为了保持既有的单向依赖方向(`pinduoduo` 包依赖
`workflow` 包,若反过来会让 `workflow.AutomationResult.Blocked`/
`WorkflowReport` 反向依赖 `pinduoduo`,与 T-262 已记录的架构决策相反)。
- `android-buyer/app/src/main/java/com/roubao/autopilot/procurement/CandidateSearchDiagnostics.kt`:
末尾追加 `specEntryAbsent: Int = 0`、`payPage: String = "NONE"` 两个字段
(既有字段顺序/含义未动)及对应 `require` 校验(含 `PAGE_NAME_PATTERN`);
`withPaymentBoundaryAttribution()` 追加 `payPage = attribution.page`(逐次
覆盖,与 `payCheckoutRoot` 等字段一致,不同于 `payStructureStreak` 的取大
合并);`auditSuffix()` 在既有 `payTextSuppressed=` 之后追加
`specEntryAbsent=`、`payPage=` 两个键,此前所有键的名称/顺序/取值逐字节
未动。
- 测试:
- `PinduoduoPageClassifierTest.kt` 新增 9 个用例:真机取证的原生结算页
样本(掩码手机号 `199****5018` + `已选:` 摘要 + 数量/`已包邮` + 微信支付
促销横幅)被识别为 `ORDER_CONFIRMATION` 且
`paymentBoundaryAttribution.page == "ORDER_CONFIRMATION"`;三个条件单独
缺失任一个都不触发(掩码手机号单独存在、已选摘要+数量但无掩码手机号);
商品详情页、规格弹层、搜索结果页在混入「已选:」摘要与纯数字文本但没有
掩码手机号时**不被**新判据误判为订单确认页(三个回归用例);归因
`toString()` 不含收件人姓名/掩码手机号/地址等测试用合成占位文本。
- `PinduoduoSpecificationParserTest.kt` 新增 1 个用例:穷举「免拼购买」
「立即购买」「确认订单」「提交订单」「去支付」「立即支付」「下单」
「加入购物车」「去结算」等购买/下单/拼单/支付语义文本,逐一断言
`isSafeEntryText()` 均返回 `false`(`SAFE_ENTRY_TEXTS`/
`SAFE_ENTRY_PREFIXES` 是纯 allowlist,不含任何交易语义词,这是移除
兜底分支后唯一的安全入口判据来源)。
- `PinduoduoCandidateAutomationTest.kt`:`FakeCandidateDriver` 的
`openSpecifications()` 改为返回枚举,新增可选构造参数
`openSpecificationsEntryAbsent`;既有「unsupported specification entry
skips candidate safely」用例追加断言 `specEntryAbsent == 0`(证明通用
不可用不会误计入该键);新增 1 个用例覆盖 `ENTRY_ABSENT` 路径——断言
`specificationOpenFailedCount`/`specEntryAbsent` 均为 1,且驱动侧唯一
发生的调用只有 `openSpecifications` 与 `returnToResults`(`
PinduoduoCandidateDriver` 接口本身没有任何购买/下单/拼单/支付语义方法可
供调用,找不到安全入口时不可能触发此类点击),`reset()` 后归零。
- `CandidateSearchDiagnosticsTest.kt`:更新 8 处既有精确匹配/`endsWith`
断言以包含 `specEntryAbsent=0 payPage=NONE`(或触发场景下的实际值)尾部
键;新增 4 个用例覆盖 `specEntryAbsent` 默认值与非负校验、`payPage`
默认值与逐次覆盖(非取大合并)语义、`payPage` 格式非法值(小写/空串)
拒绝。
- `WorkflowRunnerTest.kt`:既有「payment boundary attribution is carried
through to the report」用例追加 `page = "ORDER_CONFIRMATION"` 构造参数与
对应断言;新增 2 个用例覆盖 `PaymentBoundaryAttribution.page` 默认值
`"NONE"` 与格式非法值(小写/空串)拒绝。
#### 命令与结果
```
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testDebugUnitTest --no-daemon --console=plain"
BUILD SUCCESSFUL;Debug 单测 329 项,0 失败、0 错误(较 T-263 的 313 项新增 16 项)。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat testReleaseUnitTest --no-daemon --console=plain"
BUILD SUCCESSFUL;Release 单测 329 项,0 失败、0 错误。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat lintDebug --no-daemon --console=plain"
BUILD SUCCESSFUL。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleDebug --no-daemon --console=plain"
BUILD SUCCESSFUL。
cmd.exe /c "cd /d D:\chengma\cmroubao\android-buyer && gradlew.bat assembleRelease --no-daemon --console=plain"
BUILD SUCCESSFUL。
```
五个目标全部通过。
调试过程中曾出现一次编译错误:`return@withPinduoduoRoot` 与其返回值分写在
两行时,Kotlin 把裸 `return@label`(换行后另起一句)解析为返回 `Unit`,导致
`openPinduoduoSpecificationsDetailed()` 类型不匹配。修正为将返回值与
`return@withPinduoduoRoot` 放在同一行后编译通过,不影响任何行为逻辑。
#### 偏离任务文档之处及理由
1. **`specEntryAbsent` 通过修改 `PinduoduoCandidateDriver.openSpecifications()`
的返回类型(`Boolean` → 三态枚举)实现,而非在 `BuyerAccessibilityService`
内部用一个额外的可变字段传递「上一次是否因缺入口失败」。** 原本考虑用
`BuyerAccessibilityBridge.openSpecifications(): Boolean` 不变、另开一个
只读的 `isPinduoduoSpecificationEntryAbsent()` 供候选驱动在失败后追加查询
的方案,但那需要第二次节点树查询,且存在「两次查询之间页面已变化」的
理论竞态。改为让唯一的点击实现 `openPinduoduoSpecificationsDetailed()`
一次性产出三态结果,`openPinduoduoSpecifications(): Boolean` 只是其布尔
投影,供下单 dry-run 驱动复用同一份点击逻辑而不重复点击——这是改动面
略大但更安全、无竞态的方案。`PinduoduoOrderDryRunDriver` 是完全独立的
接口(不同文件、不同签名),未受此次接口改动影响。
2. **验收要点要求「必须写单测证明这条路径不点击任何此类控件」,但项目的
单测环境是纯 JVM(无 Robolectric、无 mockito,`build.gradle.kts` 里
`testImplementation` 只有 junit/coroutines-test/mockwebserver),无法在
单测里构造真实的 `AccessibilityNodeInfo` 来直接驱动
`BuyerAccessibilityService.openPinduoduoSpecifications()` 本身。** 因此
证明分两层:(a) `PinduoduoSpecificationParser.isSafeEntryText()`
是纯 Kotlin 函数、可直接单测,新增用例穷举购买/下单/拼单/支付语义文本
证明它们永远不会被判定为安全入口——这是移除兜底分支后唯一决定点击目标
的判据来源;(b) `PinduoduoCandidateAutomation` 经 `FakeCandidateDriver`
完全可测,新增用例证明 `ENTRY_ABSENT` 时自动化只调用
`openSpecifications`/`returnToResults`,而 `PinduoduoCandidateDriver`
接口本身没有任何购买/下单/拼单/支付语义的方法可供调用。二者结合,从
「判据永不选中购买控件」与「驱动接口没有购买类方法可调」两个角度共同
构成完整的回归证明,弥补了无法直接对 `AccessibilityNodeInfo` 做单测的
环境限制。这与 T-262 记录中「`PinduoduoUiElement` 是扁平模型,无父节点
引用」的既有架构限制是同一类工程判断的延续。
3. **`hasNativeOrderConfirmation` 触发时不强制 `safetyStop = PAYMENT_BOUNDARY`
——它只影响 `page` 的分类,不进入 `structureHit || hasCheckoutRoot ||
(textMarkerHit && !textMarkerSuppressed)` 表达式。** 这是刻意设计而非
遗漏:任务文档第二节的目标是「让既有 `awaitSpecificationEntry()` 的
`ORDER_CONFIRMATION` 分支生效」,该分支在检查 `safetyStopReason` **之前**
就已按 `page == ORDER_CONFIRMATION` 匹配并调用
`returnFromOrderConfirmation()` 安全退出——即便 `safetyStopReason` 为
`null` 也会生效。真机取证的样本本身还含有「微信支付」促销文案,会命中
既有 `paymentMarkers`(`page=ORDER_CONFIRMATION` 不在可抑制页面集合内),
因此实际场景下 `safetyStopReason` 依旧会是 `PAYMENT_BOUNDARY`——新判据
解决的是「`page` 分类不准导致 `awaitSpecificationEntry()` 走不到优雅退出
分支、只能被全局 `Blocked` 卡住」,而不是「补一条新的强制停止判据」。
`isSafeForCleanup()` 已经把 `page == ORDER_CONFIRMATION` 排除在安全清理
之外(不论 `safetyStopReason` 是否为空),因此新判据不会让清理逻辑在
该页面上意外点击任何控件;其余轮询函数(`awaitPage()`/
`awaitResultsPage()`/`validateResultsPage()`)在页面不符合预期时只做
计数与延迟,不发生点击,因此该设计不构成「放松支付边界判据」。
4. **`specEntryAbsent` 与 `payPage` 两个新键的相对顺序**:任务文档分别在
第一节(`specEntryAbsent`)与第三节(`payPage`)描述,未规定二者互相的
先后。按方案小节的编号顺序(`specEntryAbsent` 对应方案第 2 条,`payPage`
对应方案第 4 条)追加,`specEntryAbsent` 在前、`payPage` 在后。
#### 真机验证(未完成,需人工执行)
以下需人工在真机上执行,任务 status 保持 `DOING`:
- 重跑此前触发原生结算页故障的同一采购任务,确认遇到无颜色/尺码规格的
商品(如纸巾)不再点击「免拼购买」、不再进入订单确认页;若仍进入,应能
被识别为 `ORDER_CONFIRMATION` 并经 `returnFromOrderConfirmation()` 自动
退出,而非像 T-263 记录的那样卡死。
- 从 Admin 读取诊断 message 中的 `specEntryAbsent`,评估移除兜底后的候选
覆盖损失是否偏高;若偏高需另开任务以更安全的方式补回(不得点击购买按
钮)。
- 从 Admin 读取 `payPage`,确认支付边界触发时能看到页面分类名称,未触发时
为 `NONE`。