Files
cmshoppe/app/gui/tabs/collect.py
T

840 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tab 1: Excel import and data collection UI."""
from __future__ import annotations
from ..models import TaskTableModel
from ..widgets import *
from ..workers import CollectWorker as _RealCollectWorker, WriteBackWorker as _RealWriteBackWorker
def CollectWorker(*args, **kwargs):
return _call_package_attr("CollectWorker", _RealCollectWorker, *args, **kwargs)
def WriteBackWorker(*args, **kwargs):
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
class CollectTab(QWidget):
"""Tab 1: import Excel files and list imported tasks."""
STATUS_FILTERS = [
("全部状态", "all"),
("待采集", "to_collect"),
("已采集", "collected"),
("已生成", "generated"),
("已更新", "applied"),
("失败", "failed"),
("略过", "skipped"),
]
def __init__(
self,
parent=None,
db_path=None,
config=None,
status_callback=None,
open_accounts_callback=None,
refresh_workflow_callback=None,
):
super().__init__(parent)
self.config = appconfig.load_config() if config is None else config
self.db_path = _database_path(db_path, self.config)
self.status_callback = status_callback
self.open_accounts_callback = open_accounts_callback
self.refresh_workflow_callback = refresh_workflow_callback
self.current_batch_id = None
self.has_import_result = False
self.last_import_stats = None
self.collect_worker = None
self.collect_thread = None
self.write_back_worker = None
self.write_back_thread = None
self.last_collect_run_id = None
self.import_button = QPushButton("导入 Excel...")
self.refresh_button = QPushButton("刷新")
self.collect_button = QPushButton("采集旧标题/旧封面")
self.stop_collect_button = QPushButton("停止")
self.write_back_button = QPushButton("回写旧数据到 Excel")
self.stop_collect_button.setEnabled(False)
self.batch_filter = QComboBox()
self.batch_filter.setObjectName("collectBatchFilter")
self.shop_filter = QComboBox()
self.shop_filter.setObjectName("collectShopFilter")
self.item_filter = QLineEdit()
self.item_filter.setObjectName("collectItemFilter")
self.item_filter.setPlaceholderText("商品ID")
self.status_filter = QComboBox()
self.status_filter.setObjectName("collectStatusFilter")
for label, value in self.STATUS_FILTERS:
self.status_filter.addItem(label, value)
self.delete_batch_button = QPushButton("删除批次")
self.delete_batch_button.setObjectName("deleteBatchButton")
self.delete_batch_button.setStyleSheet(_danger_outline_button_style("deleteBatchButton"))
self.delete_batch_button.setEnabled(False)
toolbar = QHBoxLayout()
toolbar.addWidget(self.import_button)
toolbar.addWidget(self.refresh_button)
toolbar.addWidget(self.collect_button)
toolbar.addWidget(self.stop_collect_button)
toolbar.addWidget(self.write_back_button)
toolbar.addStretch(1)
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("批次"))
filter_layout.addWidget(self.batch_filter, 2)
filter_layout.addWidget(QLabel("店铺"))
filter_layout.addWidget(self.shop_filter, 1)
filter_layout.addWidget(QLabel("商品ID"))
filter_layout.addWidget(self.item_filter, 1)
filter_layout.addWidget(QLabel("状态"))
filter_layout.addWidget(self.status_filter, 1)
filter_layout.addWidget(self.delete_batch_button)
self.summary_label = QLabel("未导入任务")
self.summary_label.setTextFormat(Qt.RichText)
self.batch_progress_label = _build_batch_progress_overview("collectBatchProgressOverview")
self.match_detail_label = QLabel("")
self.show_all_button = QPushButton("全部")
self.show_unmatched_button = QPushButton("未匹配(0)")
self.show_unmatched_button.setObjectName("showUnmatchedButton")
summary_layout = QHBoxLayout()
summary_layout.addWidget(self.summary_label)
summary_layout.addStretch(1)
summary_layout.addWidget(self.show_all_button)
summary_layout.addWidget(self.show_unmatched_button)
self.table = QTableView()
self.model = TaskTableModel(self.table)
self.table.setModel(self.model)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table.verticalHeader().setVisible(False)
self.run_log_view = QPlainTextEdit()
self.run_log_view.setObjectName("collectRunLogView")
self.run_log_view.setReadOnly(True)
self.run_log_view.setMaximumHeight(128)
self.run_log_view.setPlaceholderText("采集运行日志")
self.empty_label = QLabel("")
(
self.empty_state_card,
self.empty_state_label,
self.empty_state_button,
) = _build_empty_state_card("collectEmptyStateCard")
if self.open_accounts_callback is not None:
self.empty_state_button.clicked.connect(self.open_accounts_callback)
layout = QVBoxLayout(self)
layout.setContentsMargins(18, 18, 18, 18)
layout.addLayout(toolbar)
layout.addLayout(filter_layout)
layout.addLayout(summary_layout)
layout.addWidget(self.match_detail_label)
layout.addWidget(self.batch_progress_label)
layout.addWidget(self.empty_state_card)
layout.addWidget(self.table, 1)
layout.addWidget(QLabel("采集运行日志"))
layout.addWidget(self.run_log_view)
layout.addWidget(self.empty_label)
self.import_button.clicked.connect(self.import_excel)
self.refresh_button.clicked.connect(self.refresh_tasks)
self.batch_filter.currentIndexChanged.connect(self.refresh_tasks)
self.shop_filter.currentIndexChanged.connect(self.refresh_tasks)
self.item_filter.textChanged.connect(self.refresh_tasks)
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.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)
self.show_unmatched_button.clicked.connect(self.show_unmatched_tasks)
self.refresh_tasks()
self._load_latest_collect_run_log()
def _set_status(self, message):
if self.status_callback is not None:
self.status_callback(message)
def _on_collect_log(self, message):
self._append_collect_log(message)
self._set_status(message)
def _append_collect_log(self, message):
self.run_log_view.appendPlainText(str(message))
def _load_latest_collect_run_log(self):
try:
logs = db.list_run_logs(limit=1, run_type="collect", path=self.db_path)
if not logs:
return
events = db.list_run_log_events(logs[0].id, limit=30, path=self.db_path)
except Exception:
return
lines = [
f"{event.created_at} [{event.level}] {event.message}"
for event in reversed(events)
]
self.run_log_view.setPlainText("\n".join(lines))
scroll_bar = self.run_log_view.verticalScrollBar()
scroll_bar.setValue(scroll_bar.maximum())
def _log_collect_run_event(self, run_id, message, level="info"):
safe_message = diagnostics.redact_log_text(message)
try:
db.add_run_log_event(run_id, safe_message, level=level, path=self.db_path)
except Exception:
return
self._append_collect_log(safe_message)
def _show_error(self, message):
QMessageBox.warning(self, "导入采集", str(message))
self._set_status(str(message))
def _show_account_guide(self, message):
full_message = (
f"{message}\n\n"
"本轮采集已中止,不会自动打开账号 Chrome。\n"
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。"
)
QMessageBox.warning(self, "账号未就绪", full_message)
self._set_status(full_message.replace("\n", " "))
if self.open_accounts_callback is not None:
self.open_accounts_callback()
def _choose_excel_files(self):
files, _selected_filter = QFileDialog.getOpenFileNames(
self,
"选择 Excel 文件",
"",
"Excel 文件 (*.xlsx *.xlsm)",
)
return files
def import_excel(self, checked=False):
file_paths = self._choose_excel_files()
if not file_paths:
return
run_id = _safe_create_run_log(
"import",
db_path=self.db_path,
total=len(file_paths),
options={"files": file_paths},
)
started = time.monotonic()
_safe_add_run_log_event(
run_id,
f"step=select_files result=success detail=选择 Excel 文件 {len(file_paths)} 个",
db_path=self.db_path,
)
try:
_safe_add_run_log_event(
run_id,
"step=parse_file result=start detail=开始解析 Excel 并写入 SQLite",
db_path=self.db_path,
)
result = excel.import_tasks(file_paths, path=self.db_path)
except Exception as exc:
elapsed_ms = _elapsed_ms(started)
safe_error = diagnostics.redact_log_text(str(exc) or exc.__class__.__name__)
_safe_add_run_log_event(
run_id,
f"step=import result=failed detail={safe_error} elapsed_ms={elapsed_ms}",
db_path=self.db_path,
level="error",
)
_safe_write_diagnostic_log(
"Excel导入失败",
level="ERROR",
step="import",
elapsed_ms=elapsed_ms,
payload={"files": file_paths, "error": safe_error},
exc=exc,
log_dir=diagnostics.DEFAULT_LOG_DIR,
)
_safe_finish_run_log(
run_id,
db_path=self.db_path,
status="failed",
done=0,
failed_count=1,
summary_json={"ok": False, "error": safe_error, "elapsed_ms": elapsed_ms},
)
self._show_error(safe_error)
return
elapsed_ms = _elapsed_ms(started)
self.has_import_result = True
self.last_import_stats = result.get("stats") or {}
self.current_batch_id = result.get("batch_id")
file_errors = self.last_import_stats.get("file_errors") or []
row_errors = self.last_import_stats.get("row_errors") or []
for item in file_errors:
missing = ",".join(item.get("missing_columns") or [])
detail = "file={file} sheet={sheet} error={error}{missing}".format(
file=os.path.basename(str(item.get("file") or "")),
sheet=item.get("sheet") or "",
error=item.get("error") or "",
missing=f" missing={missing}" if missing else "",
)
_safe_add_run_log_event(
run_id,
f"step=parse_file result=failed detail={detail}",
db_path=self.db_path,
level="error",
)
for item in row_errors:
detail = "file={file} sheet={sheet} row={row} error={error}".format(
file=os.path.basename(str(item.get("file") or "")),
sheet=item.get("sheet") or "",
row=item.get("row") or "",
error=item.get("error") or "",
)
_safe_add_run_log_event(
run_id,
f"step=row_validate result=failed detail={detail}",
db_path=self.db_path,
level="warning",
)
_safe_add_run_log_event(
run_id,
"step=db_insert result=success detail=batch_id={batch_id} files={files} total={total} valid={valid} invalid={invalid} inserted={inserted} elapsed_ms={elapsed_ms}".format(
batch_id=self.current_batch_id or "",
files=self.last_import_stats.get("files", 0),
total=self.last_import_stats.get("total", 0),
valid=self.last_import_stats.get("valid", 0),
invalid=self.last_import_stats.get("invalid", 0),
inserted=self.last_import_stats.get("inserted", 0),
elapsed_ms=elapsed_ms,
),
db_path=self.db_path,
)
_safe_finish_run_log(
run_id,
db_path=self.db_path,
status="done",
done=self.last_import_stats.get("files", 0),
success_count=self.last_import_stats.get("inserted", 0),
failed_count=len(file_errors) + len(row_errors),
summary_json={
"ok": True,
"batch_id": self.current_batch_id,
"stats": self.last_import_stats,
"elapsed_ms": elapsed_ms,
},
)
self.refresh_tasks()
self._set_status(
"导入完成:有效{valid},无效{invalid},入库{inserted},未匹配{unmatched}".format(
valid=self.last_import_stats.get("valid", 0),
invalid=self.last_import_stats.get("invalid", 0),
inserted=self.last_import_stats.get("inserted", 0),
unmatched=self.model.unmatched_count(),
)
)
def refresh_tasks(self, checked=False):
try:
db.init_db(self.db_path)
batches = db.list_batches(path=self.db_path)
selected_batch = self.batch_filter.currentData()
selected_shop = self.shop_filter.currentData()
selected_status = self.status_filter.currentData() or "all"
item_query = self.item_filter.text().strip()
if self.current_batch_id and self.batch_filter.findData(self.current_batch_id) < 0:
selected_batch = self.current_batch_id
self._populate_batch_filter(batches, selected_batch)
selected_batch = self.batch_filter.currentData()
self.current_batch_id = selected_batch
task_rows = db.list_tasks(batch_id=selected_batch, path=self.db_path)
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
self._populate_shop_filter(task_rows, account_rows, selected_shop)
selected_shop = self.shop_filter.currentData()
filtered_rows = [
task for task in task_rows
if self._matches_shop(task, selected_shop)
and self._matches_item(task, item_query)
and self._matches_status(task, selected_status, account_rows)
]
except Exception as exc:
self.model.set_tasks([], [])
self.empty_label.setText("任务读取失败")
_set_batch_progress_overview(self.batch_progress_label, [])
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
self._set_status(f"任务读取失败:{exc}")
return
self.model.set_tasks(filtered_rows, account_rows)
self._update_summary(task_rows, account_rows)
_set_batch_progress_overview(self.batch_progress_label, task_rows)
self._update_empty_state(task_rows, account_rows)
self._update_delete_batch_button()
def _populate_batch_filter(self, batches, selected_batch):
batch_ids = {batch.id for batch in batches}
previous = selected_batch if selected_batch in batch_ids else None
self.batch_filter.blockSignals(True)
self.batch_filter.clear()
self.batch_filter.addItem("全部批次", None)
for batch in batches:
self.batch_filter.addItem(self._batch_label(batch), batch.id)
index = self.batch_filter.findData(previous)
self.batch_filter.setCurrentIndex(index if index >= 0 else 0)
self.batch_filter.blockSignals(False)
def _batch_label(self, batch):
source_files = batch.source_files
first_file = os.path.basename(source_files[0]) if source_files else batch.id
return f"{batch.created_at} · {first_file}"
def _populate_shop_filter(self, task_rows, account_rows, selected_shop):
aliases = {str(task.alias).strip() for task in task_rows if str(task.alias).strip()}
previous = selected_shop if selected_shop in aliases else None
account_by_alias = {
str(account.alias).strip(): account
for account in account_rows
if str(account.alias).strip()
}
self.shop_filter.blockSignals(True)
self.shop_filter.clear()
self.shop_filter.addItem("全部店铺", None)
for alias in sorted(aliases):
self.shop_filter.addItem(self._shop_label(alias, account_by_alias), alias)
index = self.shop_filter.findData(previous)
self.shop_filter.setCurrentIndex(index if index >= 0 else 0)
self.shop_filter.blockSignals(False)
def _shop_label(self, alias, account_by_alias):
account = account_by_alias.get(alias)
if account is not None:
return f"{account.account_name} ({alias})"
return alias
def _matches_shop(self, task, selected_shop):
return selected_shop is None or str(task.alias).strip() == selected_shop
def _matches_item(self, task, item_query):
if not item_query:
return True
return item_query in str(getattr(task, "item_id", ""))
def _matches_status(self, task, selected_status, account_rows):
if selected_status in (None, "all"):
return True
if selected_status == "to_collect":
return task.stage == "imported" and task.status in {"pending", "success"}
if selected_status in {"collected", "generated", "applied"}:
return task.stage == selected_status
if selected_status == "failed":
return task.status == "failed"
if selected_status == "skipped":
return task.status == "skipped" or self._is_unmatched_task(task, account_rows)
return True
def _is_unmatched_task(self, task, account_rows):
aliases = {
str(account.alias).strip()
for account in account_rows
if str(account.alias).strip()
}
return str(task.alias).strip() not in aliases
def _selected_batch_id(self):
return self.batch_filter.currentData()
def _update_delete_batch_button(self):
running = bool(self.collect_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):
batch_id = self._selected_batch_id()
if not batch_id:
self._set_status("请先选择一个具体批次")
return
batch = db.get_batch(batch_id, path=self.db_path)
if batch is None:
self._set_status("批次不存在或已删除")
self.current_batch_id = None
self.refresh_tasks()
return
tasks = db.list_tasks(batch_id=batch_id, path=self.db_path)
committed_count = sum(1 for task in tasks if int(getattr(task, "committed", 0) or 0) == 1)
lines = [
f"确定要软删除批次 {self._batch_label(batch)} 吗?",
f"任务数:{len(tasks)}",
f"已提交线上:{committed_count}",
"",
"软删除后,该批次不会再出现在①/②/③页面、筛选、采集、生成、更新或回写入口中。",
"软删除只隐藏本地批次,不会回滚蝦皮线上修改,不删除原始 Excel,也不删除本地图片。",
]
answer = QMessageBox.question(
self,
"删除批次",
"\n".join(lines),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if answer != QMessageBox.Yes:
self._set_status("已取消删除批次")
return
try:
result = db.delete_batch(batch_id, reason="用户在导入采集页软删除", path=self.db_path)
except Exception as exc:
QMessageBox.warning(self, "删除批次", str(exc))
self._set_status(f"删除批次失败:{exc}")
return
self.current_batch_id = None
self.has_import_result = False
self.refresh_tasks()
if self.refresh_workflow_callback is not None:
self.refresh_workflow_callback()
message = "已软删除批次:任务{task_count},已提交线上{committed_count}".format(
task_count=result.get("task_count", 0),
committed_count=result.get("committed_count", 0),
)
self._set_status(message)
QMessageBox.information(self, "删除批次", message)
def collect_old_data(self, checked=False):
tasks = list(self.model.tasks)
if not tasks:
self._set_status("没有可采集任务")
return
worker = CollectWorker(
tasks,
db_path=self.db_path,
config=self.config,
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
)
worker.progress.connect(self._on_collect_progress)
worker.row_updated.connect(self._on_collect_row_updated)
worker.log.connect(self._on_collect_log)
worker.failed.connect(self._on_collect_failed)
worker.finished.connect(self._on_collect_finished)
worker.cancelled.connect(self._on_collect_cancelled)
self.run_log_view.clear()
thread = run_worker(worker, thread_name="CollectWorker", start=False)
thread.finished.connect(lambda: self._forget_collect_thread(thread))
self.collect_worker = worker
self.collect_thread = thread
self._set_collect_running(True)
thread.start()
def stop_collect(self, checked=False):
if self.collect_worker is not None:
self.collect_worker.cancel()
self._set_status("正在停止采集...")
def write_back_old_data(self, checked=False):
batch_id = self._active_batch_id()
if not batch_id:
self._set_status("没有可回写批次")
return
self._start_write_back(batch_id)
def _start_write_back(self, batch_id, auto=False):
if self.write_back_thread is not None:
self._set_status("Excel 回写正在进行...")
return False
worker = WriteBackWorker(
batch_id,
db_path=self.db_path,
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
)
worker.failed.connect(
lambda task_id, error, auto=auto: self._on_write_back_failed(
task_id,
error,
auto=auto,
)
)
worker.finished.connect(
lambda payload, auto=auto: self._on_write_back_finished(
payload,
auto=auto,
)
)
thread = run_worker(worker, thread_name="WriteBackWorker", start=False)
thread.finished.connect(lambda: self._forget_write_back_thread(thread))
self.write_back_worker = worker
self.write_back_thread = thread
self._set_write_back_running(True)
self._set_status("正在自动回写旧数据到 Excel..." if auto else "正在回写旧数据到 Excel...")
thread.start()
return True
def _active_batch_id(self):
if self.current_batch_id:
return self.current_batch_id
batch_ids = {
task.batch_id
for task in self.model.all_tasks
if getattr(task, "batch_id", None)
}
if len(batch_ids) == 1:
return next(iter(batch_ids))
return None
def _set_collect_running(self, running):
self.import_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.collect_button.setEnabled(not running)
self.write_back_button.setEnabled(not running)
self.stop_collect_button.setEnabled(running)
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
self.item_filter.setEnabled(not running)
self.status_filter.setEnabled(not running)
self._update_delete_batch_button()
def _set_write_back_running(self, running):
self.import_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.collect_button.setEnabled(not running)
self.write_back_button.setEnabled(not running)
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
self.item_filter.setEnabled(not running)
self.status_filter.setEnabled(not running)
self._update_delete_batch_button()
def _forget_collect_thread(self, thread):
if self.collect_thread is thread:
self.collect_thread = None
self.collect_worker = None
def _forget_write_back_thread(self, thread):
if self.write_back_thread is thread:
self.write_back_thread = None
self.write_back_worker = None
def _on_collect_progress(self, payload):
self._set_status(
"采集进度:{done}/{total},成功{collected},略过{skipped},失败{failed}".format(
done=payload.get("done", 0),
total=payload.get("total", 0),
collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0),
)
)
def _on_collect_row_updated(self, task_id, fields):
self.refresh_tasks()
def _on_collect_failed(self, task_id, error):
self._set_status(f"任务 {task_id} 采集失败:{error}")
def _on_collect_finished(self, payload):
self._set_collect_running(False)
self.last_collect_run_id = payload.get("run_id") or self.last_collect_run_id
self.refresh_tasks()
self._load_latest_collect_run_log()
if payload.get("blocked"):
self._show_collect_blocked(payload)
return
message = "采集完成:成功{collected},略过{skipped},失败{failed}".format(
collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0),
)
if payload.get("collected", 0) > 0:
batch_id = self._active_batch_id()
if batch_id and self._start_write_back(batch_id, auto=True):
if self.last_collect_run_id:
self._log_collect_run_event(
self.last_collect_run_id,
"step=excel_write_back result=start detail=采集成功后自动回写旧数据到 Excel",
)
self._set_status(f"{message},正在自动回写 Excel...")
return
if not batch_id:
self._set_status(f"{message},但没有可回写批次")
return
self._set_status(f"{message},Excel 回写已在进行")
return
self._set_status(message)
def _show_collect_blocked(self, payload):
lines = ["采集前检查未通过。"]
if payload.get("no_accounts"):
lines.append("当前没有配置账号。")
not_running = payload.get("not_running") or []
if not_running:
lines.append(
"以下账号 Chrome 未启动或调试端口不可访问:"
+ "、".join(self._account_label(item) for item in not_running)
)
logged_out = payload.get("logged_out") or []
if logged_out:
lines.append(
"以下账号未登录蝦皮:"
+ "、".join(self._account_label(item) for item in logged_out)
)
self._show_account_guide("\n".join(lines))
def _account_label(self, item):
if isinstance(item, dict):
name = item.get("account_name") or item.get("alias") or ""
alias = item.get("alias") or ""
reason = item.get("reason")
else:
name = getattr(item, "account_name", "") or getattr(item, "alias", "")
alias = getattr(item, "alias", "")
reason = getattr(item, "reason", None)
label = f"{name}({alias})" if alias and name != alias else (name or alias)
return f"{label}: {reason}" if reason else label
def _on_collect_cancelled(self, payload):
self._set_collect_running(False)
self.refresh_tasks()
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):
if auto:
message += "\n请关闭原 Excel 后点击「回写旧数据到 Excel」手动重试;SQLite 已保留采集结果,也可另存副本。"
else:
message += "\n请关闭原 Excel 后重试;SQLite 已保留采集结果,也可另存副本。"
QMessageBox.warning(self, "回写旧数据", message)
self._set_status(message.replace("\n", " "))
if auto and self.last_collect_run_id:
self._log_collect_run_event(
self.last_collect_run_id,
f"step=excel_write_back result=failed detail={error}",
level="error",
)
def _on_write_back_finished(self, payload, auto=False):
self._set_write_back_running(False)
if payload.get("ok") is False:
error = payload.get("error") or "未知错误"
retry_hint = ",可点击「回写旧数据到 Excel」手动重试" if auto else ""
self._set_status(f"Excel {'自动' if auto else ''}回写失败:{error}{retry_hint}")
if auto and self.last_collect_run_id:
self._log_collect_run_event(
self.last_collect_run_id,
f"step=excel_write_back result=failed detail={error}",
level="error",
)
return
self.refresh_tasks()
self._set_status(
"Excel {prefix}回写完成:文件{files},行{rows}".format(
prefix="自动" if auto else "",
files=payload.get("files", 0),
rows=payload.get("rows", 0),
)
)
if auto and self.last_collect_run_id:
self._log_collect_run_event(
self.last_collect_run_id,
"step=excel_write_back result=success detail=旧数据已回写 Excel",
)
def show_all_tasks(self, checked=False):
self.model.set_filter_mode("all")
self._update_empty_label(len(self.model.all_tasks))
def show_unmatched_tasks(self, checked=False):
self.model.set_filter_mode("unmatched")
self._update_empty_label(len(self.model.all_tasks))
def _update_summary(self, task_rows, account_rows):
stats = self.last_import_stats or {}
unmatched = self._unmatched_count(task_rows, account_rows)
matched = len(task_rows) - unmatched
files = stats.get("files", 0 if not task_rows else 1)
total = stats.get("total", len(task_rows))
valid = stats.get("valid", len(task_rows))
invalid = stats.get("invalid", 0)
invalid_text = _danger_metric_text(f"无效{invalid}", invalid > 0)
unmatched_text = _danger_metric_text(f"未匹配{unmatched}", unmatched > 0)
self.summary_label.setText(
f"{files} 文件 · {total} 行 · 有效{valid}/{invalid_text} · 匹配{matched} · {unmatched_text}"
)
self.match_detail_label.setText(self._match_detail(task_rows, account_rows))
self.show_unmatched_button.setText(f"未匹配({unmatched})")
self.show_unmatched_button.setEnabled(unmatched > 0)
self.show_unmatched_button.setStyleSheet(
_danger_outline_button_style("showUnmatchedButton") if unmatched > 0 else ""
)
if unmatched == 0 and self.model.filter_mode == "unmatched":
self.model.set_filter_mode("all")
def _match_detail(self, task_rows, account_rows):
account_by_alias = {
str(account.alias).strip(): account
for account in account_rows
if str(account.alias).strip()
}
counts = {}
for task in task_rows:
account = account_by_alias.get(str(task.alias).strip())
if account is None:
continue
name = account.account_name or account.alias
counts[name] = counts.get(name, 0) + 1
if not counts:
return "匹配明细:无"
parts = [f"{name}{count}" for name, count in sorted(counts.items())]
return "匹配明细:" + " · ".join(parts)
def _unmatched_count(self, task_rows, account_rows):
aliases = {
str(account.alias).strip()
for account in account_rows
if str(account.alias).strip()
}
return sum(1 for task in task_rows if str(task.alias).strip() not in aliases)
def _update_empty_state(self, task_rows, account_rows):
if not account_rows:
self.empty_label.setText("")
_set_empty_state(
self.empty_state_card,
self.empty_state_label,
self.empty_state_button,
"第一步:前往『④账号管理』配置并登录账号,再回到①导入 Excel。",
self.open_accounts_callback is not None,
)
return
if not task_rows:
self.empty_label.setText("")
_set_empty_state(
self.empty_state_card,
self.empty_state_label,
self.empty_state_button,
"还没有导入任务。请点击「导入 Excel...」导入待处理商品。",
)
return
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
self._update_empty_label(len(task_rows))
def _update_empty_label(self, total_rows):
if total_rows == 0:
self.empty_label.setText("暂无任务")
return
if self.model.rowCount() == 0 and self.model.filter_mode == "unmatched":
self.empty_label.setText("当前筛选没有未匹配任务")
return
if self.model.rowCount() == 0:
self.empty_label.setText("当前筛选没有匹配任务")
return
unmatched = self.model.unmatched_count()
self.empty_label.setText(
"" if unmatched == 0 else f"{unmatched} 条任务别名未匹配账号,阶段显示为“略过”"
)