From b789b50a16c26048d8f568758a02e8c5df5bc9c3 Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 10 Aug 2026 19:18:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=8B=E5=8D=95=E5=90=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=BF=9B=E5=85=A5=E5=8F=AA=E8=AF=BB=E6=A0=B8=E5=8D=95?= =?UTF-8?q?=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pdd_ui_event.py | 11 +++++ client/src/purchase_task_service.py | 2 +- client/test/test_pdd_ui_event.py | 51 +++++++++++++++++++++++ client/test/test_purchase_task_service.py | 4 +- client/test/test_task_dispatcher.py | 2 +- docs/client/06-quality-security.md | 3 ++ 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/client/src/pdd_ui_event.py b/client/src/pdd_ui_event.py index c3cff9c..329edde 100644 --- a/client/src/pdd_ui_event.py +++ b/client/src/pdd_ui_event.py @@ -112,6 +112,7 @@ ERROR_FEEDBACK_MIN_WIDTH = 520 ERROR_FEEDBACK_MIN_HEIGHT = 112 ERROR_FEEDBACK_BUTTON_MIN_WIDTH = 120 ERROR_FEEDBACK_BUTTON_MIN_HEIGHT = 40 +PURCHASE_RECONCILE_DELAY_MS = 5_000 class ClaimTaskWorker(QObject): @@ -1203,6 +1204,16 @@ class PDDTaskPageEvent(QObject): ) elif kind == "result_pending": self._on_claim_retryable_failed(message) + elif kind == "reconcile_pending": + self._retry_count = 0 + seconds = PURCHASE_RECONCILE_DELAY_MS / 1000 + self._continue_after( + PURCHASE_RECONCILE_DELAY_MS, + ( + f"自动获取:运行中 · {message};" + f"{seconds:g} 秒后自动核对订单" + ), + ) elif kind == "failed" and self._is_retry_wait_task(task_id): content = ( f"自动获取:已停止 · 任务 {task_id} 重试已暂停;" diff --git a/client/src/purchase_task_service.py b/client/src/purchase_task_service.py index b713e0f..6a501d8 100644 --- a/client/src/purchase_task_service.py +++ b/client/src/purchase_task_service.py @@ -272,7 +272,7 @@ class PurchaseTaskService: remote_task_id, ) return PurchaseTaskOutcome( - "manual_review", + "reconcile_pending", f"任务 {remote_task_id} {message}", remote_task_id, ) diff --git a/client/test/test_pdd_ui_event.py b/client/test/test_pdd_ui_event.py index d6cce28..8f3df69 100644 --- a/client/test/test_pdd_ui_event.py +++ b/client/test/test_pdd_ui_event.py @@ -24,6 +24,7 @@ from src.pdd_ui_event import ( ERROR_FEEDBACK_DURATION_MS, ERROR_FEEDBACK_MIN_HEIGHT, ERROR_FEEDBACK_MIN_WIDTH, + PURCHASE_RECONCILE_DELAY_MS, PDDTaskPageEvent, admin_task_to_new_claimed_task, summary_to_row, @@ -1474,6 +1475,56 @@ class PDDTaskPageEventTest(unittest.TestCase): events.shutdown() page.deleteLater() + def test_live_submit_schedules_read_only_reconcile_after_five_seconds(self): + page = PDDTaskPage() + events = PDDTaskPageEvent( + page, + self.repository, + claim_gateway=RecordingClaimGateway(None), + settings_repository=self._saved_settings(), + ) + events._auto_fetch_running = True + + events._on_collect_outcome( + "reconcile_pending", + "任务 PUR-001 订单提交点击已执行,结果待只读核对", + "PUR-001", + ) + + self.assertEqual( + events._cycle_next_delay_ms, PURCHASE_RECONCILE_DELAY_MS + ) + self.assertTrue(events._auto_fetch_running) + self.assertFalse(events._stop_requested) + self.assertIn("5 秒后自动核对订单", page.statusLabel.text()) + events.shutdown() + page.deleteLater() + + def test_stop_during_reconcile_wait_cancels_next_cycle(self): + page = PDDTaskPage() + events = PDDTaskPageEvent( + page, + self.repository, + claim_gateway=RecordingClaimGateway(None), + settings_repository=self._saved_settings(), + ) + events._auto_fetch_running = True + events._on_collect_outcome( + "reconcile_pending", "等待只读核对", "PUR-001" + ) + events._on_claim_thread_finished() + self.assertTrue(events._next_cycle_timer.isActive()) + self.assertEqual( + events._next_cycle_timer.interval(), PURCHASE_RECONCILE_DELAY_MS + ) + + events._request_stop_auto_fetch() + + self.assertFalse(events._next_cycle_timer.isActive()) + self.assertFalse(events._auto_fetch_running) + events.shutdown() + page.deleteLater() + def test_duplicate_task_is_normal_status(self): task = collect_admin_task() self.repository.add_claimed_task(admin_task_to_new_claimed_task(task)) diff --git a/client/test/test_purchase_task_service.py b/client/test/test_purchase_task_service.py index 1b502b4..5336072 100644 --- a/client/test/test_purchase_task_service.py +++ b/client/test/test_purchase_task_service.py @@ -304,7 +304,7 @@ class PurchaseTaskServiceTest(unittest.TestCase): outcome = self._service(adapter).execute_one_local() - self.assertEqual(outcome.kind, "manual_review") + self.assertEqual(outcome.kind, "reconcile_pending") self.assertEqual(adapter.submit_count, 1) detail = self.repository.get_task("PUR-001") run = self.repository.latest_task_run("PUR-001") @@ -332,7 +332,7 @@ class PurchaseTaskServiceTest(unittest.TestCase): outcome = self._service(adapter).execute_one_local() - self.assertEqual(outcome.kind, "manual_review") + self.assertEqual(outcome.kind, "reconcile_pending") self.assertEqual(adapter.submit_count, 1) run = self.repository.latest_task_run("PUR-001") self.assertIsNotNone(run.irreversible_action_at) diff --git a/client/test/test_task_dispatcher.py b/client/test/test_task_dispatcher.py index 2c9e785..a0db1d6 100644 --- a/client/test/test_task_dispatcher.py +++ b/client/test/test_task_dispatcher.py @@ -249,7 +249,7 @@ class TaskDispatcherTest(unittest.TestCase): ).execute_one() self.assertEqual(dry_outcome.kind, "no_task") - self.assertEqual(live_outcome.kind, "manual_review") + self.assertEqual(live_outcome.kind, "reconcile_pending") self.assertEqual( self.repository.get_task(task.task_id).execution_mode, "live", diff --git a/docs/client/06-quality-security.md b/docs/client/06-quality-security.md index 2e36c87..1c1957b 100644 --- a/docs/client/06-quality-security.md +++ b/docs/client/06-quality-security.md @@ -95,6 +95,9 @@ Client 不保存或读取 `purchase.live_*` 手工授权设置。`purchase_mode` - `execution_mode=live` 只允许采购任务,且 Client 不得自行升级 Admin 下发的模式。 - 最终点击前重新读取页面并核对规格、数量、价格、库存和唯一提交目标。 - `irreversible_action_at` 必须在点击前以事务提交;点击最多一次,返回或抛错后均只进入核单。 +- 下单点击完成或结果不确定后,任务先保存为 `reconcile_purchase`。持续自动获取 + 等待 5 秒后自动开始下一轮;Dispatcher 必须优先执行当前任务的只读核单,不能 + 重新打开商品、重新下单或先领取其他任务。5 秒只用于页面跳转缓冲,不代表下单成功。 - 正式 uiautomator2 采购 Adapter 只点击商品页采购入口和目标规格, 演练的 `enter_confirmation` 与 `stop_before_submit` 均为只读检查。 - 单元测试必须断言演练没有最终点击、live 只有一次最终点击,且模糊目标不会点击。