feat(collect): add product status recheck
This commit is contained in:
+230
-21
@@ -11,13 +11,26 @@ from ...collect_skip import (
|
||||
from ... import product_status
|
||||
from ..models import TaskTableModel
|
||||
from ..widgets import *
|
||||
from ..workers import CollectWorker as _RealCollectWorker, WriteBackWorker as _RealWriteBackWorker
|
||||
from ..workers import (
|
||||
CollectWorker as _RealCollectWorker,
|
||||
StatusRecheckWorker as _RealStatusRecheckWorker,
|
||||
WriteBackWorker as _RealWriteBackWorker,
|
||||
)
|
||||
|
||||
|
||||
def CollectWorker(*args, **kwargs):
|
||||
return _call_package_attr("CollectWorker", _RealCollectWorker, *args, **kwargs)
|
||||
|
||||
|
||||
def StatusRecheckWorker(*args, **kwargs):
|
||||
return _call_package_attr(
|
||||
"StatusRecheckWorker",
|
||||
_RealStatusRecheckWorker,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def WriteBackWorker(*args, **kwargs):
|
||||
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
|
||||
|
||||
@@ -84,15 +97,19 @@ class CollectTab(QWidget):
|
||||
self.last_import_stats = None
|
||||
self.collect_worker = None
|
||||
self.collect_thread = None
|
||||
self.status_recheck_worker = None
|
||||
self.status_recheck_thread = None
|
||||
self.write_back_worker = None
|
||||
self.write_back_thread = None
|
||||
self.last_collect_run_id = None
|
||||
self.last_status_recheck_run_id = None
|
||||
self._collect_run_started_at = None
|
||||
self._collect_task_started_at = None
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
self._collect_activity_payload = {}
|
||||
self._collect_terminal_text = ""
|
||||
self._collect_stop_requested = False
|
||||
self._collect_operation_label = "采集"
|
||||
self._collect_elapsed_timer = QTimer(self)
|
||||
self._collect_elapsed_timer.setInterval(1000)
|
||||
self._collect_elapsed_timer.timeout.connect(self._refresh_collect_activity)
|
||||
@@ -100,6 +117,8 @@ class CollectTab(QWidget):
|
||||
self.import_button = QPushButton("导入 Excel...")
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
self.collect_button = QPushButton("采集旧标题/旧封面")
|
||||
self.status_recheck_button = QPushButton("重新检测商品状态")
|
||||
self.status_recheck_button.setObjectName("statusRecheckButton")
|
||||
self.stop_collect_button = QPushButton("停止")
|
||||
self.write_back_button = QPushButton("回写旧数据到 Excel")
|
||||
self.stop_collect_button.setEnabled(False)
|
||||
@@ -123,6 +142,7 @@ class CollectTab(QWidget):
|
||||
toolbar.addWidget(self.import_button)
|
||||
toolbar.addWidget(self.refresh_button)
|
||||
toolbar.addWidget(self.collect_button)
|
||||
toolbar.addWidget(self.status_recheck_button)
|
||||
toolbar.addWidget(self.stop_collect_button)
|
||||
toolbar.addWidget(self.write_back_button)
|
||||
toolbar.addStretch(1)
|
||||
@@ -144,7 +164,7 @@ class CollectTab(QWidget):
|
||||
self.collect_activity_label = QLabel("")
|
||||
self.collect_activity_label.setObjectName("collectActivityLabel")
|
||||
self.collect_activity_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
activity_sample = "正在采集 999/999 · 等待商品页加载 · 本条 99:59"
|
||||
activity_sample = "正在商品状态重检 999/999 · 等待商品页加载 · 本条 99:59"
|
||||
activity_width = self.collect_activity_label.fontMetrics().horizontalAdvance(activity_sample) + 24
|
||||
self.collect_activity_label.setFixedWidth(activity_width)
|
||||
self.collect_activity_label.setVisible(False)
|
||||
@@ -211,6 +231,7 @@ class CollectTab(QWidget):
|
||||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.delete_batch_button.clicked.connect(self.delete_current_batch)
|
||||
self.collect_button.clicked.connect(self.collect_old_data)
|
||||
self.status_recheck_button.clicked.connect(self.recheck_product_status)
|
||||
self.stop_collect_button.clicked.connect(self.stop_collect)
|
||||
self.write_back_button.clicked.connect(self.write_back_old_data)
|
||||
self.show_all_button.clicked.connect(self.show_all_tasks)
|
||||
@@ -232,8 +253,9 @@ class CollectTab(QWidget):
|
||||
"}"
|
||||
)
|
||||
|
||||
def _start_collect_activity(self):
|
||||
def _start_collect_activity(self, operation_label="采集"):
|
||||
now = time.monotonic()
|
||||
self._collect_operation_label = str(operation_label or "采集")
|
||||
self._collect_run_started_at = now
|
||||
self._collect_task_started_at = None
|
||||
self._collect_task_elapsed_seconds = 0
|
||||
@@ -306,7 +328,7 @@ class CollectTab(QWidget):
|
||||
level = "warning"
|
||||
elif state in {"task_started", "task_step"}:
|
||||
text = (
|
||||
f"正在采集 {progress} · {step_label} · "
|
||||
f"正在{self._collect_operation_label} {progress} · {step_label} · "
|
||||
f"本条 {_format_collect_elapsed(task_elapsed)}"
|
||||
)
|
||||
level = "info"
|
||||
@@ -322,7 +344,10 @@ class CollectTab(QWidget):
|
||||
)
|
||||
level = "danger" if event.get("result") == "failed" else "muted"
|
||||
else:
|
||||
text = f"正在检查账号 · {_format_collect_elapsed(run_elapsed)}"
|
||||
if self._collect_operation_label == "采集":
|
||||
text = f"正在检查账号 · {_format_collect_elapsed(run_elapsed)}"
|
||||
else:
|
||||
text = f"正在{self._collect_operation_label}前检查账号 · {_format_collect_elapsed(run_elapsed)}"
|
||||
level = "info"
|
||||
|
||||
tooltip_parts = []
|
||||
@@ -351,21 +376,24 @@ class CollectTab(QWidget):
|
||||
self._collect_stop_requested = False
|
||||
|
||||
if outcome == "blocked":
|
||||
text = "采集未开始 · 检查未通过"
|
||||
text = f"{self._collect_operation_label}未开始 · 检查未通过"
|
||||
level = "warning"
|
||||
tooltip = "采集前检查未通过,请按弹窗提示处理"
|
||||
tooltip = f"{self._collect_operation_label}前检查未通过,请按弹窗提示处理"
|
||||
elif outcome == "cancelled":
|
||||
text = f"采集已停止 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
text = f"{self._collect_operation_label}已停止 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
level = "warning"
|
||||
tooltip = "本轮采集已停止"
|
||||
tooltip = f"本轮{self._collect_operation_label}已停止"
|
||||
elif outcome == "error":
|
||||
text = "采集已结束 · 请查看运行日志"
|
||||
text = f"{self._collect_operation_label}已结束 · 请查看运行日志"
|
||||
level = "danger"
|
||||
tooltip = "采集异常结束,请查看下方采集运行日志"
|
||||
if self._collect_operation_label == "采集":
|
||||
tooltip = "采集异常结束,请查看下方采集运行日志"
|
||||
else:
|
||||
tooltip = f"{self._collect_operation_label}异常结束,请查看下方运行日志"
|
||||
else:
|
||||
text = f"采集完成 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
text = f"{self._collect_operation_label}完成 · 总用时 {_format_collect_elapsed(total_elapsed)}"
|
||||
level = "success"
|
||||
tooltip = "本轮采集已经完成"
|
||||
tooltip = f"本轮{self._collect_operation_label}已经完成"
|
||||
self._collect_terminal_text = text
|
||||
self.collect_activity_label.setToolTip(tooltip)
|
||||
self._set_collect_activity_style(level)
|
||||
@@ -379,9 +407,9 @@ class CollectTab(QWidget):
|
||||
def _append_collect_log(self, message):
|
||||
self.run_log_view.appendPlainText(str(message))
|
||||
|
||||
def _load_latest_collect_run_log(self):
|
||||
def _load_latest_collect_run_log(self, run_type="collect"):
|
||||
try:
|
||||
logs = db.list_run_logs(limit=1, run_type="collect", path=self.db_path)
|
||||
logs = db.list_run_logs(limit=1, run_type=run_type, path=self.db_path)
|
||||
if not logs:
|
||||
return
|
||||
events = db.list_run_log_events(logs[0].id, limit=30, path=self.db_path)
|
||||
@@ -408,10 +436,10 @@ class CollectTab(QWidget):
|
||||
self._set_status(text, level="danger")
|
||||
QTimer.singleShot(0, lambda: QMessageBox.warning(self, "导入采集", text))
|
||||
|
||||
def _show_account_guide(self, message):
|
||||
def _show_account_guide(self, message, operation_label="采集"):
|
||||
full_message = (
|
||||
f"{message}\n\n"
|
||||
"本轮采集已中止。\n"
|
||||
f"本轮{operation_label}已中止。\n"
|
||||
"请先到「账号管理」检查账号配置、Chrome 路径和登录状态。"
|
||||
)
|
||||
QMessageBox.warning(self, "账号未就绪", full_message)
|
||||
@@ -663,7 +691,11 @@ class CollectTab(QWidget):
|
||||
return self.batch_filter.currentData()
|
||||
|
||||
def _update_delete_batch_button(self):
|
||||
running = bool(self.collect_thread or self.write_back_thread)
|
||||
running = bool(
|
||||
self.collect_thread
|
||||
or self.status_recheck_thread
|
||||
or self.write_back_thread
|
||||
)
|
||||
self.delete_batch_button.setEnabled((not running) and bool(self._selected_batch_id()))
|
||||
|
||||
def delete_current_batch(self, checked=False):
|
||||
@@ -716,6 +748,9 @@ class CollectTab(QWidget):
|
||||
QMessageBox.information(self, "删除批次", message)
|
||||
|
||||
def collect_old_data(self, checked=False):
|
||||
if self._collect_operation_running() or self.write_back_thread is not None:
|
||||
self._set_status("当前批处理尚未结束,请稍后再试")
|
||||
return
|
||||
tasks = list(self.model.tasks)
|
||||
if not tasks:
|
||||
self._set_status("没有可采集任务")
|
||||
@@ -749,6 +784,51 @@ class CollectTab(QWidget):
|
||||
self._start_collect_activity()
|
||||
thread.start()
|
||||
|
||||
def recheck_product_status(self, checked=False):
|
||||
if self._collect_operation_running() or self.write_back_thread is not None:
|
||||
self._set_status("当前批处理尚未结束,请稍后再试")
|
||||
return
|
||||
tasks = list(self.model.tasks)
|
||||
if not tasks:
|
||||
self._set_status("当前筛选结果没有可重新检测商品状态的任务")
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"重新检测商品状态",
|
||||
"将重新打开当前筛选结果中的 {count} 条商品详情页,只读取并保存商品状态。\n\n"
|
||||
"不会读取或覆盖旧标题、旧封面、新标题、新封面;不会改变任务阶段、结果、线上提交标记或 Excel。\n\n"
|
||||
"确定开始重新检测吗?".format(count=len(tasks)),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
self._set_status("已取消重新检测商品状态")
|
||||
return
|
||||
worker = StatusRecheckWorker(
|
||||
tasks,
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||||
)
|
||||
activity_signal = getattr(worker, "activity", None)
|
||||
if activity_signal is not None:
|
||||
activity_signal.connect(self._on_collect_activity)
|
||||
worker.progress.connect(self._on_status_recheck_progress)
|
||||
worker.row_updated.connect(self._on_status_recheck_row_updated)
|
||||
worker.log.connect(self._on_collect_log)
|
||||
worker.failed.connect(self._on_status_recheck_failed)
|
||||
worker.finished.connect(self._on_status_recheck_finished)
|
||||
worker.cancelled.connect(self._on_status_recheck_cancelled)
|
||||
self.run_log_view.clear()
|
||||
thread = run_worker(worker, thread_name="StatusRecheckWorker", start=False)
|
||||
thread.finished.connect(lambda: self._forget_status_recheck_thread(thread))
|
||||
self.status_recheck_worker = worker
|
||||
self.status_recheck_thread = thread
|
||||
self._set_collect_running(True)
|
||||
self._start_collect_activity("商品状态重检")
|
||||
self._set_status(f"正在重新检测 {len(tasks)} 条商品状态...")
|
||||
thread.start()
|
||||
|
||||
def _choose_collect_scope(self):
|
||||
box = ProductStatusScopeDialog(
|
||||
title="选择采集范围",
|
||||
@@ -772,11 +852,12 @@ class CollectTab(QWidget):
|
||||
return box.choice()
|
||||
|
||||
def stop_collect(self, checked=False):
|
||||
if self.collect_worker is not None:
|
||||
self.collect_worker.cancel()
|
||||
worker = self.collect_worker or self.status_recheck_worker
|
||||
if worker is not None:
|
||||
worker.cancel()
|
||||
self._collect_stop_requested = True
|
||||
self._refresh_collect_activity()
|
||||
self._set_status("正在停止采集...")
|
||||
self._set_status(f"正在停止{self._collect_operation_label}...")
|
||||
|
||||
def write_back_old_data(self, checked=False):
|
||||
batch_id = self._active_batch_id()
|
||||
@@ -786,6 +867,9 @@ class CollectTab(QWidget):
|
||||
self._start_write_back(batch_id)
|
||||
|
||||
def _start_write_back(self, batch_id, auto=False):
|
||||
if self._collect_operation_running():
|
||||
self._set_status("当前批处理尚未结束,暂不能回写 Excel")
|
||||
return False
|
||||
if self.write_back_thread is not None:
|
||||
self._set_status("Excel 回写正在进行...")
|
||||
return False
|
||||
@@ -832,6 +916,7 @@ class CollectTab(QWidget):
|
||||
self.import_button.setEnabled(not running)
|
||||
self.refresh_button.setEnabled(not running)
|
||||
self.collect_button.setEnabled(not running)
|
||||
self.status_recheck_button.setEnabled(not running)
|
||||
self.write_back_button.setEnabled(not running)
|
||||
self.stop_collect_button.setEnabled(running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
@@ -844,6 +929,7 @@ class CollectTab(QWidget):
|
||||
self.import_button.setEnabled(not running)
|
||||
self.refresh_button.setEnabled(not running)
|
||||
self.collect_button.setEnabled(not running)
|
||||
self.status_recheck_button.setEnabled(not running)
|
||||
self.write_back_button.setEnabled(not running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_filter.setEnabled(not running)
|
||||
@@ -858,6 +944,16 @@ class CollectTab(QWidget):
|
||||
self.collect_thread = None
|
||||
self.collect_worker = None
|
||||
|
||||
def _forget_status_recheck_thread(self, thread):
|
||||
if self.status_recheck_thread is thread:
|
||||
if self._collect_elapsed_timer.isActive():
|
||||
self._finish_collect_activity("error")
|
||||
self.status_recheck_thread = None
|
||||
self.status_recheck_worker = None
|
||||
|
||||
def _collect_operation_running(self):
|
||||
return bool(self.collect_thread or self.status_recheck_thread)
|
||||
|
||||
def _forget_write_back_thread(self, thread):
|
||||
if self.write_back_thread is thread:
|
||||
self.write_back_thread = None
|
||||
@@ -880,6 +976,25 @@ class CollectTab(QWidget):
|
||||
def _on_collect_failed(self, task_id, error):
|
||||
self._set_status(f"任务 {task_id} 采集失败:{error}")
|
||||
|
||||
def _on_status_recheck_progress(self, payload):
|
||||
self._set_status(
|
||||
"商品状态重检进度:{done}/{total},成功{rechecked},略过{skipped},失败{failed}".format(
|
||||
done=payload.get("done", 0),
|
||||
total=payload.get("total", 0),
|
||||
rechecked=payload.get("rechecked", 0),
|
||||
skipped=payload.get("skipped", 0),
|
||||
failed=payload.get("failed", 0),
|
||||
)
|
||||
)
|
||||
|
||||
def _on_status_recheck_row_updated(self, task_id, fields):
|
||||
self.refresh_tasks()
|
||||
if self.refresh_workflow_callback is not None:
|
||||
self.refresh_workflow_callback()
|
||||
|
||||
def _on_status_recheck_failed(self, task_id, error):
|
||||
self._set_status(f"任务 {task_id} 商品状态重检失败:{error}")
|
||||
|
||||
def _on_collect_finished(self, payload):
|
||||
self._set_collect_running(False)
|
||||
if payload.get("blocked"):
|
||||
@@ -928,6 +1043,40 @@ class CollectTab(QWidget):
|
||||
return
|
||||
self._set_status(message)
|
||||
|
||||
def _on_status_recheck_finished(self, payload):
|
||||
self._set_collect_running(False)
|
||||
if payload.get("blocked"):
|
||||
self._finish_collect_activity("blocked", payload)
|
||||
elif payload.get("error"):
|
||||
self._finish_collect_activity("error", payload)
|
||||
else:
|
||||
self._finish_collect_activity("finished", payload)
|
||||
self.last_status_recheck_run_id = (
|
||||
payload.get("run_id") or self.last_status_recheck_run_id
|
||||
)
|
||||
self.refresh_tasks()
|
||||
if self.refresh_workflow_callback is not None:
|
||||
self.refresh_workflow_callback()
|
||||
self._load_latest_collect_run_log("status_recheck")
|
||||
if payload.get("blocked"):
|
||||
self._show_status_recheck_blocked(payload)
|
||||
return
|
||||
message = "商品状态重检完成:成功{rechecked},略过{skipped},失败{failed}".format(
|
||||
rechecked=payload.get("rechecked", 0),
|
||||
skipped=payload.get("skipped", 0),
|
||||
failed=payload.get("failed", 0),
|
||||
)
|
||||
counts = payload.get("product_status_counts") or {}
|
||||
if counts:
|
||||
message += ";正常{normal},未上架{unlisted},审核中{reviewing},状态未知{unknown}".format(
|
||||
normal=counts.get("normal", 0),
|
||||
unlisted=counts.get("unlisted", 0),
|
||||
reviewing=counts.get("reviewing", 0),
|
||||
unknown=counts.get("unknown", 0),
|
||||
)
|
||||
self._show_status_recheck_account_summary(payload, message)
|
||||
self._set_status(message)
|
||||
|
||||
def _show_collect_blocked(self, payload):
|
||||
lines = ["采集前检查未通过。"]
|
||||
if payload.get("no_accounts"):
|
||||
@@ -952,6 +1101,18 @@ class CollectTab(QWidget):
|
||||
)
|
||||
self._show_account_guide("\n".join(lines))
|
||||
|
||||
def _show_status_recheck_blocked(self, payload):
|
||||
lines = ["商品状态重检前检查未通过。"]
|
||||
if payload.get("no_accounts"):
|
||||
lines.append("当前没有配置账号。")
|
||||
launch_failed = payload.get("launch_failed") or []
|
||||
if launch_failed:
|
||||
lines.append(
|
||||
"以下账号 Chrome 启动失败:"
|
||||
+ "、".join(self._account_label(item) for item in launch_failed)
|
||||
)
|
||||
self._show_account_guide("\n".join(lines), operation_label="商品状态重检")
|
||||
|
||||
def _show_collect_account_summary(self, payload, message):
|
||||
launched = payload.get("launched_accounts") or []
|
||||
reused = payload.get("reused_accounts") or []
|
||||
@@ -1002,6 +1163,41 @@ class CollectTab(QWidget):
|
||||
else:
|
||||
QMessageBox.information(self, "采集完成", text)
|
||||
|
||||
def _show_status_recheck_account_summary(self, payload, message):
|
||||
launched = payload.get("launched_accounts") or []
|
||||
reused = payload.get("reused_accounts") or []
|
||||
login_required = payload.get("login_required_accounts") or []
|
||||
skip_counts = normalize_skip_reason_counts(
|
||||
payload.get("skip_reason_counts"),
|
||||
skipped_total=int(payload.get("skipped", 0) or 0),
|
||||
)
|
||||
lines = [message]
|
||||
skip_summary = format_skip_reason_summary(
|
||||
skip_counts,
|
||||
skipped_total=int(payload.get("skipped", 0) or 0),
|
||||
)
|
||||
if skip_summary:
|
||||
lines.append(skip_summary)
|
||||
if launched:
|
||||
lines.append(
|
||||
"本轮已自动启动账号 Chrome:"
|
||||
+ "、".join(self._account_label(item) for item in launched)
|
||||
)
|
||||
if login_required:
|
||||
lines.append(
|
||||
"以下账号需要补登录:"
|
||||
+ "、".join(self._account_label(item) for item in login_required)
|
||||
)
|
||||
if login_required or skip_counts[LOGIN_REQUIRED] > 0:
|
||||
lines.append("请到账号管理完成对应账号登录后,再重新检测商品状态。")
|
||||
if launched or reused:
|
||||
lines.append("检测结束后不会自动关闭账号 Chrome,请按需自行关闭。")
|
||||
text = "\n".join(lines)
|
||||
if login_required or skip_counts[LOGIN_REQUIRED] > 0:
|
||||
QMessageBox.warning(self, "商品状态重检完成", text)
|
||||
else:
|
||||
QMessageBox.information(self, "商品状态重检完成", text)
|
||||
|
||||
def _account_label(self, item):
|
||||
if isinstance(item, dict):
|
||||
name = item.get("account_name") or item.get("alias") or ""
|
||||
@@ -1025,6 +1221,19 @@ class CollectTab(QWidget):
|
||||
)
|
||||
)
|
||||
|
||||
def _on_status_recheck_cancelled(self, payload):
|
||||
self._set_collect_running(False)
|
||||
self._finish_collect_activity("cancelled", payload)
|
||||
self.refresh_tasks()
|
||||
if self.refresh_workflow_callback is not None:
|
||||
self.refresh_workflow_callback()
|
||||
self._set_status(
|
||||
"商品状态重检已停止:完成{done}/{total}".format(
|
||||
done=payload.get("done", 0),
|
||||
total=payload.get("total", 0),
|
||||
)
|
||||
)
|
||||
|
||||
def _on_write_back_failed(self, task_id, error, auto=False):
|
||||
message = f"Excel {'自动' if auto else ''}回写失败:{error}"
|
||||
if "被占用" in str(error):
|
||||
|
||||
Reference in New Issue
Block a user