回填不再因单张订单详情读失败而中止整轮扫描 #322

Open
opened 2026-09-18 18:00:11 +08:00 by ila · 1 comment
Owner

原始需求

用户 2026-09-18:「这次工单主要解决没有按照指定天数遍历点击"pdd app-我的订单"订单里所有订单详情入口」。

背景:真机 CTN5T20416012406 触发回填,默认 2 天窗口,结果「已检查 6 · 成功 0 · 已回填 0 · 冲突/失败 0,滚动后无法确认订单详情,未完整扫描」——远未遍历完 2 天内应有的订单数。

事实(代码复核,2026-09-18)

scan() 读单张卡片详情时用 check() 直接抛异常:

check(BackfillPagePolicy.detail(detail)) { "点击后未识别订单详情" }
...
check(BackfillPagePolicy.detail(detail)) { "滚动后无法确认订单详情,未完整扫描" }

这两处都没有任何 try/catch 包住单张卡片,异常一路冒泡到 scan() 最外层,被 AgentForegroundService 顶层 catch 后整个回填任务直接结束。只要遍历过程中任何一单的详情读取失败一次,无论第几单,2 天窗口内其余所有订单都不会再检查——这就是「已检查 6」后中止、没有遍历完指定天数的直接原因,与具体哪一单为什么失败无关,这个结构性缺陷本身就会导致提前收尾。

真机现场停在订单列表(非详情页),但检查时间已滞后且不确定期间是否有人工操作,不作为触发原因的确证;本工单只解决「失败即中止整轮」这个结构性问题,不深究具体触发条件。

方案

