feat: 安全继续失败采购核单 (#262)

This commit is contained in:
chengma
2026-08-18 09:55:32 +08:00
parent a92b8746b2
commit 5ea8aa3d68
8 changed files with 480 additions and 20 deletions
+119 -11
View File
@@ -123,6 +123,12 @@ ERROR_FEEDBACK_BUTTON_MIN_HEIGHT = 40
PURCHASE_RECONCILE_DELAY_MS = 5_000
def _reject_reconcile_only_purchase_adapter(*_args):
"""继续核单分支绝不允许创建采购 Adapter。"""
raise RuntimeError("只读核单禁止创建采购执行器")
class ClaimTaskWorker(QObject):
"""在固定后台线程中串行补交、领取或执行至多一条任务。"""
@@ -319,6 +325,14 @@ class ClaimTaskWorker(QObject):
collect_service_factory=self._collect_service_factory,
)
result = service.execute_selected(task_id)
elif task_id in command.reconcile_task_ids:
result = self._execute_purchase_reconcile_retry(
task_id,
client,
android_serial,
current=index + 1,
total=len(command.task_ids),
)
else:
result = self._execute_purchase_rerun(
task_id,
@@ -329,6 +343,14 @@ class ClaimTaskWorker(QObject):
)
except (CollectRerunError, PurchaseRerunError, ValueError) as exc:
self.outcome.emit("skipped", str(exc), task_id)
if command.target_type is TaskType.PURCHASE:
for remaining_id in command.task_ids[index + 1 :]:
self.outcome.emit(
"skipped",
f"前一采购任务未形成成功闭环,任务 {remaining_id} 未开始",
remaining_id,
)
break
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":
@@ -340,6 +362,48 @@ class ClaimTaskWorker(QObject):
)
break
def _execute_purchase_reconcile_retry(
self,
task_id: str,
client: ClientInfo,
android_serial: str,
current: int = 1,
total: int = 1,
):
"""恢复原不可逆运行并只读核单,绝不创建采购运行。"""
if self._purchase_reconcile_factory is None:
raise ValueError("采购核单执行器未就绪;绝不重新下单")
self._task_repository.prepare_purchase_reconcile_retry(task_id)
self.taskStarted.emit(
task_id,
TaskType.PURCHASE.value,
current,
total,
)
result = PurchaseReconcileService(
self._task_repository,
android_serial,
self._purchase_reconcile_factory,
cancelled=lambda: False,
).execute_selected(task_id)
if result.kind != "result_pending":
return result
event = self._task_repository.outbox_for_resubmit(task_id)
if event is None:
return type(result)(
"manual_review", "核单结果未写入待上报队列", task_id
)
submitter = PurchaseTaskService(
self._gateway,
self._task_repository,
client,
android_serial,
_reject_reconcile_only_purchase_adapter,
cancelled=lambda: False,
)
return submitter.submit_saved_event(event)
def _execute_purchase_rerun(
self,
task_id: str,
@@ -768,30 +832,59 @@ class PDDTaskPageEvent(QObject):
return
action = "采集" if target_type is TaskType.COLLECT else "采购"
if not plan.eligible_task_ids:
processable_task_ids = (
plan.reconcile_task_ids + plan.eligible_task_ids
)
if not processable_task_ids:
self._show_rerun_warning(
f"没有可重新{action}的任务",
f"已选 {plan.selected_count} 条,过滤 {len(plan.filtered_task_ids)} 条其他类型,"
f"安全条件阻止 {len(plan.blocked)} 条。",
)
return
reconcile_count = len(plan.reconcile_task_ids)
purchase_count = len(plan.eligible_task_ids)
if target_type is TaskType.PURCHASE:
title = f"确认处理 {len(processable_task_ids)} 条采购任务?"
detail = (
f"已选 {plan.selected_count} 条;可重新采购 {purchase_count} 条;"
f"只继续核单 {reconcile_count} 条;"
f"过滤其他类型 {len(plan.filtered_task_ids)} 条;"
f"安全条件阻止 {len(plan.blocked)} 条。\n\n"
"已提交订单的任务只读取待付款订单,绝不重新下单;"
"只读核单优先执行,结果不确定时立即停止剩余任务。"
)
confirm_text = (
"继续核单"
if reconcile_count and not purchase_count
else "继续处理"
)
else:
title = f"确认重新{action} {len(plan.eligible_task_ids)} 条任务?"
detail = (
f"已选 {plan.selected_count} 条;可执行 {len(plan.eligible_task_ids)} 条;"
f"过滤其他类型 {len(plan.filtered_task_ids)} 条;"
f"安全条件阻止 {len(plan.blocked)} 条。\n\n"
"新结果会覆盖 Client 和 Admin 当前采集数据,旧执行记录仍保留。"
)
confirm_text = f"重新{action}"
dialog = MessageBox(
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 当前采集数据,旧执行记录仍保留。"),
title,
detail,
self._page.window(),
)
dialog.yesButton.setText(f"重新{action}")
dialog.yesButton.setText(confirm_text)
dialog.cancelButton.setText("取消")
dialog.cancelButton.setFocus()
if not dialog.exec():
return
self._start_rerun_worker(
TaskRerunBatch(target_type, plan.eligible_task_ids)
TaskRerunBatch(
target_type,
processable_task_ids,
plan.reconcile_task_ids,
)
)
@pyqtSlot(object)
@@ -1000,8 +1093,7 @@ class PDDTaskPageEvent(QObject):
self._claim_busy = True
self._page.set_rerun_running(True)
self._page.set_engine_status(
f"已加入队列 {len(command.task_ids)} 条,正在检查 Android 设备,等待开始重新"
f"{'采集' if command.target_type is TaskType.COLLECT else '采购'}…"
self._rerun_queue_status(command)
)
self._reload()
@@ -1010,6 +1102,22 @@ class PDDTaskPageEvent(QObject):
worker.prepare_run()
self._claimRunRequested.emit(command)
@staticmethod
def _rerun_queue_status(command: TaskRerunBatch) -> str:
total = len(command.task_ids)
if command.target_type is TaskType.COLLECT:
action = "重新采集"
elif len(command.reconcile_task_ids) == total:
action = "继续核单"
elif command.reconcile_task_ids:
action = "处理采购任务"
else:
action = "重新采购"
return (
f"已加入队列 {total} 条,正在检查 Android 设备,"
f"等待开始{action}…"
)
@pyqtSlot()
def request_cancel_rerun(self) -> None:
"""请求当前重新采集在下一个安全点停止。"""