feat: 批量重跑实时刷新任务状态 (#143)
This commit is contained in:
@@ -48,6 +48,7 @@ class CollectTaskService:
|
|||||||
device_address: str,
|
device_address: str,
|
||||||
*,
|
*,
|
||||||
cancelled: Callable[[], bool] = lambda: False,
|
cancelled: Callable[[], bool] = lambda: False,
|
||||||
|
started: Callable[[str], None] = lambda _task_id: None,
|
||||||
collect_service_factory: Optional[CollectServiceFactory] = None,
|
collect_service_factory: Optional[CollectServiceFactory] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._gateway = gateway
|
self._gateway = gateway
|
||||||
@@ -55,6 +56,7 @@ class CollectTaskService:
|
|||||||
self._client = client
|
self._client = client
|
||||||
self._device_address = device_address
|
self._device_address = device_address
|
||||||
self._cancelled = cancelled
|
self._cancelled = cancelled
|
||||||
|
self._started = started
|
||||||
self._factory = collect_service_factory or self._default_factory
|
self._factory = collect_service_factory or self._default_factory
|
||||||
|
|
||||||
def execute_one(self) -> CollectTaskOutcome:
|
def execute_one(self) -> CollectTaskOutcome:
|
||||||
@@ -132,6 +134,7 @@ class CollectTaskService:
|
|||||||
started = self._repository.start_collect_run(
|
started = self._repository.start_collect_run(
|
||||||
remote_task_id, self._device_address
|
remote_task_id, self._device_address
|
||||||
)
|
)
|
||||||
|
self._started(remote_task_id)
|
||||||
if self._cancelled():
|
if self._cancelled():
|
||||||
event = self._repository.save_collect_failure(
|
event = self._repository.save_collect_failure(
|
||||||
remote_task_id,
|
remote_task_id,
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class ClaimTaskWorker(QObject):
|
|||||||
retryableFailed = pyqtSignal(str)
|
retryableFailed = pyqtSignal(str)
|
||||||
failed = pyqtSignal(str)
|
failed = pyqtSignal(str)
|
||||||
deviceUnavailable = pyqtSignal(str)
|
deviceUnavailable = pyqtSignal(str)
|
||||||
|
taskStarted = pyqtSignal(str, str, int, int)
|
||||||
outcome = pyqtSignal(str, str, str)
|
outcome = pyqtSignal(str, str, str)
|
||||||
completed = pyqtSignal()
|
completed = pyqtSignal()
|
||||||
shutdownCompleted = pyqtSignal()
|
shutdownCompleted = pyqtSignal()
|
||||||
@@ -309,11 +310,23 @@ class ClaimTaskWorker(QObject):
|
|||||||
client,
|
client,
|
||||||
android_serial,
|
android_serial,
|
||||||
cancelled=lambda: self._cancelled,
|
cancelled=lambda: self._cancelled,
|
||||||
|
started=lambda started_id, current=index + 1: self.taskStarted.emit(
|
||||||
|
started_id,
|
||||||
|
command.target_type.value,
|
||||||
|
current,
|
||||||
|
len(command.task_ids),
|
||||||
|
),
|
||||||
collect_service_factory=self._collect_service_factory,
|
collect_service_factory=self._collect_service_factory,
|
||||||
)
|
)
|
||||||
result = service.execute_selected(task_id)
|
result = service.execute_selected(task_id)
|
||||||
else:
|
else:
|
||||||
result = self._execute_purchase_rerun(task_id, client, android_serial)
|
result = self._execute_purchase_rerun(
|
||||||
|
task_id,
|
||||||
|
client,
|
||||||
|
android_serial,
|
||||||
|
current=index + 1,
|
||||||
|
total=len(command.task_ids),
|
||||||
|
)
|
||||||
except (CollectRerunError, PurchaseRerunError, ValueError) as exc:
|
except (CollectRerunError, PurchaseRerunError, ValueError) as exc:
|
||||||
self.outcome.emit("skipped", str(exc), task_id)
|
self.outcome.emit("skipped", str(exc), task_id)
|
||||||
continue
|
continue
|
||||||
@@ -328,7 +341,12 @@ class ClaimTaskWorker(QObject):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def _execute_purchase_rerun(
|
def _execute_purchase_rerun(
|
||||||
self, task_id: str, client: ClientInfo, android_serial: str
|
self,
|
||||||
|
task_id: str,
|
||||||
|
client: ClientInfo,
|
||||||
|
android_serial: str,
|
||||||
|
current: int = 1,
|
||||||
|
total: int = 1,
|
||||||
):
|
):
|
||||||
"""执行一次安全采购,并在真实下单后完成核单和 Admin 上报。"""
|
"""执行一次安全采购,并在真实下单后完成核单和 Admin 上报。"""
|
||||||
|
|
||||||
@@ -347,6 +365,12 @@ class ClaimTaskWorker(QObject):
|
|||||||
android_serial,
|
android_serial,
|
||||||
factory,
|
factory,
|
||||||
cancelled=lambda: self._cancelled,
|
cancelled=lambda: self._cancelled,
|
||||||
|
started=lambda started_id: self.taskStarted.emit(
|
||||||
|
started_id,
|
||||||
|
TaskType.PURCHASE.value,
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
result = service.execute_selected(task_id)
|
result = service.execute_selected(task_id)
|
||||||
if result.kind != "reconcile_pending":
|
if result.kind != "reconcile_pending":
|
||||||
@@ -976,8 +1000,8 @@ class PDDTaskPageEvent(QObject):
|
|||||||
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(
|
self._page.set_engine_status(
|
||||||
f"正在检查 Android 设备并准备重新{'采集' if command.target_type is TaskType.COLLECT else '采购'}"
|
f"已加入队列 {len(command.task_ids)} 条,正在检查 Android 设备,等待开始重新"
|
||||||
f" {len(command.task_ids)} 条任务…"
|
f"{'采集' if command.target_type is TaskType.COLLECT else '采购'}…"
|
||||||
)
|
)
|
||||||
self._reload()
|
self._reload()
|
||||||
|
|
||||||
@@ -1025,6 +1049,24 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._rerun_failed += 1
|
self._rerun_failed += 1
|
||||||
self._page.set_engine_status(message)
|
self._page.set_engine_status(message)
|
||||||
|
|
||||||
|
@pyqtSlot(str, str, int, int)
|
||||||
|
def _on_rerun_task_started(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
task_type: str,
|
||||||
|
current: int,
|
||||||
|
total: int,
|
||||||
|
) -> None:
|
||||||
|
"""数据库进入 running 后,立即让主线程刷新当前任务。"""
|
||||||
|
|
||||||
|
if self._closing or self._claim_operation != "rerun":
|
||||||
|
return
|
||||||
|
action = "采集" if task_type == TaskType.COLLECT.value else "采购"
|
||||||
|
self._reload()
|
||||||
|
self._page.set_engine_status(
|
||||||
|
f"批量重新{action}:正在{action} {task_id}({current}/{total})"
|
||||||
|
)
|
||||||
|
|
||||||
@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:
|
||||||
@@ -1203,6 +1245,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
worker.retryableFailed.connect(self._route_claim_retryable_failed)
|
worker.retryableFailed.connect(self._route_claim_retryable_failed)
|
||||||
worker.failed.connect(self._route_claim_failed)
|
worker.failed.connect(self._route_claim_failed)
|
||||||
worker.deviceUnavailable.connect(self._route_device_unavailable)
|
worker.deviceUnavailable.connect(self._route_device_unavailable)
|
||||||
|
worker.taskStarted.connect(self._on_rerun_task_started)
|
||||||
worker.outcome.connect(self._route_claim_outcome)
|
worker.outcome.connect(self._route_claim_outcome)
|
||||||
worker.completed.connect(self._on_claim_cycle_completed)
|
worker.completed.connect(self._on_claim_cycle_completed)
|
||||||
# QThread 对象属于主线程;显式直连可在 shutdown_worker 完成释放后
|
# QThread 对象属于主线程;显式直连可在 shutdown_worker 完成释放后
|
||||||
@@ -1704,6 +1747,10 @@ class PDDTaskPageEvent(QObject):
|
|||||||
def summary_to_row(summary: TaskSummary) -> TaskRow:
|
def summary_to_row(summary: TaskSummary) -> TaskRow:
|
||||||
"""把领域摘要转换成只供表格显示的轻量行。"""
|
"""把领域摘要转换成只供表格显示的轻量行。"""
|
||||||
|
|
||||||
|
status_text = TASK_STATUS_TEXT[summary.status]
|
||||||
|
if summary.status is TaskStatus.RUNNING:
|
||||||
|
status_text = "采集中" if summary.task_type is TaskType.COLLECT else "采购中"
|
||||||
|
|
||||||
return TaskRow(
|
return TaskRow(
|
||||||
remote_task_id=summary.remote_task_id,
|
remote_task_id=summary.remote_task_id,
|
||||||
task_type=TASK_TYPE_TEXT[summary.task_type],
|
task_type=TASK_TYPE_TEXT[summary.task_type],
|
||||||
@@ -1714,7 +1761,7 @@ def summary_to_row(summary: TaskSummary) -> TaskRow:
|
|||||||
size=summary.target_size or "",
|
size=summary.target_size or "",
|
||||||
price_cents=summary.price_cent,
|
price_cents=summary.price_cent,
|
||||||
quantity=summary.quantity,
|
quantity=summary.quantity,
|
||||||
status=TASK_STATUS_TEXT[summary.status],
|
status=status_text,
|
||||||
latest_run_status=(
|
latest_run_status=(
|
||||||
summary.latest_run_status.value if summary.latest_run_status else ""
|
summary.latest_run_status.value if summary.latest_run_status else ""
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class PurchaseTaskService:
|
|||||||
adapter_factory: PurchaseAdapterFactory,
|
adapter_factory: PurchaseAdapterFactory,
|
||||||
*,
|
*,
|
||||||
cancelled: Callable[[], bool] = lambda: False,
|
cancelled: Callable[[], bool] = lambda: False,
|
||||||
|
started: Callable[[str], None] = lambda _task_id: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._gateway = gateway
|
self._gateway = gateway
|
||||||
self._repository = repository
|
self._repository = repository
|
||||||
@@ -76,6 +77,7 @@ class PurchaseTaskService:
|
|||||||
self._device_address = str(device_address or "").strip()
|
self._device_address = str(device_address or "").strip()
|
||||||
self._factory = adapter_factory
|
self._factory = adapter_factory
|
||||||
self._cancelled = cancelled
|
self._cancelled = cancelled
|
||||||
|
self._started = started
|
||||||
|
|
||||||
def execute_one_local(self) -> PurchaseTaskOutcome:
|
def execute_one_local(self) -> PurchaseTaskOutcome:
|
||||||
"""先补交结果,再执行最早的本地待执行采购任务。"""
|
"""先补交结果,再执行最早的本地待执行采购任务。"""
|
||||||
@@ -103,6 +105,7 @@ class PurchaseTaskService:
|
|||||||
started = self._repository.start_purchase_run(
|
started = self._repository.start_purchase_run(
|
||||||
remote_task_id, self._device_address
|
remote_task_id, self._device_address
|
||||||
)
|
)
|
||||||
|
self._started(remote_task_id)
|
||||||
adapter: PddPurchaseAdapter | None = None
|
adapter: PddPurchaseAdapter | None = None
|
||||||
step = "purchase_prepare"
|
step = "purchase_prepare"
|
||||||
business_failure_message = ""
|
business_failure_message = ""
|
||||||
|
|||||||
@@ -207,6 +207,30 @@ class CollectTaskServiceTest(unittest.TestCase):
|
|||||||
self.repository.get_task("COL-OTHER").status, TaskStatus.CLAIMED
|
self.repository.get_task("COL-OTHER").status, TaskStatus.CLAIMED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_started_callback_runs_after_database_enters_running(self):
|
||||||
|
self.repository.add_claimed_task(
|
||||||
|
NewClaimedTask(
|
||||||
|
remote_task_id="COL-STARTED",
|
||||||
|
task_type=TaskType.COLLECT,
|
||||||
|
goods_url="https://example.test/COL-STARTED",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
observed = []
|
||||||
|
service = CollectTaskService(
|
||||||
|
self.gateway,
|
||||||
|
self.repository,
|
||||||
|
self.client,
|
||||||
|
"USB-001",
|
||||||
|
started=lambda task_id: observed.append(
|
||||||
|
(task_id, self.repository.get_task(task_id).status)
|
||||||
|
),
|
||||||
|
collect_service_factory=lambda *_args: FakeCollector([]),
|
||||||
|
)
|
||||||
|
|
||||||
|
service.execute_selected("COL-STARTED")
|
||||||
|
|
||||||
|
self.assertEqual(observed, [("COL-STARTED", TaskStatus.RUNNING)])
|
||||||
|
|
||||||
def test_selected_task_cancelled_before_collector_starts_is_persisted(self):
|
def test_selected_task_cancelled_before_collector_starts_is_persisted(self):
|
||||||
self.repository.add_claimed_task(
|
self.repository.add_claimed_task(
|
||||||
NewClaimedTask(
|
NewClaimedTask(
|
||||||
|
|||||||
@@ -1094,6 +1094,37 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
self.assertEqual(row.goods_id, "")
|
self.assertEqual(row.goods_id, "")
|
||||||
self.assertFalse(hasattr(row, "pdd_data"))
|
self.assertFalse(hasattr(row, "pdd_data"))
|
||||||
|
|
||||||
|
def test_summary_to_row_uses_task_specific_running_text(self):
|
||||||
|
common = dict(
|
||||||
|
id=1,
|
||||||
|
goods_id=None,
|
||||||
|
title=None,
|
||||||
|
target_color=None,
|
||||||
|
target_size=None,
|
||||||
|
price_cent=None,
|
||||||
|
quantity=None,
|
||||||
|
status=TaskStatus.RUNNING,
|
||||||
|
updated_at="2026-08-11T08:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
collect = summary_to_row(
|
||||||
|
TaskSummary(
|
||||||
|
remote_task_id="COL-RUNNING",
|
||||||
|
task_type=TaskType.COLLECT,
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
purchase = summary_to_row(
|
||||||
|
TaskSummary(
|
||||||
|
remote_task_id="PUR-RUNNING",
|
||||||
|
task_type=TaskType.PURCHASE,
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(collect.status, "采集中")
|
||||||
|
self.assertEqual(purchase.status, "采购中")
|
||||||
|
|
||||||
def test_main_window_keeps_event_object_alive(self):
|
def test_main_window_keeps_event_object_alive(self):
|
||||||
window = MainWindow(
|
window = MainWindow(
|
||||||
task_repository=self.repository,
|
task_repository=self.repository,
|
||||||
|
|||||||
@@ -217,6 +217,24 @@ class PurchaseTaskServiceTest(unittest.TestCase):
|
|||||||
self.assertFalse(purchase["order_submitted"])
|
self.assertFalse(purchase["order_submitted"])
|
||||||
self.assertFalse(purchase["payment_attempted"])
|
self.assertFalse(purchase["payment_attempted"])
|
||||||
|
|
||||||
|
def test_started_callback_runs_after_database_enters_running(self):
|
||||||
|
self._prepare_task(task_id="PUR-STARTED")
|
||||||
|
observed = []
|
||||||
|
service = PurchaseTaskService(
|
||||||
|
self.gateway,
|
||||||
|
self.repository,
|
||||||
|
self.client,
|
||||||
|
"USB-001",
|
||||||
|
lambda _address, _cancelled: RecordingDryRunAdapter(),
|
||||||
|
started=lambda task_id: observed.append(
|
||||||
|
(task_id, self.repository.get_task(task_id).status)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
service.execute_selected("PUR-STARTED")
|
||||||
|
|
||||||
|
self.assertEqual(observed, [("PUR-STARTED", TaskStatus.RUNNING)])
|
||||||
|
|
||||||
def test_price_above_limit_stops_before_confirmation(self):
|
def test_price_above_limit_stops_before_confirmation(self):
|
||||||
self._prepare_task()
|
self._prepare_task()
|
||||||
adapter = RecordingDryRunAdapter(price_cent=5001)
|
adapter = RecordingDryRunAdapter(price_cent=5001)
|
||||||
|
|||||||
@@ -102,6 +102,9 @@
|
|||||||
- 重新采集的前置校验警告和后台错误 `InfoBar` 都显示在软件窗口顶部水平居中,并有可见的“关闭提示”按钮;提示 5 秒后自动关闭,同一时间只保留一条,新提示替换旧提示。
|
- 重新采集的前置校验警告和后台错误 `InfoBar` 都显示在软件窗口顶部水平居中,并有可见的“关闭提示”按钮;提示 5 秒后自动关闭,同一时间只保留一条,新提示替换旧提示。
|
||||||
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
||||||
- 重新采购逐条串行执行。任一任务未形成“执行、核单、上报”成功闭环时立即停止剩余队列。历史上任何一次执行已有 `irreversible_action_at` 的任务永久禁止重新采购,只能只读核单。
|
- 重新采购逐条串行执行。任一任务未形成“执行、核单、上报”成功闭环时立即停止剩余队列。历史上任何一次执行已有 `irreversible_action_at` 的任务永久禁止重新采购,只能只读核单。
|
||||||
|
- 用户确认批量操作后立即刷新列表,并在状态区显示已加入队列的数量。排队中的任务保持原状态;
|
||||||
|
只有工作线程已经把当前任务写成 `running` 后,才通知主线程刷新该行并显示“采集中”或
|
||||||
|
“采购中”。每条完成、失败、跳过或取消后再次刷新,再开始下一条。
|
||||||
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
||||||
“重试已暂停”,并提示选择任务点击“重新执行”或重新启动获取任务。
|
“重试已暂停”,并提示选择任务点击“重新执行”或重新启动获取任务。
|
||||||
- “重新上报”作用于当前已经加载并勾选的任务。执行前显示任务数量,并明确说明不会重新采集、采购或操作手机。
|
- “重新上报”作用于当前已经加载并勾选的任务。执行前显示任务数量,并明确说明不会重新采集、采购或操作手机。
|
||||||
|
|||||||
Reference in New Issue
Block a user