feat: 批量重跑实时刷新任务状态 (#143)

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