fix: 优化重新采集取消交互 (#82)
This commit is contained in:
@@ -126,12 +126,20 @@ class CollectTaskService:
|
|||||||
def _execute_task(self, remote_task_id: str) -> CollectTaskOutcome:
|
def _execute_task(self, remote_task_id: str) -> CollectTaskOutcome:
|
||||||
"""执行一条已经保存在本地且处于待执行状态的采集任务。"""
|
"""执行一条已经保存在本地且处于待执行状态的采集任务。"""
|
||||||
|
|
||||||
if self._cancelled():
|
|
||||||
return CollectTaskOutcome("cancelled", "本次采集已取消", remote_task_id)
|
|
||||||
|
|
||||||
started = self._repository.start_collect_run(
|
started = self._repository.start_collect_run(
|
||||||
remote_task_id, self._device_address
|
remote_task_id, self._device_address
|
||||||
)
|
)
|
||||||
|
if self._cancelled():
|
||||||
|
event = self._repository.save_collect_failure(
|
||||||
|
remote_task_id,
|
||||||
|
started.attempt_id,
|
||||||
|
TaskStatus.CANCELLED,
|
||||||
|
"PDD_CANCELLED",
|
||||||
|
"采集任务已安全取消",
|
||||||
|
False,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
return self._submit_cancelled(event, remote_task_id)
|
||||||
collector = self._factory(
|
collector = self._factory(
|
||||||
self._device_address, self._client.client_id, self._cancelled
|
self._device_address, self._client.client_id, self._cancelled
|
||||||
)
|
)
|
||||||
@@ -152,8 +160,22 @@ class CollectTaskService:
|
|||||||
retryable,
|
retryable,
|
||||||
exc.diagnostics,
|
exc.diagnostics,
|
||||||
)
|
)
|
||||||
|
if exc.code == "PDD_CANCELLED":
|
||||||
|
return self._submit_cancelled(event, remote_task_id)
|
||||||
return self._submit(event)
|
return self._submit(event)
|
||||||
|
|
||||||
|
def _submit_cancelled(
|
||||||
|
self, event: OutboxEventRecord, remote_task_id: str
|
||||||
|
) -> CollectTaskOutcome:
|
||||||
|
"""提交取消结果;Admin 接收后对界面返回明确的已取消。"""
|
||||||
|
|
||||||
|
outcome = self._submit(event)
|
||||||
|
if outcome.kind == "failed":
|
||||||
|
return CollectTaskOutcome(
|
||||||
|
"cancelled", f"任务 {remote_task_id} 已安全取消", remote_task_id
|
||||||
|
)
|
||||||
|
return outcome
|
||||||
|
|
||||||
def _submit(self, event: OutboxEventRecord) -> CollectTaskOutcome:
|
def _submit(self, event: OutboxEventRecord) -> CollectTaskOutcome:
|
||||||
task_id = self._repository.outbox_task_id(event.id)
|
task_id = self._repository.outbox_task_id(event.id)
|
||||||
self._repository.mark_outbox_sending(event.id)
|
self._repository.mark_outbox_sending(event.id)
|
||||||
|
|||||||
+32
-5
@@ -277,12 +277,14 @@ class PDDTaskPage(QWidget):
|
|||||||
searchRequested = pyqtSignal(dict)
|
searchRequested = pyqtSignal(dict)
|
||||||
refreshRequested = pyqtSignal()
|
refreshRequested = pyqtSignal()
|
||||||
rerunRequested = pyqtSignal(str)
|
rerunRequested = pyqtSignal(str)
|
||||||
|
rerunCancelRequested = pyqtSignal()
|
||||||
detailRequested = pyqtSignal(str)
|
detailRequested = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("pddTaskPage")
|
self.setObjectName("pddTaskPage")
|
||||||
|
|
||||||
|
self._rerun_state = "idle"
|
||||||
self.taskModel = TaskTableModel(self)
|
self.taskModel = TaskTableModel(self)
|
||||||
self._build_command_area()
|
self._build_command_area()
|
||||||
self._build_content_area()
|
self._build_content_area()
|
||||||
@@ -508,22 +510,47 @@ class PDDTaskPage(QWidget):
|
|||||||
return task.remote_task_id if task else ""
|
return task.remote_task_id if task else ""
|
||||||
|
|
||||||
def set_rerun_running(self, running: bool) -> None:
|
def set_rerun_running(self, running: bool) -> None:
|
||||||
"""重新采集期间锁定两个会操作手机的入口。"""
|
"""设置重新采集状态;运行时保留可见的停止入口。"""
|
||||||
|
|
||||||
self.rerunButton.setText("正在重新采集" if running else "重新执行")
|
self._rerun_state = "running" if running else "idle"
|
||||||
self.autoFetchButton.setEnabled(not running)
|
self._update_rerun_button()
|
||||||
|
|
||||||
|
def set_rerun_cancelling(self) -> None:
|
||||||
|
"""显示取消请求已发出,防止用户重复点击。"""
|
||||||
|
|
||||||
|
self._rerun_state = "cancelling"
|
||||||
self._update_rerun_button()
|
self._update_rerun_button()
|
||||||
|
|
||||||
def _request_rerun(self) -> None:
|
def _request_rerun(self) -> None:
|
||||||
|
if self._rerun_state == "running":
|
||||||
|
self.rerunCancelRequested.emit()
|
||||||
|
return
|
||||||
|
if self._rerun_state != "idle":
|
||||||
|
return
|
||||||
task_id = self.current_task_id()
|
task_id = self.current_task_id()
|
||||||
if task_id:
|
if task_id:
|
||||||
self.rerunRequested.emit(task_id)
|
self.rerunRequested.emit(task_id)
|
||||||
|
|
||||||
def _update_rerun_button(self) -> None:
|
def _update_rerun_button(self) -> None:
|
||||||
running = self.rerunButton.text() == "正在重新采集"
|
if self._rerun_state == "running":
|
||||||
|
self.rerunButton.setText("停止重新采集")
|
||||||
|
self.rerunButton.setAccessibleName("停止当前重新采集")
|
||||||
|
self.rerunButton.setEnabled(True)
|
||||||
|
self.autoFetchButton.setEnabled(False)
|
||||||
|
return
|
||||||
|
if self._rerun_state == "cancelling":
|
||||||
|
self.rerunButton.setText("正在停止…")
|
||||||
|
self.rerunButton.setAccessibleName("正在停止重新采集")
|
||||||
|
self.rerunButton.setEnabled(False)
|
||||||
|
self.autoFetchButton.setEnabled(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rerunButton.setText("重新执行")
|
||||||
|
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||||
|
self.autoFetchButton.setEnabled(True)
|
||||||
has_selection = self.taskTable.selectionModel().hasSelection()
|
has_selection = self.taskTable.selectionModel().hasSelection()
|
||||||
self.rerunButton.setEnabled(
|
self.rerunButton.setEnabled(
|
||||||
has_selection and bool(self.current_task_id()) and not running
|
has_selection and bool(self.current_task_id())
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_filters(self) -> None:
|
def _apply_filters(self) -> None:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from PyQt5.QtCore import (
|
|||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
pyqtSlot,
|
pyqtSlot,
|
||||||
)
|
)
|
||||||
from qfluentwidgets import InfoBar, InfoBarPosition, MessageBox
|
from qfluentwidgets import InfoBar, InfoBarPosition, MessageBox, PushButton
|
||||||
|
|
||||||
from .admin_gateway import (
|
from .admin_gateway import (
|
||||||
AdminGatewayError,
|
AdminGatewayError,
|
||||||
@@ -134,7 +134,7 @@ class ClaimTaskWorker(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
try:
|
try:
|
||||||
if self._cancelled:
|
if self._cancelled and not self._selected_task_id:
|
||||||
return
|
return
|
||||||
client_settings = self._client_service.load()
|
client_settings = self._client_service.load()
|
||||||
if not client_settings.client_id:
|
if not client_settings.client_id:
|
||||||
@@ -212,6 +212,8 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._claim_busy = False
|
self._claim_busy = False
|
||||||
self._claim_thread: Optional[QThread] = None
|
self._claim_thread: Optional[QThread] = None
|
||||||
self._claim_worker: Optional[ClaimTaskWorker] = None
|
self._claim_worker: Optional[ClaimTaskWorker] = None
|
||||||
|
self._rerun_cancel_requested = False
|
||||||
|
self._rerun_feedback: Optional[InfoBar] = None
|
||||||
self._auto_fetch_running = False
|
self._auto_fetch_running = False
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
self._cycle_next_delay_ms: Optional[int] = None
|
self._cycle_next_delay_ms: Optional[int] = None
|
||||||
@@ -261,6 +263,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
page.searchRequested.connect(self.search_tasks)
|
page.searchRequested.connect(self.search_tasks)
|
||||||
page.refreshRequested.connect(self.refresh_tasks)
|
page.refreshRequested.connect(self.refresh_tasks)
|
||||||
page.rerunRequested.connect(self.request_rerun)
|
page.rerunRequested.connect(self.request_rerun)
|
||||||
|
page.rerunCancelRequested.connect(self.request_cancel_rerun)
|
||||||
page.autoFetchRequested.connect(self._request_claim_task)
|
page.autoFetchRequested.connect(self._request_claim_task)
|
||||||
page.detailRequested.connect(self.show_task_detail)
|
page.detailRequested.connect(self.show_task_detail)
|
||||||
page.taskModel.loadMoreRequested.connect(self._load_page)
|
page.taskModel.loadMoreRequested.connect(self._load_page)
|
||||||
@@ -345,7 +348,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._page.window(),
|
self._page.window(),
|
||||||
)
|
)
|
||||||
dialog.yesButton.setText("重新采集")
|
dialog.yesButton.setText("重新采集")
|
||||||
dialog.cancelButton.setText("取消")
|
dialog.cancelButton.setText("暂不重新采集")
|
||||||
dialog.cancelButton.setFocus()
|
dialog.cancelButton.setFocus()
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
return
|
return
|
||||||
@@ -367,6 +370,8 @@ class PDDTaskPageEvent(QObject):
|
|||||||
"""启动只处理指定任务的工作线程。"""
|
"""启动只处理指定任务的工作线程。"""
|
||||||
|
|
||||||
assert self._claim_gateway is not None
|
assert self._claim_gateway is not None
|
||||||
|
self._close_rerun_feedback()
|
||||||
|
self._rerun_cancel_requested = False
|
||||||
self._claim_busy = True
|
self._claim_busy = True
|
||||||
self._page.set_rerun_running(True)
|
self._page.set_rerun_running(True)
|
||||||
self._page.set_engine_status(f"正在重新采集任务 {task_id}…")
|
self._page.set_engine_status(f"正在重新采集任务 {task_id}…")
|
||||||
@@ -394,32 +399,67 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._claim_worker = worker
|
self._claim_worker = worker
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def request_cancel_rerun(self) -> None:
|
||||||
|
"""请求当前重新采集在下一个安全点停止。"""
|
||||||
|
|
||||||
|
worker = self._claim_worker
|
||||||
|
if (
|
||||||
|
self._closing
|
||||||
|
or self._rerun_cancel_requested
|
||||||
|
or not self._claim_busy
|
||||||
|
or worker is None
|
||||||
|
):
|
||||||
|
return
|
||||||
|
self._rerun_cancel_requested = True
|
||||||
|
self._page.set_rerun_cancelling()
|
||||||
|
self._page.set_engine_status(
|
||||||
|
"正在停止重新采集…正在等待手机当前操作结束"
|
||||||
|
)
|
||||||
|
# uiautomator2/ADB 的单次调用无法安全强制中断。
|
||||||
|
# Worker 会在调用返回后通过 cancelled 回调在安全点停止。
|
||||||
|
worker.cancel()
|
||||||
|
|
||||||
@pyqtSlot(str, str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def _on_rerun_outcome(self, kind: str, message: str, _task_id: str) -> None:
|
def _on_rerun_outcome(self, kind: str, message: str, _task_id: str) -> None:
|
||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
self._reload()
|
self._reload()
|
||||||
|
if self._rerun_cancel_requested:
|
||||||
|
self._page.set_engine_status(
|
||||||
|
"停止请求已处理,请查看任务最新状态"
|
||||||
|
)
|
||||||
|
return
|
||||||
self._page.set_engine_status(message)
|
self._page.set_engine_status(message)
|
||||||
if kind != "succeeded":
|
if kind != "succeeded":
|
||||||
self._show_claim_error("重新采集需要处理", message)
|
self._show_rerun_error("重新采集需要处理", message)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _on_rerun_failed(self, message: str) -> None:
|
def _on_rerun_failed(self, message: str) -> None:
|
||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
|
if self._rerun_cancel_requested:
|
||||||
|
return
|
||||||
self._page.set_engine_status(message)
|
self._page.set_engine_status(message)
|
||||||
self._show_claim_error("重新采集失败", message)
|
self._show_rerun_error("重新采集失败", message)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _on_rerun_thread_finished(self) -> None:
|
def _on_rerun_thread_finished(self) -> None:
|
||||||
|
cancel_requested = self._rerun_cancel_requested
|
||||||
self._claim_worker = None
|
self._claim_worker = None
|
||||||
self._claim_thread = None
|
self._claim_thread = None
|
||||||
self._claim_busy = False
|
self._claim_busy = False
|
||||||
|
self._rerun_cancel_requested = False
|
||||||
if not self._closing:
|
if not self._closing:
|
||||||
self._page.set_rerun_running(False)
|
self._page.set_rerun_running(False)
|
||||||
|
if cancel_requested:
|
||||||
|
self._reload()
|
||||||
|
self._page.set_engine_status(
|
||||||
|
"停止请求已处理,请查看任务最新状态"
|
||||||
|
)
|
||||||
|
|
||||||
def _show_rerun_warning(self, title: str, content: str) -> None:
|
def _show_rerun_warning(self, title: str, content: str) -> None:
|
||||||
InfoBar.warning(
|
bar = InfoBar.warning(
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
isClosable=True,
|
isClosable=True,
|
||||||
@@ -427,6 +467,51 @@ class PDDTaskPageEvent(QObject):
|
|||||||
position=InfoBarPosition.TOP_RIGHT,
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
parent=self._page,
|
parent=self._page,
|
||||||
)
|
)
|
||||||
|
self._replace_rerun_feedback(bar)
|
||||||
|
|
||||||
|
def _show_rerun_error(self, title: str, content: str) -> None:
|
||||||
|
"""显示一条可明确关闭的重新采集错误。"""
|
||||||
|
|
||||||
|
bar = InfoBar.error(
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
isClosable=True,
|
||||||
|
duration=-1,
|
||||||
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
|
parent=self._page,
|
||||||
|
)
|
||||||
|
self._replace_rerun_feedback(bar)
|
||||||
|
|
||||||
|
def _replace_rerun_feedback(self, bar: InfoBar) -> None:
|
||||||
|
"""用新提示替换旧提示,避免连续失败后堆叠。"""
|
||||||
|
|
||||||
|
self._close_rerun_feedback()
|
||||||
|
self._rerun_feedback = bar
|
||||||
|
close_button = PushButton("关闭提示", bar)
|
||||||
|
close_button.setAccessibleName("关闭重新采集提示")
|
||||||
|
close_button.clicked.connect(
|
||||||
|
lambda _checked=False, current=bar: self._close_rerun_feedback(current)
|
||||||
|
)
|
||||||
|
bar.addWidget(close_button)
|
||||||
|
bar.destroyed.connect(
|
||||||
|
lambda _object=None, current=bar: self._forget_rerun_feedback(current)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _close_rerun_feedback(self, expected: Optional[InfoBar] = None) -> None:
|
||||||
|
"""关闭当前重新采集提示;旧提示不得关闭新提示。"""
|
||||||
|
|
||||||
|
bar = self._rerun_feedback
|
||||||
|
if bar is None or (expected is not None and bar is not expected):
|
||||||
|
return
|
||||||
|
self._rerun_feedback = None
|
||||||
|
try:
|
||||||
|
bar.close()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _forget_rerun_feedback(self, bar: InfoBar) -> None:
|
||||||
|
if self._rerun_feedback is bar:
|
||||||
|
self._rerun_feedback = None
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _request_claim_task(self) -> None:
|
def _request_claim_task(self) -> None:
|
||||||
@@ -763,6 +848,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._auto_fetch_running = False
|
self._auto_fetch_running = False
|
||||||
self._stop_requested = True
|
self._stop_requested = True
|
||||||
self._next_cycle_timer.stop()
|
self._next_cycle_timer.stop()
|
||||||
|
self._close_rerun_feedback()
|
||||||
|
|
||||||
for window in list(self._detail_windows.values()):
|
for window in list(self._detail_windows.values()):
|
||||||
window.close()
|
window.close()
|
||||||
|
|||||||
@@ -194,6 +194,40 @@ class CollectTaskServiceTest(unittest.TestCase):
|
|||||||
self.repository.get_task("COL-OTHER").status, TaskStatus.CLAIMED
|
self.repository.get_task("COL-OTHER").status, TaskStatus.CLAIMED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_selected_task_cancelled_before_collector_starts_is_persisted(self):
|
||||||
|
self.repository.add_claimed_task(
|
||||||
|
NewClaimedTask(
|
||||||
|
remote_task_id="COL-CANCEL",
|
||||||
|
task_type=TaskType.COLLECT,
|
||||||
|
goods_url="https://example.test/COL-CANCEL",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class AcceptGateway:
|
||||||
|
def submit_failure(self, *_args):
|
||||||
|
return SubmissionReceipt(
|
||||||
|
True, "FAILURE-001", "2026-08-07T08:00:00Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
service = CollectTaskService(
|
||||||
|
AcceptGateway(),
|
||||||
|
self.repository,
|
||||||
|
self.client,
|
||||||
|
"USB-001",
|
||||||
|
cancelled=lambda: True,
|
||||||
|
collect_service_factory=lambda *_args: FakeCollector(calls),
|
||||||
|
)
|
||||||
|
|
||||||
|
outcome = service.execute_selected("COL-CANCEL")
|
||||||
|
|
||||||
|
self.assertEqual(outcome.kind, "cancelled")
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
self.assertEqual(
|
||||||
|
self.repository.get_task("COL-CANCEL").status,
|
||||||
|
TaskStatus.CANCELLED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from src.pdd_ui_event import (
|
|||||||
)
|
)
|
||||||
from src.mock_admin_gateway import MockAdminGateway
|
from src.mock_admin_gateway import MockAdminGateway
|
||||||
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
||||||
|
from src.pdd_collect_service import PddCollectError
|
||||||
from src.pdd_u2_purchase_adapter import create_u2_purchase_adapter
|
from src.pdd_u2_purchase_adapter import create_u2_purchase_adapter
|
||||||
from src.settings_repository import SettingsRepository
|
from src.settings_repository import SettingsRepository
|
||||||
from src.task_models import NewClaimedTask, TaskStatus, TaskSummary, TaskType
|
from src.task_models import NewClaimedTask, TaskStatus, TaskSummary, TaskType
|
||||||
@@ -361,6 +362,125 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
events.shutdown()
|
events.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_rerun_confirmation_has_clear_safe_cancel_action(self):
|
||||||
|
self._add_task(1)
|
||||||
|
first = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||||
|
event = self.repository.save_collect_result(
|
||||||
|
"PDD-001", first.attempt_id, FakeCollectResult().to_pdd_data()
|
||||||
|
)
|
||||||
|
self.repository.mark_outbox_sent(event.id)
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(
|
||||||
|
page,
|
||||||
|
self.repository,
|
||||||
|
claim_gateway=RecordingClaimGateway(),
|
||||||
|
settings_repository=self._saved_settings(),
|
||||||
|
collect_service_factory=fake_collect_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("src.pdd_ui_event.MessageBox") as message_box:
|
||||||
|
message_box.return_value.exec.return_value = False
|
||||||
|
page.rerunRequested.emit("PDD-001")
|
||||||
|
|
||||||
|
message_box.return_value.cancelButton.setText.assert_called_once_with(
|
||||||
|
"暂不重新采集"
|
||||||
|
)
|
||||||
|
message_box.return_value.cancelButton.setFocus.assert_called_once_with()
|
||||||
|
self.assertFalse(events._claim_busy)
|
||||||
|
self.assertEqual(
|
||||||
|
self.repository.get_task("PDD-001").status, TaskStatus.SUCCEEDED
|
||||||
|
)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_running_rerun_can_request_cooperative_cancel_once(self):
|
||||||
|
self._add_task(1)
|
||||||
|
first = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||||
|
event = self.repository.save_collect_result(
|
||||||
|
"PDD-001", first.attempt_id, FakeCollectResult().to_pdd_data()
|
||||||
|
)
|
||||||
|
self.repository.mark_outbox_sent(event.id)
|
||||||
|
started = threading.Event()
|
||||||
|
|
||||||
|
def cancellable_factory(_address, _client_id, cancelled):
|
||||||
|
class CancellableCollector:
|
||||||
|
def collect(self, _task):
|
||||||
|
started.set()
|
||||||
|
while not cancelled():
|
||||||
|
time.sleep(0.01)
|
||||||
|
raise PddCollectError("PDD_CANCELLED", "采集任务已安全取消")
|
||||||
|
|
||||||
|
return CancellableCollector()
|
||||||
|
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(
|
||||||
|
page,
|
||||||
|
self.repository,
|
||||||
|
claim_gateway=RecordingClaimGateway(),
|
||||||
|
settings_repository=self._saved_settings(),
|
||||||
|
collect_service_factory=cancellable_factory,
|
||||||
|
)
|
||||||
|
with patch("src.pdd_ui_event.MessageBox") as message_box:
|
||||||
|
message_box.return_value.exec.return_value = True
|
||||||
|
page.rerunRequested.emit("PDD-001")
|
||||||
|
self.assertTrue(started.wait(1.0), "重新采集没有启动")
|
||||||
|
self.assertTrue(
|
||||||
|
wait_until(
|
||||||
|
self.app,
|
||||||
|
lambda: page.rerunButton.text() == "停止重新采集",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(page.rerunButton.isEnabled())
|
||||||
|
page.rerunButton.click()
|
||||||
|
self.app.processEvents()
|
||||||
|
self.assertEqual(page.rerunButton.text(), "正在停止…")
|
||||||
|
self.assertFalse(page.rerunButton.isEnabled())
|
||||||
|
self.assertIn("等待手机当前操作结束", page.statusLabel.text())
|
||||||
|
self.assertTrue(
|
||||||
|
wait_until(self.app, lambda: not events._claim_busy),
|
||||||
|
"取消后工作线程没有按时结束",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(page.rerunButton.text(), "重新执行")
|
||||||
|
self.assertIn("停止请求已处理", page.statusLabel.text())
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_rerun_feedback_has_close_button_and_replaces_previous(self):
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
|
|
||||||
|
events._show_rerun_warning("暂时不能重新执行", "测试警告")
|
||||||
|
first = events._rerun_feedback
|
||||||
|
self.assertIsNotNone(first)
|
||||||
|
events._show_rerun_error("重新采集失败", "测试错误")
|
||||||
|
second = events._rerun_feedback
|
||||||
|
self.assertIsNot(first, second)
|
||||||
|
|
||||||
|
close_buttons = [
|
||||||
|
button
|
||||||
|
for button in second.findChildren(type(page.rerunButton))
|
||||||
|
if button.text() == "关闭提示"
|
||||||
|
]
|
||||||
|
self.assertEqual(len(close_buttons), 1)
|
||||||
|
close_buttons[0].click()
|
||||||
|
self.app.processEvents()
|
||||||
|
self.assertIsNone(events._rerun_feedback)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_late_rerun_success_does_not_override_cancel_status(self):
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(page, self.repository)
|
||||||
|
events._rerun_cancel_requested = True
|
||||||
|
|
||||||
|
events._on_rerun_outcome("succeeded", "采集成功", "PDD-001")
|
||||||
|
|
||||||
|
self.assertNotIn("采集成功", page.statusLabel.text())
|
||||||
|
self.assertIn("停止请求已处理", page.statusLabel.text())
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_query_failure_shows_readable_error(self):
|
def test_query_failure_shows_readable_error(self):
|
||||||
page = PDDTaskPage()
|
page = PDDTaskPage()
|
||||||
events = PDDTaskPageEvent(page, BrokenRepository())
|
events = PDDTaskPageEvent(page, BrokenRepository())
|
||||||
|
|||||||
@@ -82,10 +82,12 @@
|
|||||||
|
|
||||||
- “刷新”只重新读取本地任务列表,不请求 Admin,也不操作手机。
|
- “刷新”只重新读取本地任务列表,不请求 Admin,也不操作手机。
|
||||||
- “重新执行”位于刷新右侧;没有选中任务时禁用。
|
- “重新执行”位于刷新右侧;没有选中任务时禁用。
|
||||||
- 点击后先校验任务,再显示明确的“重新采集”确认弹窗。弹窗显示任务编号、商品标题,并说明新结果会覆盖 Client 和 Admin 的当前采集数据。
|
- 点击后先校验任务,再显示明确的“重新采集”确认弹窗。弹窗显示任务编号、商品标题,并说明新结果会覆盖 Client 和 Admin 的当前采集数据。“暂不重新采集”是默认聚焦的安全操作,点击该按钮、按 `Escape` 或关闭弹窗都不得启动任务。
|
||||||
- 允许重新采集已经结束或处于“等待重试”的采集任务。采购、执行中、结果待提交、仍有未发送 Outbox 或自动获取忙碌时必须阻止,并用中文说明原因。
|
- 允许重新采集已经结束或处于“等待重试”的采集任务。采购、执行中、结果待提交、仍有未发送 Outbox 或自动获取忙碌时必须阻止,并用中文说明原因。
|
||||||
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
||||||
- 重新采集在工作线程运行。执行期间禁用“获取任务”和“重新执行”,完成后刷新列表。
|
- 重新采集在工作线程运行。执行期间禁用“获取任务”,“重新执行”变为可点击的“停止重新采集”。
|
||||||
|
- 点击停止后按钮显示“正在停止…”并禁用重复点击;底部明确说明正在等待手机当前操作结束。uiautomator2/ADB 的单次调用返回后,采集在下一个安全检查点停止;不得强制结束工作线程。
|
||||||
|
- 重新采集的警告和错误 `InfoBar` 必须有可见的“关闭提示”按钮;同一时间只保留一条,新提示替换旧提示。
|
||||||
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
||||||
- 采购任务始终不能通过“重新执行”入口启动;需要处理时由自动获取的安全恢复流程决定。
|
- 采购任务始终不能通过“重新执行”入口启动;需要处理时由自动获取的安全恢复流程决定。
|
||||||
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
||||||
|
|||||||
Reference in New Issue
Block a user