+37
-16
@@ -390,7 +390,8 @@ class PDDTaskPage(QWidget):
|
||||
autoFetchRequested = pyqtSignal()
|
||||
searchRequested = pyqtSignal(dict)
|
||||
refreshRequested = pyqtSignal()
|
||||
rerunRequested = pyqtSignal(str)
|
||||
rerunRequested = pyqtSignal(object)
|
||||
purchaseRerunRequested = pyqtSignal(object)
|
||||
rerunCancelRequested = pyqtSignal()
|
||||
resubmitRequested = pyqtSignal(object)
|
||||
removeRequested = pyqtSignal(object)
|
||||
@@ -460,10 +461,14 @@ class PDDTaskPage(QWidget):
|
||||
self.invertSelectionButton.setToolTip("反选当前已加载的任务")
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
|
||||
self.rerunButton = PushButton(FIF.UPDATE, "重新执行", self)
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.rerunButton = PushButton(FIF.UPDATE, "重新采集", self)
|
||||
self.rerunButton.setAccessibleName("重新采集勾选的采集任务")
|
||||
self.rerunButton.setEnabled(False)
|
||||
|
||||
self.purchaseRerunButton = PushButton(FIF.SHOPPING_CART, "重新采购", self)
|
||||
self.purchaseRerunButton.setAccessibleName("重新采购勾选的采购任务")
|
||||
self.purchaseRerunButton.setEnabled(False)
|
||||
|
||||
self.resubmitButton = PushButton(FIF.SEND, "重新上报", self)
|
||||
self.resubmitButton.setAccessibleName("重新上报勾选任务的已有结果")
|
||||
self.resubmitButton.setEnabled(False)
|
||||
@@ -489,7 +494,7 @@ class PDDTaskPage(QWidget):
|
||||
if compact == self._compact_commands and self.commandLayout.count():
|
||||
return
|
||||
self._compact_commands = compact
|
||||
for column in range(16):
|
||||
for column in range(17):
|
||||
self.commandLayout.setColumnStretch(column, 0)
|
||||
|
||||
if compact:
|
||||
@@ -506,9 +511,10 @@ class PDDTaskPage(QWidget):
|
||||
(self.selectAllButton, 2, 5, 1, 1),
|
||||
(self.invertSelectionButton, 2, 6, 1, 1),
|
||||
(self.checkedCountLabel, 2, 7, 1, 1),
|
||||
(self.rerunButton, 2, 8, 1, 1),
|
||||
(self.resubmitButton, 2, 9, 1, 1),
|
||||
(self.removeButton, 2, 10, 1, 1),
|
||||
(self.resubmitButton, 2, 8, 1, 1),
|
||||
(self.rerunButton, 2, 9, 1, 1),
|
||||
(self.purchaseRerunButton, 2, 10, 1, 1),
|
||||
(self.removeButton, 2, 11, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(4, 1)
|
||||
else:
|
||||
@@ -525,9 +531,10 @@ class PDDTaskPage(QWidget):
|
||||
(self.selectAllButton, 0, 10, 1, 1),
|
||||
(self.invertSelectionButton, 0, 11, 1, 1),
|
||||
(self.checkedCountLabel, 0, 12, 1, 1),
|
||||
(self.rerunButton, 0, 13, 1, 1),
|
||||
(self.resubmitButton, 0, 14, 1, 1),
|
||||
(self.removeButton, 0, 15, 1, 1),
|
||||
(self.resubmitButton, 0, 13, 1, 1),
|
||||
(self.rerunButton, 0, 14, 1, 1),
|
||||
(self.purchaseRerunButton, 0, 15, 1, 1),
|
||||
(self.removeButton, 0, 16, 1, 1),
|
||||
)
|
||||
self.commandLayout.setColumnStretch(6, 4)
|
||||
self.commandLayout.setColumnStretch(9, 1)
|
||||
@@ -619,6 +626,7 @@ class PDDTaskPage(QWidget):
|
||||
self.taskModel.invert_loaded_task_checks
|
||||
)
|
||||
self.rerunButton.clicked.connect(self._request_rerun)
|
||||
self.purchaseRerunButton.clicked.connect(self._request_purchase_rerun)
|
||||
self.resubmitButton.clicked.connect(self._request_resubmit)
|
||||
self.removeButton.clicked.connect(self._request_remove)
|
||||
self.clearFiltersButton.clicked.connect(self.clear_filters)
|
||||
@@ -636,7 +644,7 @@ class PDDTaskPage(QWidget):
|
||||
def resizeEvent(self, event) -> None:
|
||||
"""按内容区宽度切换顶部命令区的单行或双行布局。"""
|
||||
|
||||
self._update_command_layout(compact=event.size().width() < 1460)
|
||||
self._update_command_layout(compact=event.size().width() < 1580)
|
||||
super().resizeEvent(event)
|
||||
|
||||
def set_tasks(self, tasks: Iterable[TaskRow]) -> None:
|
||||
@@ -750,8 +758,15 @@ class PDDTaskPage(QWidget):
|
||||
if self._rerun_state != "idle":
|
||||
return
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if len(task_ids) == 1:
|
||||
self.rerunRequested.emit(task_ids[0])
|
||||
if task_ids:
|
||||
self.rerunRequested.emit(task_ids)
|
||||
|
||||
def _request_purchase_rerun(self) -> None:
|
||||
if self._rerun_state != "idle":
|
||||
return
|
||||
task_ids = self.taskModel.checked_task_ids()
|
||||
if task_ids:
|
||||
self.purchaseRerunRequested.emit(task_ids)
|
||||
|
||||
def _request_resubmit(self) -> None:
|
||||
if self._resubmit_running or self._remove_running:
|
||||
@@ -773,6 +788,7 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setAccessibleName("停止当前重新采集")
|
||||
self.rerunButton.setEnabled(True)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.purchaseRerunButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.selectAllButton.setEnabled(False)
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
@@ -783,21 +799,26 @@ class PDDTaskPage(QWidget):
|
||||
self.rerunButton.setAccessibleName("正在停止重新采集")
|
||||
self.rerunButton.setEnabled(False)
|
||||
self.resubmitButton.setEnabled(False)
|
||||
self.purchaseRerunButton.setEnabled(False)
|
||||
self.removeButton.setEnabled(False)
|
||||
self.selectAllButton.setEnabled(False)
|
||||
self.invertSelectionButton.setEnabled(False)
|
||||
self.autoFetchButton.setEnabled(False)
|
||||
return
|
||||
|
||||
self.rerunButton.setText("重新执行")
|
||||
self.rerunButton.setAccessibleName("重新执行当前选中的采集任务")
|
||||
self.rerunButton.setText("重新采集")
|
||||
self.rerunButton.setAccessibleName("重新采集勾选的采集任务")
|
||||
self.purchaseRerunButton.setText("重新采购")
|
||||
idle = not self._resubmit_running and not self._remove_running
|
||||
has_tasks = self.taskModel.data_row_count() > 0
|
||||
self.autoFetchButton.setEnabled(idle)
|
||||
self.selectAllButton.setEnabled(idle and has_tasks)
|
||||
self.invertSelectionButton.setEnabled(idle and has_tasks)
|
||||
self.rerunButton.setEnabled(
|
||||
idle and len(self.taskModel.checked_task_ids()) == 1
|
||||
idle and bool(self.taskModel.checked_task_ids())
|
||||
)
|
||||
self.purchaseRerunButton.setEnabled(
|
||||
idle and bool(self.taskModel.checked_task_ids())
|
||||
)
|
||||
self.resubmitButton.setEnabled(
|
||||
idle and bool(self.taskModel.checked_task_ids())
|
||||
|
||||
+171
-25
@@ -50,21 +50,29 @@ from .pdd_device_service import (
|
||||
PersistentPddDeviceService,
|
||||
bind_thread_device_service,
|
||||
)
|
||||
from .purchase_task_service import PurchaseAdapterFactory
|
||||
from .purchase_task_service import LivePurchaseAdapterFactory
|
||||
from .purchase_reconcile_service import PurchaseReconcileFactory
|
||||
from .purchase_task_service import (
|
||||
LivePurchaseAdapterFactory,
|
||||
PurchaseAdapterFactory,
|
||||
PurchaseTaskService,
|
||||
)
|
||||
from .purchase_reconcile_service import (
|
||||
PurchaseReconcileFactory,
|
||||
PurchaseReconcileService,
|
||||
)
|
||||
from .selected_android_device_service import SelectedAndroidDeviceService
|
||||
from .settings_repository import SettingsRepository
|
||||
from .task_models import (
|
||||
OutboxEventType,
|
||||
OutboxStatus,
|
||||
TaskFilters,
|
||||
TaskRerunBatch,
|
||||
TaskStatus,
|
||||
TaskSummary,
|
||||
TaskType,
|
||||
)
|
||||
from .task_repository import (
|
||||
CollectRerunError,
|
||||
PurchaseRerunError,
|
||||
TaskRemovalError,
|
||||
TaskRepository,
|
||||
)
|
||||
@@ -185,8 +193,8 @@ class ClaimTaskWorker(QObject):
|
||||
|
||||
self.run_selected(self._selected_task_id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def run_selected(self, selected_task_id: str = "") -> None:
|
||||
@pyqtSlot(object)
|
||||
def run_selected(self, command="") -> None:
|
||||
"""执行一条命令;该槽始终由固定设备 QThread 串行调用。"""
|
||||
|
||||
if not self._accepting:
|
||||
@@ -196,7 +204,7 @@ class ClaimTaskWorker(QObject):
|
||||
assert self._idle_timer is not None
|
||||
self._idle_timer.stop()
|
||||
try:
|
||||
if self._cancelled and not selected_task_id:
|
||||
if self._cancelled and not command:
|
||||
return
|
||||
with bind_thread_device_service(self._device_service):
|
||||
client_settings = self._client_service.load()
|
||||
@@ -209,6 +217,13 @@ class ClaimTaskWorker(QObject):
|
||||
client_settings.client_id,
|
||||
client_settings.client_name,
|
||||
)
|
||||
if isinstance(command, TaskRerunBatch):
|
||||
self._device_connection_checker(android_serial or "")
|
||||
self._execute_rerun_batch(
|
||||
command, client, android_serial or ""
|
||||
)
|
||||
return
|
||||
selected_task_id = str(command or "")
|
||||
if selected_task_id:
|
||||
self._device_connection_checker(android_serial or "")
|
||||
self._task_repository.prepare_collect_rerun(
|
||||
@@ -276,6 +291,90 @@ class ClaimTaskWorker(QObject):
|
||||
)
|
||||
self.completed.emit()
|
||||
|
||||
def _execute_rerun_batch(
|
||||
self, command: TaskRerunBatch, client: ClientInfo, android_serial: str
|
||||
) -> None:
|
||||
"""在同一设备线程中逐条执行;采购不确定时停止剩余队列。"""
|
||||
|
||||
for index, task_id in enumerate(command.task_ids):
|
||||
if self._cancelled:
|
||||
self.outcome.emit("cancelled", f"任务 {task_id} 尚未开始", task_id)
|
||||
continue
|
||||
try:
|
||||
if command.target_type is TaskType.COLLECT:
|
||||
self._task_repository.prepare_collect_rerun(task_id)
|
||||
service = CollectTaskService(
|
||||
self._gateway,
|
||||
self._task_repository,
|
||||
client,
|
||||
android_serial,
|
||||
cancelled=lambda: self._cancelled,
|
||||
collect_service_factory=self._collect_service_factory,
|
||||
)
|
||||
result = service.execute_selected(task_id)
|
||||
else:
|
||||
result = self._execute_purchase_rerun(task_id, client, android_serial)
|
||||
except (CollectRerunError, PurchaseRerunError, ValueError) as exc:
|
||||
self.outcome.emit("skipped", str(exc), task_id)
|
||||
continue
|
||||
self.outcome.emit(result.kind, result.message, result.task_id or task_id)
|
||||
if command.target_type is TaskType.PURCHASE and result.kind != "succeeded":
|
||||
for remaining_id in command.task_ids[index + 1 :]:
|
||||
self.outcome.emit(
|
||||
"skipped",
|
||||
f"前一采购任务未形成成功闭环,任务 {remaining_id} 未开始",
|
||||
remaining_id,
|
||||
)
|
||||
break
|
||||
|
||||
def _execute_purchase_rerun(
|
||||
self, task_id: str, client: ClientInfo, android_serial: str
|
||||
):
|
||||
"""执行一次安全采购,并在真实下单后完成核单和 Admin 上报。"""
|
||||
|
||||
task = self._task_repository.prepare_purchase_rerun(task_id)
|
||||
factory = (
|
||||
self._live_purchase_adapter_factory
|
||||
if task.execution_mode == "live"
|
||||
else self._purchase_adapter_factory
|
||||
)
|
||||
if factory is None:
|
||||
raise ValueError("采购执行器未就绪")
|
||||
service = PurchaseTaskService(
|
||||
self._gateway,
|
||||
self._task_repository,
|
||||
client,
|
||||
android_serial,
|
||||
factory,
|
||||
cancelled=lambda: self._cancelled,
|
||||
)
|
||||
result = service.execute_selected(task_id)
|
||||
if result.kind != "reconcile_pending":
|
||||
return result
|
||||
|
||||
# 已进入不可逆阶段后,即使用户停止,也必须完成当前任务的只读核单。
|
||||
remaining_ms = PURCHASE_RECONCILE_DELAY_MS
|
||||
while remaining_ms > 0:
|
||||
step_ms = min(250, remaining_ms)
|
||||
QThread.msleep(step_ms)
|
||||
remaining_ms -= step_ms
|
||||
if self._purchase_reconcile_factory is None:
|
||||
return type(result)(
|
||||
"manual_review", "采购核单执行器未就绪;绝不重新下单", task_id
|
||||
)
|
||||
reconcile = PurchaseReconcileService(
|
||||
self._task_repository,
|
||||
android_serial,
|
||||
self._purchase_reconcile_factory,
|
||||
cancelled=lambda: False,
|
||||
).execute_selected(task_id)
|
||||
if reconcile.kind != "result_pending":
|
||||
return type(result)(reconcile.kind, reconcile.message, task_id)
|
||||
event = self._task_repository.outbox_for_resubmit(task_id)
|
||||
if event is None:
|
||||
return type(result)("manual_review", "核单结果未写入待上报队列", task_id)
|
||||
return service.submit_saved_event(event)
|
||||
|
||||
@pyqtSlot()
|
||||
def release_device(self) -> None:
|
||||
"""停止、换设备或配置变化后,在设备线程安全释放缓存。"""
|
||||
@@ -419,7 +518,7 @@ class TaskRemoveWorker(QObject):
|
||||
class PDDTaskPageEvent(QObject):
|
||||
"""把 PDD 页面只读操作连接到本地任务 Repository。"""
|
||||
|
||||
_claimRunRequested = pyqtSignal(str)
|
||||
_claimRunRequested = pyqtSignal(object)
|
||||
_claimReleaseRequested = pyqtSignal()
|
||||
_claimShutdownRequested = pyqtSignal()
|
||||
_claimGatewayChanged = pyqtSignal(object)
|
||||
@@ -452,6 +551,11 @@ class PDDTaskPageEvent(QObject):
|
||||
self._claim_worker: Optional[ClaimTaskWorker] = None
|
||||
self._claim_operation = ""
|
||||
self._rerun_cancel_requested = False
|
||||
self._rerun_target_type = TaskType.COLLECT
|
||||
self._rerun_total = 0
|
||||
self._rerun_succeeded = 0
|
||||
self._rerun_failed = 0
|
||||
self._rerun_skipped = 0
|
||||
self._rerun_feedback: Optional[InfoBar] = None
|
||||
self._claim_feedback: Optional[InfoBar] = None
|
||||
self._device_feedback: Optional[InfoBar] = None
|
||||
@@ -517,6 +621,7 @@ class PDDTaskPageEvent(QObject):
|
||||
page.searchRequested.connect(self.search_tasks)
|
||||
page.refreshRequested.connect(self.refresh_tasks)
|
||||
page.rerunRequested.connect(self.request_rerun)
|
||||
page.purchaseRerunRequested.connect(self.request_purchase_rerun)
|
||||
page.rerunCancelRequested.connect(self.request_cancel_rerun)
|
||||
page.resubmitRequested.connect(self.request_resubmit)
|
||||
page.removeRequested.connect(self.request_remove)
|
||||
@@ -574,11 +679,23 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
self._reload()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def request_rerun(self, task_id: str) -> None:
|
||||
"""确认后在后台重新采集当前选中的一条终态任务。"""
|
||||
@pyqtSlot(object)
|
||||
def request_rerun(self, task_ids) -> None:
|
||||
"""预检并确认批量重新采集。"""
|
||||
|
||||
if self._closing or not task_id:
|
||||
self._request_rerun_batch(task_ids, TaskType.COLLECT)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def request_purchase_rerun(self, task_ids) -> None:
|
||||
"""预检并确认带不可逆门禁的批量重新采购。"""
|
||||
|
||||
self._request_rerun_batch(task_ids, TaskType.PURCHASE)
|
||||
|
||||
def _request_rerun_batch(self, task_ids, target_type: TaskType) -> None:
|
||||
values = (task_ids,) if isinstance(task_ids, str) else task_ids
|
||||
stable_ids = tuple(dict.fromkeys(str(value) for value in values if value))
|
||||
|
||||
if self._closing or not stable_ids:
|
||||
return
|
||||
if (
|
||||
self._auto_fetch_running
|
||||
@@ -616,8 +733,8 @@ class PDDTaskPageEvent(QObject):
|
||||
)
|
||||
return
|
||||
try:
|
||||
detail = self._repository.validate_collect_rerun(task_id)
|
||||
except (CollectRerunError, ValueError) as exc:
|
||||
plan = self._repository.plan_rerun_batch(stable_ids, target_type)
|
||||
except (CollectRerunError, PurchaseRerunError, ValueError) as exc:
|
||||
self._show_rerun_warning("不能重新执行", str(exc))
|
||||
return
|
||||
except Exception:
|
||||
@@ -626,20 +743,32 @@ class PDDTaskPageEvent(QObject):
|
||||
)
|
||||
return
|
||||
|
||||
title = detail.title or "尚未获取标题"
|
||||
action = "采集" if target_type is TaskType.COLLECT else "采购"
|
||||
if not plan.eligible_task_ids:
|
||||
self._show_rerun_warning(
|
||||
f"没有可重新{action}的任务",
|
||||
f"已选 {plan.selected_count} 条,过滤 {len(plan.filtered_task_ids)} 条其他类型,"
|
||||
f"安全条件阻止 {len(plan.blocked)} 条。",
|
||||
)
|
||||
return
|
||||
dialog = MessageBox(
|
||||
"确认重新采集",
|
||||
f"任务:{detail.remote_task_id}\n商品:{title}\n\n"
|
||||
"新结果会覆盖 Client 和 Admin 的当前采集数据,旧结果仍保留在执行记录中。",
|
||||
f"确认重新{action} {len(plan.eligible_task_ids)} 条任务?",
|
||||
f"已选 {plan.selected_count} 条;可执行 {len(plan.eligible_task_ids)} 条;"
|
||||
f"过滤其他类型 {len(plan.filtered_task_ids)} 条;安全条件阻止 {len(plan.blocked)} 条。\n\n"
|
||||
+ ("采购任务会逐条执行;核单不确定时立即停止剩余任务,绝不重新下单。"
|
||||
if target_type is TaskType.PURCHASE else
|
||||
"新结果会覆盖 Client 和 Admin 当前采集数据,旧执行记录仍保留。"),
|
||||
self._page.window(),
|
||||
)
|
||||
dialog.yesButton.setText("重新采集")
|
||||
dialog.cancelButton.setText("暂不重新采集")
|
||||
dialog.yesButton.setText(f"重新{action}")
|
||||
dialog.cancelButton.setText("取消")
|
||||
dialog.cancelButton.setFocus()
|
||||
if not dialog.exec():
|
||||
return
|
||||
|
||||
self._start_rerun_worker(task_id)
|
||||
self._start_rerun_worker(
|
||||
TaskRerunBatch(target_type, plan.eligible_task_ids)
|
||||
)
|
||||
|
||||
@pyqtSlot(object)
|
||||
def request_resubmit(self, task_ids) -> None:
|
||||
@@ -833,7 +962,7 @@ class PDDTaskPageEvent(QObject):
|
||||
if not self._closing:
|
||||
self._page.set_resubmit_running(False)
|
||||
|
||||
def _start_rerun_worker(self, task_id: str) -> None:
|
||||
def _start_rerun_worker(self, command: TaskRerunBatch) -> None:
|
||||
"""把指定任务提交到持久设备工作线程。"""
|
||||
|
||||
assert self._claim_gateway is not None
|
||||
@@ -841,17 +970,21 @@ class PDDTaskPageEvent(QObject):
|
||||
self._close_claim_feedback()
|
||||
self._close_device_feedback()
|
||||
self._rerun_cancel_requested = False
|
||||
self._rerun_target_type = command.target_type
|
||||
self._rerun_total = len(command.task_ids)
|
||||
self._rerun_succeeded = self._rerun_failed = self._rerun_skipped = 0
|
||||
self._claim_busy = True
|
||||
self._page.set_rerun_running(True)
|
||||
self._page.set_engine_status(
|
||||
f"正在检查 Android 设备并准备重新采集任务 {task_id}…"
|
||||
f"正在检查 Android 设备并准备重新{'采集' if command.target_type is TaskType.COLLECT else '采购'}"
|
||||
f" {len(command.task_ids)} 条任务…"
|
||||
)
|
||||
self._reload()
|
||||
|
||||
worker = self._ensure_claim_executor()
|
||||
self._claim_operation = "rerun"
|
||||
worker.prepare_run()
|
||||
self._claimRunRequested.emit(task_id)
|
||||
self._claimRunRequested.emit(command)
|
||||
|
||||
@pyqtSlot()
|
||||
def request_cancel_rerun(self) -> None:
|
||||
@@ -884,9 +1017,13 @@ class PDDTaskPageEvent(QObject):
|
||||
"停止请求已处理,请查看任务最新状态"
|
||||
)
|
||||
return
|
||||
if kind == "succeeded":
|
||||
self._rerun_succeeded += 1
|
||||
elif kind in {"skipped", "cancelled"}:
|
||||
self._rerun_skipped += 1
|
||||
else:
|
||||
self._rerun_failed += 1
|
||||
self._page.set_engine_status(message)
|
||||
if kind != "succeeded":
|
||||
self._show_rerun_error("重新采集需要处理", message)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_rerun_failed(self, message: str) -> None:
|
||||
@@ -919,6 +1056,15 @@ class PDDTaskPageEvent(QObject):
|
||||
self._page.set_engine_status(
|
||||
"停止请求已处理,请查看任务最新状态"
|
||||
)
|
||||
elif self._rerun_succeeded + self._rerun_failed + self._rerun_skipped:
|
||||
action = "采集" if self._rerun_target_type is TaskType.COLLECT else "采购"
|
||||
summary = (
|
||||
f"批量重新{action}完成:成功 {self._rerun_succeeded} 条,"
|
||||
f"失败 {self._rerun_failed} 条,跳过 {self._rerun_skipped} 条"
|
||||
)
|
||||
self._page.set_engine_status(summary)
|
||||
if self._rerun_failed or self._rerun_skipped:
|
||||
self._show_rerun_warning(f"重新{action}未全部完成", summary)
|
||||
|
||||
def _show_rerun_warning(self, title: str, content: str) -> None:
|
||||
bar = InfoBar.warning(
|
||||
|
||||
@@ -549,12 +549,24 @@ class PurchaseTaskService:
|
||||
f"任务 {task_id} 采购演练未完成,失败信息已提交 Admin",
|
||||
task_id,
|
||||
)
|
||||
purchase = event.payload_json.get("purchase")
|
||||
if isinstance(purchase, Mapping) and purchase.get("mode") == "live":
|
||||
return PurchaseTaskOutcome(
|
||||
"succeeded",
|
||||
f"任务 {task_id} 已核对订单并提交 Admin",
|
||||
task_id,
|
||||
)
|
||||
return PurchaseTaskOutcome(
|
||||
"succeeded",
|
||||
f"任务 {task_id} 演练完成并已提交 Admin;没有提交订单",
|
||||
task_id,
|
||||
)
|
||||
|
||||
def submit_saved_event(self, event: OutboxEventRecord) -> PurchaseTaskOutcome:
|
||||
"""提交核单后已经落库的 Outbox,供批量采购闭环复用。"""
|
||||
|
||||
return self._submit(event)
|
||||
|
||||
def _result_data(
|
||||
self, target: PurchaseTarget, state: PurchasePageState
|
||||
) -> dict[str, object]:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
from typing import Any, Dict, Mapping, Optional, Tuple
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
@@ -104,6 +104,25 @@ class TaskFilters:
|
||||
keyword: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskRerunPlan:
|
||||
"""批量重新执行预检结果;界面只展示计数,不自行判断安全条件。"""
|
||||
|
||||
target_type: TaskType
|
||||
selected_count: int
|
||||
eligible_task_ids: Tuple[str, ...]
|
||||
filtered_task_ids: Tuple[str, ...]
|
||||
blocked: Tuple[Tuple[str, str], ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskRerunBatch:
|
||||
"""交给固定设备线程串行处理的批量命令。"""
|
||||
|
||||
target_type: TaskType
|
||||
task_ids: Tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskSummary:
|
||||
"""任务表格使用的轻量数据,不包含完整 JSON。"""
|
||||
|
||||
@@ -20,6 +20,7 @@ from .task_models import (
|
||||
StartedTaskRun,
|
||||
TaskDetail,
|
||||
TaskFilters,
|
||||
TaskRerunPlan,
|
||||
TaskStatus,
|
||||
TaskSummary,
|
||||
TaskType,
|
||||
@@ -39,6 +40,10 @@ class CollectRerunError(ValueError):
|
||||
"""当前任务不满足重新采集条件。"""
|
||||
|
||||
|
||||
class PurchaseRerunError(ValueError):
|
||||
"""当前任务不满足安全重新采购条件。"""
|
||||
|
||||
|
||||
class TaskRemovalError(ValueError):
|
||||
"""勾选任务不满足从普通列表移除的安全条件。"""
|
||||
|
||||
@@ -472,6 +477,42 @@ class TaskRepository:
|
||||
connection.close()
|
||||
return self._to_detail(row)
|
||||
|
||||
def plan_rerun_batch(
|
||||
self, remote_task_ids: Iterable[str], target_type: TaskType
|
||||
) -> TaskRerunPlan:
|
||||
"""批量预检并分类:可执行、类型不符、被安全条件阻止。"""
|
||||
|
||||
stable_ids = tuple(dict.fromkeys(str(value) for value in remote_task_ids if value))
|
||||
eligible: List[str] = []
|
||||
filtered: List[str] = []
|
||||
blocked: List[Tuple[str, str]] = []
|
||||
connection = open_database(self._db_path)
|
||||
try:
|
||||
for task_id in stable_ids:
|
||||
row = connection.execute(
|
||||
"SELECT * FROM pdd_tasks WHERE remote_task_id = ?", (task_id,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
blocked.append((task_id, "任务不存在"))
|
||||
continue
|
||||
if row["task_type"] != target_type.value:
|
||||
filtered.append(task_id)
|
||||
continue
|
||||
try:
|
||||
if target_type is TaskType.COLLECT:
|
||||
self._check_collect_rerun(connection, row)
|
||||
else:
|
||||
self._check_purchase_rerun(connection, row)
|
||||
except (CollectRerunError, PurchaseRerunError) as exc:
|
||||
blocked.append((task_id, str(exc)))
|
||||
else:
|
||||
eligible.append(task_id)
|
||||
finally:
|
||||
connection.close()
|
||||
return TaskRerunPlan(
|
||||
target_type, len(stable_ids), tuple(eligible), tuple(filtered), tuple(blocked)
|
||||
)
|
||||
|
||||
def prepare_collect_rerun(self, remote_task_id: str) -> TaskDetail:
|
||||
"""事务内把已结束的采集任务恢复为待执行,保留旧结果。"""
|
||||
|
||||
@@ -508,6 +549,7 @@ class TaskRepository:
|
||||
if row["task_type"] != TaskType.COLLECT.value:
|
||||
raise CollectRerunError("采购任务不能重新执行,以免重复下单")
|
||||
allowed_statuses = {
|
||||
TaskStatus.CLAIMED.value,
|
||||
TaskStatus.RETRY_WAIT.value,
|
||||
TaskStatus.SUCCEEDED.value,
|
||||
TaskStatus.FAILED.value,
|
||||
@@ -547,6 +589,77 @@ class TaskRepository:
|
||||
"任务仍有未发送的结果,请先勾选任务点击“重新上报”"
|
||||
)
|
||||
|
||||
def validate_purchase_rerun(self, remote_task_id: str) -> TaskDetail:
|
||||
"""校验采购任务能否重新采购;历史上已下单的任务永久拒绝。"""
|
||||
|
||||
connection = open_database(self._db_path)
|
||||
try:
|
||||
row = connection.execute(
|
||||
"SELECT * FROM pdd_tasks WHERE remote_task_id = ?", (remote_task_id,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise PurchaseRerunError(f"任务 {remote_task_id} 不存在")
|
||||
self._check_purchase_rerun(connection, row)
|
||||
finally:
|
||||
connection.close()
|
||||
return self._to_detail(row)
|
||||
|
||||
def prepare_purchase_rerun(self, remote_task_id: str) -> TaskDetail:
|
||||
"""事务内恢复安全采购任务;不会清除任何历史执行记录。"""
|
||||
|
||||
now = utc_now_iso()
|
||||
connection = open_database(self._db_path)
|
||||
try:
|
||||
with connection:
|
||||
row = connection.execute(
|
||||
"SELECT * FROM pdd_tasks WHERE remote_task_id = ?", (remote_task_id,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise PurchaseRerunError(f"任务 {remote_task_id} 不存在")
|
||||
self._check_purchase_rerun(connection, row)
|
||||
connection.execute(
|
||||
"UPDATE pdd_tasks SET status = 'claimed',"
|
||||
" current_step = 'purchase_rerun_requested', finished_at = NULL,"
|
||||
" last_error_code = NULL, last_error_message = NULL, updated_at = ?"
|
||||
" WHERE id = ?",
|
||||
(now, row["id"]),
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
task = self.get_task(remote_task_id)
|
||||
assert task is not None
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def _check_purchase_rerun(
|
||||
connection: sqlite3.Connection, row: sqlite3.Row
|
||||
) -> None:
|
||||
if row["task_type"] != TaskType.PURCHASE.value:
|
||||
raise PurchaseRerunError("采集任务不能重新采购")
|
||||
irreversible = connection.execute(
|
||||
"SELECT 1 FROM task_runs WHERE task_id = ?"
|
||||
" AND irreversible_action_at IS NOT NULL LIMIT 1",
|
||||
(row["id"],),
|
||||
).fetchone()
|
||||
if irreversible is not None:
|
||||
raise PurchaseRerunError("任务历史上已进入下单阶段,只准核对订单,绝不重新下单")
|
||||
allowed = {
|
||||
TaskStatus.CLAIMED.value,
|
||||
TaskStatus.RETRY_WAIT.value,
|
||||
TaskStatus.SUCCEEDED.value,
|
||||
TaskStatus.FAILED.value,
|
||||
TaskStatus.CANCELLED.value,
|
||||
TaskStatus.MANUAL_REVIEW.value,
|
||||
}
|
||||
if row["status"] not in allowed:
|
||||
raise PurchaseRerunError(f"任务当前状态 {row['status']} 不能重新采购")
|
||||
unsent = connection.execute(
|
||||
"SELECT 1 FROM outbox_events WHERE task_id = ? AND status != 'sent' LIMIT 1",
|
||||
(row["id"],),
|
||||
).fetchone()
|
||||
if unsent is not None:
|
||||
raise PurchaseRerunError("任务仍有未发送数据,请先点击“重新上报”")
|
||||
|
||||
def start_collect_run(
|
||||
self, remote_task_id: str, device_address: str
|
||||
) -> StartedTaskRun:
|
||||
|
||||
Reference in New Issue
Block a user