把详情读取逻辑抽成 readDetail(): BackfillDetail?,两处判据从 check()(抛异常)改为返回 null(信号「这一单读不到」)。scan() 收到 null 时:

  • 计数 readFailures,不提交任何数据;
  • 调用 awaitOrders() 尝试恢复到列表——它已有「停在陈旧详情页先退出」(#318)和「长时间空壳重新加载」(#317)的恢复路径;
  • 恢复成功则继续下一张卡片(该卡片指纹已提前写入 seenCards,不会被重新挑中);
  • 恢复不了(awaitOrders() 自己超时)才真正中止,这种情况值得停下来。

checkActive()(用户取消、10 分钟上限、服务器/设备身份变化)不受影响,仍直接抛出并立即中止——只有「这一单读不到」被降级为跳过,真正的中止条件不变。

跳过的单数写入最终结果文案(finish() 追加"有 N 单详情读取失败已跳过,未纳入结果"),不悄悄吞掉。

验收

  • 新增用例:3 张卡片中第 2 张详情读取失败,扫描继续处理第 3 张;结果包含 2 个成功项,文案披露「1 单详情读取失败已跳过」。
  • 还原为旧的 check() 写法后该用例必须红。
  • 既有全部用例不受影响。
  • 真机验证:待有合适设备后确认能遍历完指定天数窗口内的全部订单,不再因单点失败提前收尾。

风险与回退

跳过路径复用已有的 awaitOrders() 恢复机制,不新增独立恢复逻辑;checkActive() 触发的中止条件未改变。回退即把两处 if (!detail) return null 换回 check()。

文档影响

无长期文档影响:未改入口、配置、接口,只改回填扫描器内部的失败处理策略。

## 原始需求 用户 2026-09-18:「这次工单主要解决没有按照指定天数遍历点击"pdd app-我的订单"订单里所有订单详情入口」。 背景:真机 CTN5T20416012406 触发回填,默认 2 天窗口,结果「已检查 6 · 成功 0 · 已回填 0 · 冲突/失败 0,滚动后无法确认订单详情,未完整扫描」——远未遍历完 2 天内应有的订单数。 ## 事实(代码复核,2026-09-18) `scan()` 读单张卡片详情时用 `check()` 直接抛异常: ```kotlin check(BackfillPagePolicy.detail(detail)) { "点击后未识别订单详情" } ... check(BackfillPagePolicy.detail(detail)) { "滚动后无法确认订单详情,未完整扫描" } ``` 这两处都没有任何 try/catch 包住单张卡片,异常一路冒泡到 `scan()` 最外层,被 `AgentForegroundService` 顶层 catch 后整个回填任务直接结束。**只要遍历过程中任何一单的详情读取失败一次,无论第几单,2 天窗口内其余所有订单都不会再检查**——这就是「已检查 6」后中止、没有遍历完指定天数的直接原因,与具体哪一单为什么失败无关,这个结构性缺陷本身就会导致提前收尾。 真机现场停在订单列表(非详情页),但检查时间已滞后且不确定期间是否有人工操作,不作为触发原因的确证;本工单只解决「失败即中止整轮」这个结构性问题,不深究具体触发条件。 ## 方案 把详情读取逻辑抽成 `readDetail(): BackfillDetail?`,两处判据从 `check()`(抛异常)改为返回 `null`(信号「这一单读不到」)。`scan()` 收到 null 时: - 计数 `readFailures`,不提交任何数据; - 调用 `awaitOrders()` 尝试恢复到列表——它已有「停在陈旧详情页先退出」(#318)和「长时间空壳重新加载」(#317)的恢复路径; - 恢复成功则继续下一张卡片(该卡片指纹已提前写入 `seenCards`,不会被重新挑中); - 恢复不了(`awaitOrders()` 自己超时)才真正中止,这种情况值得停下来。 `checkActive()`(用户取消、10 分钟上限、服务器/设备身份变化)不受影响,仍直接抛出并立即中止——只有「这一单读不到」被降级为跳过,真正的中止条件不变。 跳过的单数写入最终结果文案(`finish()` 追加"有 N 单详情读取失败已跳过,未纳入结果"),不悄悄吞掉。 ## 验收 - 新增用例:3 张卡片中第 2 张详情读取失败,扫描继续处理第 3 张;结果包含 2 个成功项,文案披露「1 单详情读取失败已跳过」。 - 还原为旧的 `check()` 写法后该用例必须红。 - 既有全部用例不受影响。 - 真机验证:待有合适设备后确认能遍历完指定天数窗口内的全部订单,不再因单点失败提前收尾。 ## 风险与回退 跳过路径复用已有的 `awaitOrders()` 恢复机制,不新增独立恢复逻辑;`checkActive()` 触发的中止条件未改变。回退即把两处 `if (!detail) return null` 换回 `check()`。 ## 文档影响 无长期文档影响:未改入口、配置、接口,只改回填扫描器内部的失败处理策略。
Author
Owner

实施

提交 bd78fc2(deploy-main)。抽出 readDetail(),两处判据从 check() 改为返回 null;scan() 遇到 null 时计数、调用 awaitOrders() 恢复后继续下一张卡片;finish() 追加跳过单数说明。

验证

  • 单测 412 通过 0 失败。
  • 新增用例覆盖「3 张卡片中第 2 张读失败,扫描继续处理第 3 张」,断言成功项数量、订单号集合、结果文案含跳过说明。
  • 反证:还原为旧 check() 写法后该用例变红。

未验证

真机验证——你手上暂时没有真正有采购记录(含 _cg 标记订单)的手机可测。这条改动能不能让 CTN5T20416012406 遍历完 2 天窗口,需要等能确认「失败后确实恢复到列表并继续」这一步在真机上成立;awaitOrders() 的恢复逻辑本身(#317/#318)也还没有被完整扫描流程验证过。

状态:待验收。

## 实施 提交 `bd78fc2`(deploy-main)。抽出 `readDetail()`,两处判据从 `check()` 改为返回 null;`scan()` 遇到 null 时计数、调用 `awaitOrders()` 恢复后继续下一张卡片;`finish()` 追加跳过单数说明。 ## 验证 - 单测 412 通过 0 失败。 - 新增用例覆盖「3 张卡片中第 2 张读失败,扫描继续处理第 3 张」,断言成功项数量、订单号集合、结果文案含跳过说明。 - 反证:还原为旧 `check()` 写法后该用例变红。 ## 未验证 真机验证——你手上暂时没有真正有采购记录(含 _cg 标记订单)的手机可测。这条改动能不能让 CTN5T20416012406 遍历完 2 天窗口,需要等能确认「失败后确实恢复到列表并继续」这一步在真机上成立;`awaitOrders()` 的恢复逻辑本身(#317/#318)也还没有被完整扫描流程验证过。 状态:待验收。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#322