docs: import wiki at afc651f75a3a

ila
2026-08-07 16:37:10 +08:00
parent a6f690a823
commit 7bd29fd4bc
+150
@@ -0,0 +1,150 @@
<!-- docs-wiki-sync:docs/tasks/T-270.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
> 同步来源:[`docs/tasks/T-270.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/tasks/T-270.md) · commit `afc651f75a3a`
---
id: T-270
title: 任务阶段结束后一次性切回 Roubao
phase: 2
deps:
- T-207
status: DOING
created: 2026-08-01
context_ref: 243874a
work_branch: null
write_paths:
- docs/tasks/T-270.md
- docs/current-state.md
- android-buyer/app/src/main/**
- android-buyer/app/src/test/**
---
## 问题 / 背景
Roubao 把拼多多置于前台执行图片搜索、规格匹配或下单核对后,阶段结果虽然已经保存并
准备回传 Admin,手机仍停留在拼多多。采购人员无法直观看到任务已经进入等待确认、失败、
安全停止或待付款状态,容易误以为自动化仍在运行,也会增加手工切换 App 的操作成本。
本任务只处理**任务阶段结束后的前台归位**。状态持久化、候选/事件 outbox、Admin 回传和
支付边界仍是事实来源;切换失败不能改写业务结果,也不能诱发重复搜索或重复下单。
## 关联需求与交互
- 功能:F-005、F-006、F-007
- 用户故事:US-004、US-006
- 交互:复用 Android 采购任务页,不新增采购步骤
- 架构/API:Android 本地导航与通知;后端 API、数据库和任务状态机零改动
## 行为契约
### 1. 先保存结果,再请求切回
只有本地执行状态或 terminal outbox 已经可靠写入后,才允许请求把 Roubao 放到前台。
切回属于展示副作用,不参与候选成功、执行失败、安全停止或订单核对的事务判断。
允许触发的阶段:
- `WAITING_ADMIN_CONFIRMATION`:本轮候选采集已经封板,等待 Admin 人工确认;
- pending terminal `FAIL`:失败结果已进入 outbox;
- `AUTHORIZATION_EXPIRED`:取消、授权到期或安全停止已持久化;
- `ORDER_SUBMISSION_MANUAL_REVIEW`:订单结果无法自动确认,需要人工核对;
- `ORDER_RECONCILED`:拼多多订单号和下单时间已经保存并准备/完成回传,等待付款。
以下情况不得触发:单条候选增量回传、第一/二/三轮之间、证据仍在保存、普通 `RUNNING`、
`ORDER_DRY_RUN_RUNNING`、`ORDER_SUBMISSION_RECONCILING`、空闲同步或页面恢复步骤。
### 2. 每个执行阶段只切回一次
幂等键为 `executionId + terminalPhase`。设备本地持久化最近的幂等键;同一状态重复发布、
Service 重启、Activity/Service 同时观察到状态时只能有一个调用取得执行权。不同阶段可以
各触发一次,例如先在候选封板时返回,Admin 授权下单后又在订单核对完成时返回。
持久化中只保存幂等键摘要,不记录页面文本、商品信息、token 或设备密钥,并对历史键设置
固定上限,避免无限增长。
### 3. 使用显式 Intent 返回采购任务页
- 只启动本应用 `MainActivity`,使用显式 Intent 和
`FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP | FLAG_ACTIVITY_REORDER_TO_FRONT`;
- `MainActivity` 的既有 `singleTask` 保持不变,在 `onNewIntent()` 消费内部 action 并切到
采购任务页;不重建登录表单、不清空已填数据;
- 不使用 Home/最近任务坐标、Shizuku 或无障碍去点击系统桌面;
- 不携带候选标题、SKU、价格、图片、共享链接或其他业务数据到 Intent。
### 4. Android 后台启动限制的降级
Android 可能根据系统版本、厂商策略或用户设置拒绝后台 Activity 启动。因此触发时同步发布
一条非持续、可点击的任务结果通知,点击后使用同一个显式 Intent 进入采购任务页。后台拉起
失败不重试抢前台,不标记任务失败,不改变 outbox;通知文案仅包含本应用自有的阶段说明。
### 5. 安全边界
- 返回 Roubao 后不得继续触发拼多多动作;后续只允许既有 outbox 后台同步;
- 不放宽登录、验证码、风控、结算、提交订单或支付安全停止;
- `ORDER_RECONCILED` 仍只提醒采购人员去拼多多确认和付款,Roubao 不点击支付;
- App 不在前台、通知权限关闭或系统拦截启动时,业务结果仍必须保持已保存状态。
## 方案
1. 在采购状态模型中派生不含敏感数据的 `ForegroundReturnTarget`,统一声明允许返回的阶段。
2. 用 SharedPreferences 保存有限数量的幂等键摘要,并用进程内同步保证 Activity 和 Service
竞争时只有一个观察者取得该键。
3. 增加前台返回协调器:先 claim 幂等键,再发布完成通知并发送显式 Intent。
4. `ProcurementExecutionService` 观察状态,保证 Activity 已被系统回收或不活跃时仍能处理;
`MainActivity` 同时观察以缩短在拼多多前台执行完成后的响应时间,二者由同一幂等门保护。
5. `MainActivity.onNewIntent()` 只处理应用内部 action,并把 Compose 导航切到采购任务页。
## 验收要点
- [x] 候选封板并持久化后,Roubao 最多请求前台一次并显示采购任务页。
- [x] 同一 execution、同一阶段的重复状态发布或 Service 重启不会重复抢前台。
- [x] 同一 execution 从等待候选确认进入订单核对完成时,两个不同阶段各允许返回一次。
- [x] pending terminal `FAIL`、授权到期/安全停止、人工核对、订单已核对均有明确返回目标。
- [x] 中间轮次、单条候选、普通运行和下单核对过程中不会返回 Roubao。
- [x] 系统不允许后台启动时,可通过结果通知进入同一采购任务页;业务状态不受影响。
- [x] Intent、通知和日志不含任务商品数据、页面原文或认证秘密。
- [x] Android Debug/Release 单测、lint 和 APK 构建通过。
- [ ] 真机让拼多多停留前台,分别验证候选封板与订单核对完成后的返回行为;若系统拦截,
验证通知降级且不重复启动。
## 边界
- 不修改 Admin、Go API、数据库 migration、候选匹配、三轮搜索预算或下单自动化。
- 不承诺绕过 Android/厂商对后台 Activity 启动的系统限制。
- 不新增自动支付、免密支付或绕过登录/验证码/风控的能力。
- 本任务不关闭 T-265/T-268/T-269 的独立真机验收项。
## 执行记录
- 2026-08-01:根据阶段完成后手机仍停在拼多多的问题创建任务,状态 `TODO`;先提交行为
契约,尚未修改生产代码。
- 2026-08-01:领取任务,状态改为 `DOING`,开始 Android 实现。
- 2026-08-01:新增 `ProcurementForegroundReturnPolicy`,只从已持久化的候选等待确认、
pending `FAIL`、授权结束、人工对账和订单已核对状态派生返回目标;普通运行、各轮搜索、
dry-run 和提交对账中间状态均返回 null。`ProcurementUiState` 只暴露 execution ID 与自有
阶段枚举,不带商品和页面数据。
- 2026-08-01:新增进程共享的 `ProcurementForegroundReturnCoordinator`。先以
`SHA-256(executionId + phase)` 同步提交最多 32 条本地 receipt,再发布可点击的采购结果
通知并发送显式 `NEW_TASK | SINGLE_TOP | REORDER_TO_FRONT` Intent;Activity 与前台
Service 同时观察同一状态,但共享 gate 保证只消费一次。`MainActivity.onNewIntent()`
只切换到既有采购任务页,未重建登录输入;Service 在同步前后也检查目标,避免 STOP 时
来不及返回。
- 修改文件:`App.kt`、`MainActivity.kt`、`ProcurementExecutionService.kt`、
`ProcurementForegroundReturn.kt`、`ProcurementModels.kt`、`ProcurementRepository.kt`、
`ProcurementForegroundReturnTest.kt`、本任务和 `docs/current-state.md`。
- 自动化验证:首次运行 `.\gradlew.bat :app:compileDebugKotlin` 因当前 shell 未设置
`ANDROID_HOME`,在依赖解析前失败;设置
`$env:ANDROID_HOME="$env:LOCALAPPDATA\Android\Sdk"` 和同值 `ANDROID_SDK_ROOT` 后,
`:app:compileDebugKotlin` 成功。
- 自动化验证:运行
`.\gradlew.bat :app:testDebugUnitTest :app:testReleaseUnitTest :app:lintDebug
:app:assembleDebug :app:assembleRelease` 成功;Debug/Release 各 417 项单测,0 failure、
0 error,lint 成功,`app-debug.apk` 与 `app-release-unsigned.apk` 均生成。
- 仓库验证:在项目根运行 `.\init.ps1` 成功;Android 全量测试和 Debug APK 构建通过,
Go 后端所有 package 测试通过。
- 真机 smoke:设备 PKG110,Android 16 / SDK 36,Roubao 1.4.18 (23),拼多多 8.17.0。
通过 `adb install -r ...\app-debug.apk` 覆盖安装成功;用包管理器解析并显式启动拼多多后,
`topResumedActivity` 为 `com.xunmeng.pinduoduo/.activity.NewPageActivity`;再发送
`com.roubao.autopilot.action.SHOW_PROCUREMENT` 到 `.MainActivity`,前台变为
`com.roubao.autopilot/.MainActivity`。该 smoke 未操作商品、订单或支付。
- 未验证范围:尚未用一条真实任务让候选封板/订单核对状态自动触发返回,也未在系统实际
拦截后台 Activity 启动的场景验证通知点击降级。因此状态保持 `DOING`,不改为 `DONE`。