From 39686d7b3b54969bba7db9eaacbdff752290fbf9 Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 10 Aug 2026 17:11:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A2=86=E5=8F=96=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=90=8E=E7=AB=8B=E5=8D=B3=E5=88=B7=E6=96=B0=E5=88=97=E8=A1=A8?= =?UTF-8?q?=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pdd_ui.py | 13 ++++--- client/src/pdd_ui_event.py | 1 + client/src/task_dispatcher.py | 6 +++ client/test/test_pdd_ui.py | 19 ++++++++++ client/test/test_pdd_ui_event.py | 59 +++++++++++++++++++++++++++-- client/test/test_task_dispatcher.py | 32 ++++++++++++++++ docs/client/02-architecture.md | 2 +- docs/client/05-ui-specification.md | 8 ++-- 8 files changed, 125 insertions(+), 15 deletions(-) diff --git a/client/src/pdd_ui.py b/client/src/pdd_ui.py index a9989ae..beffb36 100644 --- a/client/src/pdd_ui.py +++ b/client/src/pdd_ui.py @@ -21,7 +21,7 @@ - 图标用 `FluentIcon`,**不得用表情符号**。 - 页面 `objectName` 固定为 `pddTaskPage`,不要改。 -注意:“开始自动获取”和“重新执行”会真的操作手机。重新执行当前只允许 +注意:“自动获取”和“重新执行”会真的操作手机。重新执行当前只允许 采集任务,采购任务必须在事件层拦截,不能从本入口下单。 """ @@ -387,10 +387,10 @@ class PDDTaskPage(QWidget): def _build_command_area(self) -> None: self.autoFetchButton = PrimaryPushButton( - FIF.PLAY, "开始自动获取", self + FIF.PLAY, "自动获取", self ) self.autoFetchButton.setMinimumWidth(132) - self.autoFetchButton.setAccessibleName("开始自动获取 PDD 任务") + self.autoFetchButton.setAccessibleName("自动获取 PDD 任务") self.taskTypeLabel = CaptionLabel("任务类型", self) self.taskTypeCombo = ComboBox(self) @@ -410,8 +410,8 @@ class PDDTaskPage(QWidget): self.keywordInput = SearchLineEdit(self) self.keywordInput.setPlaceholderText("任务编号、商品编号或商品标题") self.keywordInput.setClearButtonEnabled(True) - self.keywordInput.setMinimumWidth(190) - self.keywordInput.setMaximumWidth(480) + self.keywordInput.setMinimumWidth(150) + self.keywordInput.setMaximumWidth(240) self.keywordInput.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.keywordInput.setAccessibleName("任务关键词") @@ -626,7 +626,7 @@ class PDDTaskPage(QWidget): """更新自动获取按钮;state 为 stopped/starting/running/stopping。""" states = { - "stopped": ("开始自动获取", FIF.PLAY, True), + "stopped": ("自动获取", FIF.PLAY, True), "starting": ("正在启动", FIF.SYNC, False), "running": ("停止自动获取", FIF.PAUSE, True), "stopping": ("正在停止", FIF.PAUSE, False), @@ -635,6 +635,7 @@ class PDDTaskPage(QWidget): self.autoFetchButton.setText(text) self.autoFetchButton.setIcon(icon) self.autoFetchButton.setEnabled(enabled) + self.autoFetchButton.setAccessibleName(f"{text} PDD 任务") def set_engine_status(self, text: str) -> None: """更新页面底部的任务引擎状态说明。""" diff --git a/client/src/pdd_ui_event.py b/client/src/pdd_ui_event.py index d259327..3ea988a 100644 --- a/client/src/pdd_ui_event.py +++ b/client/src/pdd_ui_event.py @@ -214,6 +214,7 @@ class ClaimTaskWorker(QObject): purchase_reconcile_factory=self._purchase_reconcile_factory, cancelled=lambda: self._cancelled, device_connection_checker=self._device_connection_checker, + task_saved=self.taskSaved.emit, ) result = dispatcher.execute_one() if not self._cancelled or result.kind == "cancelled": diff --git a/client/src/task_dispatcher.py b/client/src/task_dispatcher.py index 4b354d2..ecf9f56 100644 --- a/client/src/task_dispatcher.py +++ b/client/src/task_dispatcher.py @@ -138,6 +138,7 @@ class TaskDispatcher: purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None, cancelled: Callable[[], bool] = lambda: False, device_connection_checker: Optional[Callable[[str], None]] = None, + task_saved: Callable[[str], None] = lambda _task_id: None, ) -> None: self._gateway = gateway self._repository = repository @@ -151,6 +152,7 @@ class TaskDispatcher: self._purchase_mode = purchase_mode self._reconcile_factory = purchase_reconcile_factory self._cancelled = cancelled + self._task_saved = task_saved self._device_connection_checker = ( device_connection_checker or AndroidDeviceService().require_connected @@ -251,10 +253,12 @@ class TaskDispatcher: ) if remote is None: return TaskDispatchOutcome("no_task", "暂无可领取的任务") + saved_now = False try: self._repository.add_claimed_task( admin_task_to_new_claimed_task(remote) ) + saved_now = True except DuplicateTaskError: pass except Exception as exc: @@ -266,6 +270,8 @@ class TaskDispatcher: raise RuntimeError( f"任务 {remote.task_id} 已领取,但未能保存到本地" ) + if saved_now: + self._task_saved(remote.task_id) if task.task_type is TaskType.COLLECT: service = CollectTaskService( diff --git a/client/test/test_pdd_ui.py b/client/test/test_pdd_ui.py index 08a92ba..1e2ccca 100644 --- a/client/test/test_pdd_ui.py +++ b/client/test/test_pdd_ui.py @@ -80,6 +80,25 @@ class TaskTableModelCheckTests(unittest.TestCase): self.assertEqual(page.checkedCountLabel.text(), "已选 0 条") page.deleteLater() + def test_auto_fetch_text_and_keyword_width(self): + page = PDDTaskPage() + + self.assertEqual(page.autoFetchButton.text(), "自动获取") + self.assertEqual( + page.autoFetchButton.accessibleName(), "自动获取 PDD 任务" + ) + self.assertEqual(page.keywordInput.minimumWidth(), 150) + self.assertEqual(page.keywordInput.maximumWidth(), 240) + + page.set_auto_fetch_state("running") + self.assertEqual(page.autoFetchButton.text(), "停止自动获取") + self.assertEqual( + page.autoFetchButton.accessibleName(), "停止自动获取 PDD 任务" + ) + page.set_auto_fetch_state("stopped") + self.assertEqual(page.autoFetchButton.text(), "自动获取") + page.deleteLater() + if __name__ == "__main__": unittest.main() diff --git a/client/test/test_pdd_ui_event.py b/client/test/test_pdd_ui_event.py index 1cc51f7..66264fb 100644 --- a/client/test/test_pdd_ui_event.py +++ b/client/test/test_pdd_ui_event.py @@ -182,6 +182,20 @@ class FakeCollector: return FakeCollectResult() +class BlockingCollector: + """让测试能在新任务落库后、采集结束前检查界面。""" + + def __init__(self, started, release): + self.started = started + self.release = release + + def collect(self, _task): + self.started.set() + if not self.release.wait(timeout=3): + raise RuntimeError("测试没有及时结束阻塞采集") + return FakeCollectResult() + + def fake_collect_factory(*_args): return FakeCollector() @@ -1040,6 +1054,43 @@ class PDDTaskPageEventTest(unittest.TestCase): events.shutdown() page.deleteLater() + def test_new_claim_refreshes_table_before_collection_finishes(self): + gateway = RecordingClaimGateway(collect_admin_task("COL-EARLY")) + collect_started = threading.Event() + release_collect = threading.Event() + page = PDDTaskPage() + events = PDDTaskPageEvent( + page, + self.repository, + claim_gateway=gateway, + settings_repository=self._saved_settings(), + collect_service_factory=( + lambda *_args: BlockingCollector( + collect_started, release_collect + ) + ), + ) + + page.autoFetchRequested.emit() + try: + self.assertTrue(wait_until(self.app, collect_started.is_set)) + self.assertTrue(events._claim_busy) + self.assertTrue( + wait_until( + self.app, + lambda: page.taskModel.data_row_count() == 1, + ) + ) + self.assertEqual( + page.taskModel.row_at(0).remote_task_id, "COL-EARLY" + ) + finally: + release_collect.set() + + self.assertTrue(wait_until(self.app, lambda: not events._claim_busy)) + events.shutdown() + page.deleteLater() + def test_ready_runtime_claims_and_dispatches_purchase_in_worker(self): gateway = RecordingClaimGateway(purchase_admin_task()) page = PDDTaskPage() @@ -1091,7 +1142,7 @@ class PDDTaskPageEventTest(unittest.TestCase): page.autoFetchRequested.emit() self.assertFalse(events._next_cycle_timer.isActive()) - self.assertEqual(page.autoFetchButton.text(), "开始自动获取") + self.assertEqual(page.autoFetchButton.text(), "自动获取") events.shutdown() page.deleteLater() @@ -1113,7 +1164,7 @@ class PDDTaskPageEventTest(unittest.TestCase): self.assertFalse(page.autoFetchButton.isEnabled()) self.assertTrue(wait_until(self.app, lambda: not events._claim_busy)) self.assertEqual(len(gateway.calls), 1) - self.assertEqual(page.autoFetchButton.text(), "开始自动获取") + self.assertEqual(page.autoFetchButton.text(), "自动获取") events.shutdown() page.deleteLater() @@ -1142,7 +1193,7 @@ class PDDTaskPageEventTest(unittest.TestCase): page.autoFetchRequested.emit() self.assertTrue(wait_until(self.app, lambda: not events._claim_busy)) - self.assertEqual(page.autoFetchButton.text(), "开始自动获取") + self.assertEqual(page.autoFetchButton.text(), "自动获取") events.shutdown() page.deleteLater() @@ -1307,7 +1358,7 @@ class PDDTaskPageEventTest(unittest.TestCase): self.assertEqual(gateway.calls, []) self.assertIn("设置页", page.statusLabel.text()) self.assertFalse(events._auto_fetch_running) - self.assertEqual(page.autoFetchButton.text(), "开始自动获取") + self.assertEqual(page.autoFetchButton.text(), "自动获取") events.shutdown() page.deleteLater() diff --git a/client/test/test_task_dispatcher.py b/client/test/test_task_dispatcher.py index 6371658..5b543b7 100644 --- a/client/test/test_task_dispatcher.py +++ b/client/test/test_task_dispatcher.py @@ -133,6 +133,7 @@ class TaskDispatcherTest(unittest.TestCase): live_ready=False, purchase_mode="dry_run", device_checker=lambda _serial: None, + task_saved=lambda _task_id: None, ): purchase_factory = None if purchase_ready: @@ -159,6 +160,7 @@ class TaskDispatcherTest(unittest.TestCase): ), purchase_mode=purchase_mode, device_connection_checker=device_checker, + task_saved=task_saved, ) def test_capability_only_includes_purchase_when_adapter_is_ready(self): @@ -249,6 +251,36 @@ class TaskDispatcherTest(unittest.TestCase): ) self.assertIn(("purchase", "stopped"), self.calls) + def test_new_task_notifies_after_save_and_before_execution(self): + task = AdminTask( + "COL-SAVED", + TaskType.COLLECT, + 1, + 0, + { + "goods_url": "https://example.test/COL-SAVED", + "goods_id": "COL-GOODS", + }, + ) + self.gateway.enqueue_task(task, self.client.client_id) + notifications = [] + + def record_saved(task_id): + detail = self.repository.get_task(task_id) + notifications.append( + (task_id, detail.status if detail else None, list(self.calls)) + ) + + outcome = self._dispatcher( + purchase_ready=False, task_saved=record_saved + ).execute_one() + + self.assertEqual(outcome.kind, "succeeded") + self.assertEqual( + notifications, + [("COL-SAVED", TaskStatus.CLAIMED, [])], + ) + def test_purchase_is_not_claimed_when_runtime_is_not_ready(self): self.gateway.enqueue_task(purchase_task(), self.client.client_id) diff --git a/docs/client/02-architecture.md b/docs/client/02-architecture.md index da6e566..92bd74c 100644 --- a/docs/client/02-architecture.md +++ b/docs/client/02-architecture.md @@ -87,7 +87,7 @@ client/ |---|---|---| | 主窗口文件 | `src/ui/main_window.py` | `src/ui_main.py`。**注意**:`client/AGENTS.md` 已按 `src/ui_main.py` 写规则,两处不一致,见下方说明 | | 目录分层 | domain / application / infrastructure / workers / ui | 平铺在 `src/` 下:`db.py`、`db_schema.py`、`task_repository.py`、`settings_repository.py`、`task_models.py` 等,另有 `src/util/`、`src/demo1/` | -| 主按钮文案 | 「开始自动获取」⇄「停止自动获取」 | 已按持续串行模式实现 | +| 主按钮文案 | 「自动获取」⇄「停止自动获取」 | 已按持续串行模式实现 | | Admin 网关 | `AdminGateway` + Mock/HTTP 两实现 | 登记、领取、结果和失败提交已实现 | | 任务应用服务 | `TaskDispatcher` / `CollectTaskService` / `PurchaseTaskService` | 自动获取先补交 Outbox,再按领取时间执行本地任务,最后按安全能力领取并分派新任务 | | Outbox 提交 | 从 `outbox_events` 取件重试 | 采集结果与失败已实现,重试不重复采集 | diff --git a/docs/client/05-ui-specification.md b/docs/client/05-ui-specification.md index 4301531..872c7e2 100644 --- a/docs/client/05-ui-specification.md +++ b/docs/client/05-ui-specification.md @@ -35,7 +35,7 @@ ```text ┌─────────────────────────────────────────────────────────────┐ │ PDD 任务 │ -│ [开始自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [已选0条] [重新执行] [重新上报] │ +│ [自动获取] [类型▼] [状态▼] [关键词...] [搜索] [刷新] [已选0条] [重新执行] [重新上报] │ ├─────────────────────────────────────────────────────────────┤ │ 类型 │ 商品标题 │ 颜色 │ 尺码 │ 价格 │ 数量 │ 状态 │ 更新时间 │详情│ │ │ @@ -50,10 +50,10 @@ ## 4. 顶部命令区 -### 4.1 开始自动获取 / 停止自动获取 +### 4.1 自动获取 / 停止自动获取 - 使用 `PrimaryPushButton`,是页面唯一主要强调操作。 -- 初始文本为“开始自动获取”,图标表达开始。 +- 初始文本为“自动获取”,图标表达开始。 - 启动成功后文本变为“停止自动获取”,按钮位置和宽度尽量稳定。 - `[必须]` 这两个文案是**按钮标签**。状态栏里的“自动获取:已开启/已停止”说的是**功能状态**,两者不是一回事,不要混改。 - 启动过程中禁用重复点击,显示“正在启动”。 @@ -78,7 +78,7 @@ - 任务类型:`ComboBox`,选项为“全部、采集、采购”。 - 任务状态:`ComboBox`,选项为“全部”及标准任务状态。 -- 关键词:`SearchLineEdit`,提示“任务编号、商品编号或商品标题”。 +- 关键词:`SearchLineEdit`,提示“任务编号、商品编号或商品标题”;建议宽度 150~240,随命令区布局自适应。 - 搜索:普通 `PushButton`。 筛选条件之间采用 AND。点击搜索或在关键词输入框按 Enter 执行本地数据库查询,不请求领取任务。活动筛选必须可见,筛选无结果时保留条件并提供清除入口。