feat: 任务失败后继续执行下一条 (#170)
This commit is contained in:
@@ -36,6 +36,20 @@ CollectServiceFactory = Callable[
|
||||
[str, str, Callable[[], bool]], PddCollectService
|
||||
]
|
||||
|
||||
_QUEUE_BLOCKING_ERROR_CODES = frozenset(
|
||||
{
|
||||
"DEVICE_APP_START_FAILED",
|
||||
"DEVICE_DISCONNECTED",
|
||||
"DEVICE_NOT_FOUND",
|
||||
"DEVICE_OFFLINE",
|
||||
"DEVICE_SESSION_CLOSED",
|
||||
"DEVICE_UNAUTHORIZED",
|
||||
"PDD_PAGE_CAPTCHA",
|
||||
"PDD_PAGE_LOGIN_REQUIRED",
|
||||
"PDD_PAGE_UNKNOWN",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CollectTaskService:
|
||||
"""一次调用只处理一条本地工作或一个待提交事件。"""
|
||||
@@ -149,13 +163,16 @@ class CollectTaskService:
|
||||
collector = self._factory(
|
||||
self._device_address, self._client.client_id, self._cancelled
|
||||
)
|
||||
business_failure_message = ""
|
||||
failure_message = ""
|
||||
queue_blocked = False
|
||||
try:
|
||||
result = collector.collect(started.task)
|
||||
event = self._repository.save_collect_result(
|
||||
remote_task_id, started.attempt_id, result.to_pdd_data()
|
||||
)
|
||||
except PddCollectError as exc:
|
||||
failure_message = exc.message
|
||||
queue_blocked = exc.code in _QUEUE_BLOCKING_ERROR_CODES
|
||||
status, retryable = self._classify_error(exc.code)
|
||||
report_code = self._report_error_code(exc.code)
|
||||
event = self._repository.save_collect_failure(
|
||||
@@ -169,12 +186,12 @@ class CollectTaskService:
|
||||
)
|
||||
if exc.code == "PDD_CANCELLED":
|
||||
return self._submit_cancelled(event, remote_task_id)
|
||||
if exc.code == "PDD_GOODS_UNAVAILABLE":
|
||||
business_failure_message = exc.message
|
||||
outcome = self._submit(event)
|
||||
if business_failure_message and outcome.kind == "failed":
|
||||
if failure_message and outcome.kind == "failed":
|
||||
return CollectTaskOutcome(
|
||||
"business_failed", business_failure_message, remote_task_id
|
||||
"global_failed" if queue_blocked else "task_failed",
|
||||
f"任务 {remote_task_id} 采集失败:{failure_message}",
|
||||
remote_task_id,
|
||||
)
|
||||
return outcome
|
||||
|
||||
@@ -239,18 +256,10 @@ class CollectTaskService:
|
||||
return TaskStatus.CANCELLED, False
|
||||
if code == "PDD_GOODS_UNAVAILABLE":
|
||||
return TaskStatus.FAILED, False
|
||||
if code in {
|
||||
"PDD_PAGE_LOGIN_REQUIRED",
|
||||
"PDD_PAGE_CAPTCHA",
|
||||
"PDD_DATA_SPEC_INCOMPLETE",
|
||||
"PDD_DATA_TITLE_MISSING",
|
||||
"PDD_DATA_PRICE_MISSING",
|
||||
"PDD_DATA_SKU_NAME_TRUNCATED",
|
||||
}:
|
||||
if code in {"PDD_PAGE_LOGIN_REQUIRED", "PDD_PAGE_CAPTCHA"}:
|
||||
return TaskStatus.MANUAL_REVIEW, False
|
||||
if code.startswith("PDD_DATA_GOODS_"):
|
||||
return TaskStatus.FAILED, False
|
||||
return TaskStatus.RETRY_WAIT, True
|
||||
# 自动流程只执行一次。失败任务由用户明确点击“重新采集”后再运行。
|
||||
return TaskStatus.FAILED, False
|
||||
|
||||
@staticmethod
|
||||
def _report_error_code(code: str) -> str:
|
||||
|
||||
@@ -108,7 +108,7 @@ TASK_STATUS_TEXT = {
|
||||
TaskStatus.CLAIMED: "待执行",
|
||||
TaskStatus.RUNNING: "执行中",
|
||||
TaskStatus.RESULT_PENDING: "结果待提交",
|
||||
TaskStatus.RETRY_WAIT: "等待重试",
|
||||
TaskStatus.RETRY_WAIT: "失败",
|
||||
TaskStatus.MANUAL_REVIEW: "需要人工处理",
|
||||
TaskStatus.SUCCEEDED: "已完成",
|
||||
TaskStatus.FAILED: "失败",
|
||||
@@ -1420,7 +1420,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._reload()
|
||||
if kind == "no_task":
|
||||
self._on_no_claimed_task()
|
||||
elif kind in {"succeeded", "business_failed"}:
|
||||
elif kind in {"succeeded", "business_failed", "task_failed"}:
|
||||
self._retry_count = 0
|
||||
self._continue_after(
|
||||
self._next_task_delay_ms,
|
||||
@@ -1438,14 +1438,7 @@ class PDDTaskPageEvent(QObject):
|
||||
f"{seconds:g} 秒后自动核对订单"
|
||||
),
|
||||
)
|
||||
elif kind == "failed" and self._is_retry_wait_task(task_id):
|
||||
content = (
|
||||
f"自动获取:已停止 · 任务 {task_id} 重试已暂停;"
|
||||
"请选择该任务点击“重新执行”,或重新启动获取任务"
|
||||
)
|
||||
self._show_retry_paused(content)
|
||||
self._stop_after_current(content)
|
||||
elif kind in {"manual_review", "failed"}:
|
||||
elif kind in {"global_failed", "manual_review", "failed"}:
|
||||
self._show_claim_error("任务需要处理", message)
|
||||
self._stop_after_current(message)
|
||||
elif kind == "cancelled":
|
||||
@@ -1455,21 +1448,6 @@ class PDDTaskPageEvent(QObject):
|
||||
self._show_claim_error("自动获取已停止", content)
|
||||
self._stop_after_current(content)
|
||||
|
||||
def _is_retry_wait_task(self, task_id: str) -> bool:
|
||||
"""判断失败结果对应的任务是否仍可在本地重试。"""
|
||||
|
||||
if not task_id:
|
||||
return False
|
||||
try:
|
||||
task = self._repository.get_task(task_id)
|
||||
except Exception:
|
||||
return False
|
||||
return (
|
||||
task is not None
|
||||
and task.task_type is TaskType.COLLECT
|
||||
and task.status is TaskStatus.RETRY_WAIT
|
||||
)
|
||||
|
||||
def _show_retry_paused(self, content: str) -> None:
|
||||
"""用持久警告说明任务不会自行倒计时重试。"""
|
||||
|
||||
@@ -1750,6 +1728,10 @@ 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 "采购中"
|
||||
elif summary.status in {TaskStatus.RETRY_WAIT, TaskStatus.FAILED}:
|
||||
status_text = (
|
||||
"采集失败" if summary.task_type is TaskType.COLLECT else "采购失败"
|
||||
)
|
||||
|
||||
return TaskRow(
|
||||
remote_task_id=summary.remote_task_id,
|
||||
|
||||
@@ -56,6 +56,20 @@ LivePurchaseAdapterFactory = Callable[
|
||||
[str, Callable[[], bool]], PddLivePurchaseAdapter
|
||||
]
|
||||
|
||||
_QUEUE_BLOCKING_ERROR_CODES = frozenset(
|
||||
{
|
||||
"DEVICE_APP_START_FAILED",
|
||||
"DEVICE_DISCONNECTED",
|
||||
"DEVICE_SESSION_CLOSED",
|
||||
"PDD_PAGE_CAPTCHA",
|
||||
"PDD_PAGE_LOGIN_REQUIRED",
|
||||
"PDD_PAGE_PAYMENT",
|
||||
"PDD_PAGE_RISK_CONTROL",
|
||||
"PDD_PAGE_UNKNOWN",
|
||||
"PURCHASE_ADAPTER_ERROR",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class PurchaseTaskService:
|
||||
"""只执行本地已保存的采购任务,不领取新任务。"""
|
||||
@@ -108,7 +122,8 @@ class PurchaseTaskService:
|
||||
self._started(remote_task_id)
|
||||
adapter: PddPurchaseAdapter | None = None
|
||||
step = "purchase_prepare"
|
||||
business_failure_message = ""
|
||||
failure_message = ""
|
||||
failure_outcome_kind = ""
|
||||
try:
|
||||
target = self._target_from_task(started.task)
|
||||
adapter = self._factory(self._device_address, self._cancelled)
|
||||
@@ -184,6 +199,13 @@ class PurchaseTaskService:
|
||||
remote_task_id, started.attempt_id, result
|
||||
)
|
||||
except PddPurchaseError as exc:
|
||||
failure_message = exc.message
|
||||
if exc.code == "PURCHASE_CANCELLED":
|
||||
failure_outcome_kind = "cancelled"
|
||||
elif exc.code in _QUEUE_BLOCKING_ERROR_CODES:
|
||||
failure_outcome_kind = "global_failed"
|
||||
else:
|
||||
failure_outcome_kind = "task_failed"
|
||||
event = self._save_failure(
|
||||
remote_task_id,
|
||||
started.attempt_id,
|
||||
@@ -193,9 +215,9 @@ class PurchaseTaskService:
|
||||
step,
|
||||
exc.diagnostics,
|
||||
)
|
||||
if exc.code == "PDD_GOODS_UNAVAILABLE":
|
||||
business_failure_message = exc.message
|
||||
except Exception as exc:
|
||||
failure_message = f"采购在“{step}”发生未知错误:{exc}"
|
||||
failure_outcome_kind = "global_failed"
|
||||
event = self._save_failure(
|
||||
remote_task_id,
|
||||
started.attempt_id,
|
||||
@@ -209,9 +231,11 @@ class PurchaseTaskService:
|
||||
if adapter is not None:
|
||||
adapter.close()
|
||||
outcome = self._submit(event)
|
||||
if business_failure_message and outcome.kind == "failed":
|
||||
if failure_message and outcome.kind == "failed":
|
||||
return PurchaseTaskOutcome(
|
||||
"business_failed", business_failure_message, remote_task_id
|
||||
failure_outcome_kind,
|
||||
f"任务 {remote_task_id} 采购失败:{failure_message}",
|
||||
remote_task_id,
|
||||
)
|
||||
return outcome
|
||||
|
||||
@@ -488,25 +512,25 @@ class PurchaseTaskService:
|
||||
attempt_id: str,
|
||||
code: str,
|
||||
message: str,
|
||||
retryable: bool,
|
||||
_retryable: bool,
|
||||
step: str,
|
||||
diagnostics: Mapping[str, object],
|
||||
) -> OutboxEventRecord:
|
||||
if code == "PURCHASE_CANCELLED":
|
||||
status = TaskStatus.CANCELLED
|
||||
elif code == "PDD_GOODS_UNAVAILABLE":
|
||||
status = TaskStatus.FAILED
|
||||
else:
|
||||
# 采购没有手动“重新执行”入口。即使错误属于
|
||||
# 技术上可重试,也先留给人工判断,避免隐式重复采购。
|
||||
elif code in _QUEUE_BLOCKING_ERROR_CODES:
|
||||
status = TaskStatus.MANUAL_REVIEW
|
||||
else:
|
||||
# 一次任务只自动执行一次;再次采购只能由用户明确发起,且仍需
|
||||
# 通过不可逆标记等现有安全预检。
|
||||
status = TaskStatus.FAILED
|
||||
return self._repository.save_purchase_failure(
|
||||
remote_task_id,
|
||||
attempt_id,
|
||||
status,
|
||||
code,
|
||||
message,
|
||||
retryable,
|
||||
False,
|
||||
step,
|
||||
dict(diagnostics),
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ TASK_STATUS_TEXT = {
|
||||
TaskStatus.CLAIMED: "待执行",
|
||||
TaskStatus.RUNNING: "执行中",
|
||||
TaskStatus.RESULT_PENDING: "结果待提交",
|
||||
TaskStatus.RETRY_WAIT: "等待重试",
|
||||
TaskStatus.RETRY_WAIT: "失败",
|
||||
TaskStatus.MANUAL_REVIEW: "需要人工处理",
|
||||
TaskStatus.SUCCEEDED: "已完成",
|
||||
TaskStatus.FAILED: "失败",
|
||||
@@ -370,10 +370,14 @@ def build_task_detail_view_data(detail: TaskDetail) -> TaskDetailViewData:
|
||||
if value
|
||||
)
|
||||
|
||||
status_text = TASK_STATUS_TEXT.get(detail.status, detail.status.value)
|
||||
if detail.status in {TaskStatus.RETRY_WAIT, TaskStatus.FAILED}:
|
||||
status_text = "采集失败" if detail.task_type is TaskType.COLLECT else "采购失败"
|
||||
|
||||
return TaskDetailViewData(
|
||||
task_id=detail.remote_task_id,
|
||||
task_type=TASK_TYPE_TEXT.get(detail.task_type, detail.task_type.value),
|
||||
status=TASK_STATUS_TEXT.get(detail.status, detail.status.value),
|
||||
status=status_text,
|
||||
title=_non_empty_text(pdd_data.get("title")) or detail.title or "未采集",
|
||||
goods_id=_non_empty_text(pdd_data.get("goods_id")) or detail.goods_id or "—",
|
||||
goods_url=detail.goods_url,
|
||||
|
||||
@@ -379,7 +379,7 @@ class TaskRepository:
|
||||
try:
|
||||
row = connection.execute(
|
||||
"SELECT * FROM pdd_tasks"
|
||||
" WHERE task_type = 'collect' AND status IN ('claimed', 'retry_wait')"
|
||||
" WHERE task_type = 'collect' AND status = 'claimed'"
|
||||
" ORDER BY received_at ASC, id ASC LIMIT 1"
|
||||
).fetchone()
|
||||
finally:
|
||||
@@ -456,15 +456,11 @@ class TaskRepository:
|
||||
|
||||
if include_purchase:
|
||||
where = (
|
||||
"((task_type = 'collect'"
|
||||
" AND status IN ('claimed', 'retry_wait'))"
|
||||
"((task_type = 'collect' AND status = 'claimed')"
|
||||
" OR (task_type = 'purchase' AND status = 'claimed'))"
|
||||
)
|
||||
else:
|
||||
where = (
|
||||
"task_type = 'collect'"
|
||||
" AND status IN ('claimed', 'retry_wait')"
|
||||
)
|
||||
where = "task_type = 'collect' AND status = 'claimed'"
|
||||
connection = open_database(self._db_path)
|
||||
try:
|
||||
row = connection.execute(
|
||||
|
||||
Reference in New Issue
Block a user