diff --git a/T-265.-.md b/T-265.-.md new file mode 100644 index 0000000..b833689 --- /dev/null +++ b/T-265.-.md @@ -0,0 +1,205 @@ + +> 同步来源:[`docs/tasks/T-265.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-265.md) · commit `afc651f75a3a` + +--- +id: T-265 +title: 恢复购买入口作为唯一规格入口并依赖订单确认页识别安全回退 +phase: 2 +deps: + - T-264 +status: DOING +created: 2026-07-31 +context_ref: d856c0b +work_branch: null +write_paths: + - docs/tasks/T-265.md + - docs/current-state.md + - android-buyer/app/src/main/** + - android-buyer/app/src/test/** +--- + +## 问题 / 背景 + +T-264 移除「免拼购买」兜底点击后,真机三轮**每一个候选都无法打开规格面板**: + +| 轮次 | attempts | specOpenFailed | specEntryAbsent | reason | +| --- | --- | --- | --- | --- | +| 1 | 2 | 2 | **2** | `CANDIDATE_SPECIFICATION_UNAVAILABLE` | +| 2 | 4 | 4 | **4** | `CANDIDATE_SPECIFICATION_UNAVAILABLE` | +| 3 | 3 | 3 | **3** | `CANDIDATE_SPECIFICATION_UNAVAILABLE` | + +`specEntryAbsent` 恒等于 `attempts`,即**覆盖损失 100%**。 + +### 真机取证:详情页根本没有独立的规格入口 + +对一个真实服装商品详情页 dump(190 节点),**全部可见文本中没有任何** +「选择规格」「请选择规格」「颜色分类」「颜色款式」「选择颜色」「选择尺码」,也没有 +以「请选择:」或「已选:」开头的文本。整页只有标题、价格、优惠券、拼单信息、店铺、 +分享/收藏/客服,以及底部的: + +``` +'已优惠6元 免拼购买' +``` + +**当前拼多多版本的商品详情页不提供独立的规格入口——「免拼购买」就是唯一入口**,点击 +后才弹出规格面板供选择颜色和尺码。 + +### T-264 的判断错误 + +T-264 依据纸巾案例(点「免拼购买」直接进入订单确认页)推断该兜底本身危险,予以移除。 +但真实情况是: + +| 商品 | 点「免拼购买」后 | +| --- | --- | +| 正常服装(有颜色/尺码) | 弹出规格面板,流程正常 | +| 纸巾(无可选规格)+ 促销 | 直接进入订单确认页 | + +**问题不在于点击该入口,而在于点击后落到订单确认页时无法识别、无法退出。** +后者已由 T-264 的原生订单确认页识别修复。 + +`openPinduoduoSpecifications()` 的既有安全入口匹配(`SAFE_ENTRY_TEXTS` / +`SAFE_ENTRY_PREFIXES`)**仍然保留**,只是当前版本页面不命中;未来版本若恢复独立入口, +该路径将优先生效。 + +## 关联需求与交互 + +- 功能:F-005、F-007 +- 用户故事:US-004、US-006 +- 交互:不新增采购操作 +- 架构/API:沿用既有诊断机制,**后端零改动** + +## 行为契约 + +### 一、恢复购买入口,但仅作为次优先路径 + +`openPinduoduoSpecifications()` 恢复「免拼购买」入口点击,**顺序不变**: + +1. 优先匹配 `SAFE_ENTRY_TEXTS` / `SAFE_ENTRY_PREFIXES` 的安全入口; +2. 仅当第 1 步找不到唯一目标时,才使用「免拼购买」入口; +3. 两者都找不到时返回 `ENTRY_ABSENT`,维持 T-264 的跳过行为。 + +**除「免拼购买」外,不得新增任何其他购买、下单、拼单、支付语义控件作为入口。** + +### 二、落到订单确认页即判定该候选无可选规格 + +点击入口后 `awaitSpecificationEntry()` 返回 `ORDER_CONFIRMATION` 时: + +1. 调用既有 `returnFromOrderConfirmation()` 退出(沿用其现有实现与有界次数); +2. **将该候选判定为「无可选规格」,跳过并继续下一个候选**,不得对同一候选重试点击; +3. 退出失败时维持既有 `Blocked(SafetyStopReason.PAYMENT_BOUNDARY)` 行为。 + +依据:任务的 SKU 硬约束要求颜色与尺码,而点击规格入口却直接进入下单流程,说明该商品 +不存在可选规格,不可能满足硬约束。 + +### 三、订单确认页上零点击 + +**这是硬边界。** 在订单确认页上,除既有 `returnFromOrderConfirmation()` 的返回动作外, +**不得点击任何控件**,尤其不得点击提交订单、立即支付、免密支付、先用后付或任何扣款 +语义控件。必须有单测证明该路径不调用任何此类动作。 + +### 四、诊断可见 + +追加计数,置于既有键之后: + +``` +specEntryViaPurchase= specLandedOnCheckout= +``` + +| 键 | 含义 | +| --- | --- | +| `specEntryViaPurchase` | 通过「免拼购买」入口打开规格的次数 | +| `specLandedOnCheckout` | 点击入口后落到订单确认页并被跳过的候选数 | + +`specLandedOnCheckout` 是本次恢复的安全审计口径:**它直接度量自动化到达订单确认页的 +频率**。若真机上该值偏高,说明大量候选无可选规格,应在更早阶段过滤(如品类判断), +而非依赖事后回退。 + +### 五、既有安全能力一律不放松 + +- 不改 T-264 的原生订单确认页识别与 `hasOrderConfirmation` 任一判据。 +- 不改 T-263 结构判据、`hasCheckoutRoot`、`paymentMarkers` 及其抑制名单。 +- 不改登录、验证码、风控的检测逻辑与优先级。 +- 不改 `returnFromOrderConfirmation()` 的既有实现与有界次数。 +- 不改 T-261 清理逻辑与 `isSafeForCleanup`。 +- 不改 T-256/T-258/T-259/T-260 的成果。 +- 既有诊断键顺序、名称、取值逐字节不变,新键追加在末尾。 + +## 方案 + +1. `openPinduoduoSpecifications()` 恢复「免拼购买」为第二优先入口,返回值区分 + 经由哪条入口打开,`ENTRY_ABSENT` 语义不变。 +2. `PinduoduoCandidateAutomation` 在 `ORDER_CONFIRMATION` 分支将候选计为 + `specLandedOnCheckout` 并跳过,不重试。 +3. 两个新计数并入诊断快照与 message 尾部。 +4. 单测覆盖:安全入口存在时优先使用且不点购买入口;安全入口缺失时使用购买入口; + 两者皆无时返回 `ENTRY_ABSENT`;落到订单确认页时只调用返回动作并跳过候选; + 订单确认页路径不调用任何提交/支付语义动作;退出失败仍返回 + `Blocked(PAYMENT_BOUNDARY)`。 + +## 验收要点 + +- [x] 安全入口存在时优先使用,不点击购买入口,有单测证明。 +- [x] 安全入口缺失时使用「免拼购买」入口,能打开规格面板。 +- [x] 两者皆无时返回 `ENTRY_ABSENT`,行为与 T-264 一致。 +- [x] 落到订单确认页时调用既有返回动作、跳过该候选、不重试,有单测证明。 +- [x] 订单确认页路径不调用任何提交订单/支付/扣款语义动作,有单测证明。 +- [x] 退出订单确认页失败时仍返回 `Blocked(PAYMENT_BOUNDARY)`。 +- [x] `specEntryViaPurchase` 与 `specLandedOnCheckout` 计数正确并出现在诊断 message。 +- [x] T-263/T-264 的支付边界与订单确认页判据未被削弱,有回归测试。 +- [x] 登录、验证码、风控判定与优先级未变。 +- [x] 既有诊断键顺序、名称、取值不变。 +- [x] Android Debug/Release 单测、`lintDebug`、`assembleDebug`、`assembleRelease` 全部通过。 +- [ ] 真机验证:重跑采购任务能打开规格面板并进入 SKU 匹配;从 Admin 读取 + `specEntryViaPurchase` 与 `specLandedOnCheckout` 记入执行记录。 + +## 边界 + +- 除「免拼购买」外不新增任何购买语义控件作为入口。 +- 订单确认页上除既有返回动作外零点击,绝不提交订单或支付。 +- 不改 T-261/T-262/T-263/T-264 的任何安全判据与清理逻辑。 +- 不改候选卡片识别、签名算法、跨轮去重、价格解析或三轮预算。 +- 不改后端代码、数据库、API 契约或 Admin 模板。 +- **不在本任务实现品类过滤**。若 `specLandedOnCheckout` 偏高,应另开任务在候选筛选 + 阶段引入品类判断(可考虑接入既有候选评估 VLM),而非在此处堆规则。 + +## 附带记录(供后续价格任务参考) + +同一次商品详情页取证显示价格文本形态为: + +``` +'券后¥15.98' 前缀「券后」 +'券后 ¥ 15.98' 含空格 +'¥' '15.98' 货币符号与数字被拆成两个独立节点 +``` + +与 T-257 观测到的 `pricePrefix` 占比吻合,并新增一个此前未知的因素:**价格被拆成多个 +节点**,这是 `PRICE_PATTERN.matchEntire` 失败的另一独立原因。价格解析任务需同时处理 +前缀与节点拆分两种情况。 + +该页面同时显示 `# 先用后付 # 支持0元下单,确认收货后再付款`,说明先用后付在正常服装 +商品上同样存在,**不能以其存在与否作为跳过依据**;下单路径排除信用支付需另行设计。 + +## 执行记录 + +- 2026-07-31:恢复唯一「免拼购买」次优先入口。入口选择先按 + `SAFE_ENTRY_TEXTS`/`SAFE_ENTRY_PREFIXES` 得到唯一安全目标;仅无唯一安全目标时 + 才接受唯一且文本以「免拼购买」结尾的目标,其它购买、下单、拼单和支付文本仍拒绝。 +- 2026-07-31:`PinduoduoSpecificationOpenOutcome` 区分安全入口与购买入口; + 候选循环对购买入口和落到订单确认页分别累计 `specEntryViaPurchase`/ + `specLandedOnCheckout`。订单确认页继续只调用既有 + `returnFromOrderConfirmation()`;成功返回后跳过当前候选,失败保持 + `Blocked(PAYMENT_BOUNDARY)`。 +- 2026-07-31:两个诊断键追加在既有 `payPage` 之后;旧键顺序、名称和格式不变。 + 单测按动作发生页面证明 `ORDER_CONFIRMATION` 上唯一动作是返回,返回到商品详情后 + 才执行回搜索结果页;同一候选不重试,下一候选可继续进入规格匹配。 +- 2026-07-31:Debug/Release 各 336 项 App 单测零失败,`lintDebug`、 + `assembleDebug`、`assembleRelease` 均通过;根 `init.ps1` 以及全部 Go 后端测试 + 通过。 +- 2026-07-31:`adb devices -l` 无已连接设备,因此不能执行最后一项真机采购任务 + 重跑和 Admin 诊断读取。任务保持 `DOING`,不伪造 + `specEntryViaPurchase`/`specLandedOnCheckout` 真机结果。 +- 2026-07-31:设备重新连接后,通过 USB 在 OnePlus PKG110 Android 16/API 36 + 覆盖安装 T-265 Debug APK;项目未在本任务修改版本号,设备仍显示 + `1.4.18 (23)`,包更新时间为 `2026-07-31 17:20:18`。`MainActivity` 正常启动, + 采购无障碍、悬浮窗、通知和 Shizuku 权限均保持启用。采购任务重跑和 Admin 两个 + 诊断计数仍待验证。