2026-07-02 16:47:37 +08:00
|
|
|
|
"""Tab 3: Shopee update UI."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from ..models import ApplyTaskTableModel
|
|
|
|
|
|
from ..widgets import *
|
|
|
|
|
|
from ..workers import ApplyWorker as _RealApplyWorker, WriteBackWorker as _RealWriteBackWorker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ApplyWorker(*args, **kwargs):
|
|
|
|
|
|
return _call_package_attr("ApplyWorker", _RealApplyWorker, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def WriteBackWorker(*args, **kwargs):
|
|
|
|
|
|
return _call_package_attr("WriteBackWorker", _RealWriteBackWorker, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
class ApplyTab(QWidget):
|
|
|
|
|
|
"""Tab 3: list generated tasks and confirm the update scope."""
|
|
|
|
|
|
|
|
|
|
|
|
STATUS_FILTERS = [
|
|
|
|
|
|
("已生成", "generated"),
|
|
|
|
|
|
("失败", "failed"),
|
|
|
|
|
|
("已更新", "applied"),
|
|
|
|
|
|
("略过", "skipped"),
|
|
|
|
|
|
("全部状态", "all"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
parent=None,
|
|
|
|
|
|
db_path=None,
|
|
|
|
|
|
config=None,
|
|
|
|
|
|
status_callback=None,
|
|
|
|
|
|
open_accounts_callback=None,
|
|
|
|
|
|
open_settings_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.open_settings_callback = open_settings_callback
|
|
|
|
|
|
self.apply_worker = None
|
|
|
|
|
|
self.apply_thread = None
|
|
|
|
|
|
self.result_write_back_worker = None
|
|
|
|
|
|
self.result_write_back_thread = None
|
|
|
|
|
|
self.last_apply_summary = None
|
|
|
|
|
|
|
|
|
|
|
|
self.batch_filter = QComboBox()
|
|
|
|
|
|
self.batch_filter.setObjectName("applyBatchFilter")
|
|
|
|
|
|
self.shop_filter = QComboBox()
|
|
|
|
|
|
self.shop_filter.setObjectName("applyShopFilter")
|
|
|
|
|
|
self.item_filter = QLineEdit()
|
|
|
|
|
|
self.item_filter.setObjectName("applyItemFilter")
|
|
|
|
|
|
self.item_filter.setPlaceholderText("商品ID")
|
|
|
|
|
|
self.status_filter = QComboBox()
|
|
|
|
|
|
self.status_filter.setObjectName("applyStatusFilter")
|
|
|
|
|
|
for label, value in self.STATUS_FILTERS:
|
|
|
|
|
|
self.status_filter.addItem(label, value)
|
|
|
|
|
|
self.refresh_button = QPushButton("刷新")
|
|
|
|
|
|
|
|
|
|
|
|
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.refresh_button)
|
|
|
|
|
|
|
|
|
|
|
|
self.summary_label = QLabel("任务 0 条")
|
|
|
|
|
|
self.batch_progress_label = _build_batch_progress_overview("applyBatchProgressOverview")
|
|
|
|
|
|
self.risk_label = QLabel("可先点击「检查本轮更新」确认当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
|
|
|
|
|
|
(
|
|
|
|
|
|
self.empty_state_card,
|
|
|
|
|
|
self.empty_state_label,
|
|
|
|
|
|
self.empty_state_button,
|
|
|
|
|
|
) = _build_empty_state_card("applyEmptyStateCard")
|
|
|
|
|
|
if self.open_accounts_callback is not None:
|
|
|
|
|
|
self.empty_state_button.clicked.connect(self.open_accounts_callback)
|
|
|
|
|
|
self.task_table = QTableView()
|
|
|
|
|
|
self.model = ApplyTaskTableModel(self.task_table)
|
|
|
|
|
|
self.task_table.setModel(self.model)
|
|
|
|
|
|
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
|
|
|
|
self.task_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
|
|
|
|
self.task_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
|
|
|
|
self.task_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
|
|
|
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
|
|
|
|
self.task_table.verticalHeader().setVisible(False)
|
|
|
|
|
|
self.run_log_view = QPlainTextEdit()
|
|
|
|
|
|
self.run_log_view.setObjectName("applyRunLogView")
|
|
|
|
|
|
self.run_log_view.setReadOnly(True)
|
|
|
|
|
|
self.run_log_view.setMaximumHeight(128)
|
|
|
|
|
|
self.run_log_view.setPlaceholderText("运行日志")
|
|
|
|
|
|
|
|
|
|
|
|
self.preview_update_button = QPushButton("检查本轮更新")
|
|
|
|
|
|
self.preview_update_button.setObjectName("previewUpdateButton")
|
|
|
|
|
|
self.start_update_button = QPushButton("开始更新")
|
|
|
|
|
|
self.start_update_button.setObjectName("startUpdateButton")
|
|
|
|
|
|
self.start_update_button.setMinimumWidth(118)
|
|
|
|
|
|
self.start_update_button.setStyleSheet(
|
2026-07-07 09:21:19 +08:00
|
|
|
|
_warning_outline_button_style("startUpdateButton")
|
2026-07-02 16:47:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
self.stop_update_button = QPushButton("停止")
|
|
|
|
|
|
self.reset_update_button = QPushButton("重置更新状态")
|
|
|
|
|
|
self.reset_update_button.setObjectName("resetUpdateButton")
|
|
|
|
|
|
self.reset_update_button.setVisible(False)
|
|
|
|
|
|
self.write_back_button = QPushButton("回写结果到 Excel")
|
|
|
|
|
|
self.stop_update_button.setEnabled(False)
|
|
|
|
|
|
self.write_back_button.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
action_layout = QHBoxLayout()
|
|
|
|
|
|
action_layout.addWidget(self.preview_update_button)
|
|
|
|
|
|
action_layout.addWidget(self.start_update_button)
|
|
|
|
|
|
action_layout.addWidget(self.stop_update_button)
|
|
|
|
|
|
action_layout.addStretch(1)
|
|
|
|
|
|
action_layout.addWidget(self.write_back_button)
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(18, 18, 18, 18)
|
|
|
|
|
|
layout.addLayout(filter_layout)
|
|
|
|
|
|
layout.addWidget(self.risk_label)
|
|
|
|
|
|
layout.addWidget(self.summary_label)
|
|
|
|
|
|
layout.addWidget(self.batch_progress_label)
|
|
|
|
|
|
layout.addWidget(self.empty_state_card)
|
|
|
|
|
|
layout.addWidget(self.task_table, 1)
|
|
|
|
|
|
layout.addWidget(QLabel("运行日志"))
|
|
|
|
|
|
layout.addWidget(self.run_log_view)
|
|
|
|
|
|
layout.addLayout(action_layout)
|
|
|
|
|
|
|
|
|
|
|
|
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.refresh_button.clicked.connect(self.refresh_tasks)
|
|
|
|
|
|
self.preview_update_button.clicked.connect(self.preview_update)
|
|
|
|
|
|
self.start_update_button.clicked.connect(self.start_update)
|
|
|
|
|
|
self.stop_update_button.clicked.connect(self.stop_update)
|
|
|
|
|
|
self.reset_update_button.clicked.connect(self.reset_apply_status)
|
|
|
|
|
|
self.task_table.customContextMenuRequested.connect(self.show_task_context_menu)
|
|
|
|
|
|
self.write_back_button.clicked.connect(self.write_back_results)
|
|
|
|
|
|
|
|
|
|
|
|
self.refresh_tasks()
|
2026-07-02 17:32:51 +08:00
|
|
|
|
self._show_current_run_log_empty()
|
2026-07-02 16:47:37 +08:00
|
|
|
|
|
|
|
|
|
|
def _set_status(self, message):
|
|
|
|
|
|
if self.status_callback is not None:
|
|
|
|
|
|
self.status_callback(message)
|
|
|
|
|
|
|
|
|
|
|
|
def refresh_tasks(self, checked=False):
|
|
|
|
|
|
try:
|
|
|
|
|
|
db.init_db(self.db_path)
|
|
|
|
|
|
batches = db.list_batches(path=self.db_path)
|
|
|
|
|
|
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
|
|
|
|
|
selected_batch = self.batch_filter.currentData()
|
|
|
|
|
|
selected_shop = self.shop_filter.currentData()
|
|
|
|
|
|
selected_status = self.status_filter.currentData() or "generated"
|
|
|
|
|
|
item_query = self.item_filter.text().strip()
|
|
|
|
|
|
self._populate_batch_filter(batches, selected_batch)
|
|
|
|
|
|
selected_batch = self.batch_filter.currentData()
|
|
|
|
|
|
all_batch_tasks = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
|
|
|
|
|
batch_tasks = [
|
|
|
|
|
|
task for task in all_batch_tasks
|
|
|
|
|
|
if self._is_update_task(task)
|
|
|
|
|
|
]
|
|
|
|
|
|
self._populate_shop_filter(batch_tasks, account_rows, selected_shop)
|
|
|
|
|
|
selected_shop = self.shop_filter.currentData()
|
|
|
|
|
|
filtered_tasks = [
|
|
|
|
|
|
task for task in batch_tasks
|
|
|
|
|
|
if self._matches_shop(task, selected_shop)
|
|
|
|
|
|
and self._matches_item(task, item_query)
|
|
|
|
|
|
and self._matches_status(task, selected_status)
|
|
|
|
|
|
]
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
self.model.set_tasks([], [])
|
|
|
|
|
|
self.summary_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_tasks, account_rows)
|
|
|
|
|
|
self.summary_label.setText(
|
|
|
|
|
|
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
|
|
|
|
|
)
|
|
|
|
|
|
_set_batch_progress_overview(self.batch_progress_label, all_batch_tasks)
|
|
|
|
|
|
self._update_empty_state(batch_tasks, filtered_tasks, account_rows)
|
|
|
|
|
|
self._update_write_back_button()
|
|
|
|
|
|
|
|
|
|
|
|
def _update_empty_state(self, batch_tasks, filtered_tasks, account_rows):
|
|
|
|
|
|
if not account_rows:
|
|
|
|
|
|
_set_empty_state(
|
|
|
|
|
|
self.empty_state_card,
|
|
|
|
|
|
self.empty_state_label,
|
|
|
|
|
|
self.empty_state_button,
|
|
|
|
|
|
"第一步:前往『④账号管理』配置并登录账号,再回到③更新 Shopee。",
|
|
|
|
|
|
self.open_accounts_callback is not None,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if not batch_tasks:
|
|
|
|
|
|
_set_empty_state(
|
|
|
|
|
|
self.empty_state_card,
|
|
|
|
|
|
self.empty_state_label,
|
|
|
|
|
|
self.empty_state_button,
|
|
|
|
|
|
"还没有可更新任务。请先在②AI生成完成新标题或新封面。",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if not filtered_tasks:
|
|
|
|
|
|
_set_empty_state(
|
|
|
|
|
|
self.empty_state_card,
|
|
|
|
|
|
self.empty_state_label,
|
|
|
|
|
|
self.empty_state_button,
|
|
|
|
|
|
"当前筛选没有可更新任务,请调整批次、店铺、商品ID或状态筛选。",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
|
|
|
|
|
|
|
|
|
|
|
def start_update(self, checked=False):
|
|
|
|
|
|
self._start_update(dry_run=False)
|
|
|
|
|
|
|
|
|
|
|
|
def preview_update(self, checked=False):
|
|
|
|
|
|
self._start_update(dry_run=True)
|
|
|
|
|
|
|
|
|
|
|
|
def _start_update(self, dry_run=False):
|
|
|
|
|
|
if self.apply_thread is not None:
|
|
|
|
|
|
self._set_status("更新正在进行...")
|
|
|
|
|
|
return
|
|
|
|
|
|
tasks = [
|
|
|
|
|
|
task for task in self.model.tasks
|
|
|
|
|
|
if self._is_actionable_task(task)
|
|
|
|
|
|
]
|
|
|
|
|
|
if not tasks:
|
|
|
|
|
|
self._set_status("当前筛选结果没有可更新任务")
|
|
|
|
|
|
return
|
|
|
|
|
|
update_cfg = self._shopee_update_config()
|
|
|
|
|
|
dry_run = bool(dry_run)
|
|
|
|
|
|
safety_error = self._update_safety_error(tasks, dry_run=dry_run)
|
|
|
|
|
|
if safety_error:
|
|
|
|
|
|
self._show_update_safety_error(safety_error)
|
|
|
|
|
|
self._set_status(safety_error.replace("\n", " "))
|
|
|
|
|
|
return
|
|
|
|
|
|
answer = QMessageBox.question(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"确认检查本轮更新" if dry_run else "确认开始更新",
|
|
|
|
|
|
self._confirmation_message(tasks, dry_run=dry_run),
|
|
|
|
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
|
|
|
|
QMessageBox.No,
|
|
|
|
|
|
)
|
|
|
|
|
|
if answer != QMessageBox.Yes:
|
|
|
|
|
|
self._set_status("已取消检查本轮更新" if dry_run else "已取消开始更新")
|
|
|
|
|
|
return
|
|
|
|
|
|
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
|
|
|
|
|
worker = ApplyWorker(
|
|
|
|
|
|
tasks,
|
|
|
|
|
|
db_path=self.db_path,
|
|
|
|
|
|
config=self.config,
|
|
|
|
|
|
close_success_tab=bool(update_cfg.get("close_success_tab", False)),
|
|
|
|
|
|
dry_run=dry_run,
|
|
|
|
|
|
parallel_accounts=bool(update_cfg.get("parallel_accounts", False)),
|
|
|
|
|
|
max_parallel_accounts=max(
|
|
|
|
|
|
1,
|
|
|
|
|
|
int(update_cfg.get("max_parallel_accounts", 1) or 1),
|
|
|
|
|
|
),
|
|
|
|
|
|
batch_size=batch_size,
|
|
|
|
|
|
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
|
|
|
|
|
)
|
|
|
|
|
|
worker.progress.connect(self._on_apply_progress)
|
|
|
|
|
|
worker.row_updated.connect(self._on_apply_row_updated)
|
|
|
|
|
|
worker.log.connect(self._on_apply_log)
|
|
|
|
|
|
worker.failed.connect(self._on_apply_failed)
|
|
|
|
|
|
worker.finished.connect(self._on_apply_finished)
|
|
|
|
|
|
worker.cancelled.connect(self._on_apply_cancelled)
|
|
|
|
|
|
thread = run_worker(worker, thread_name="ApplyWorker", start=False)
|
|
|
|
|
|
thread.finished.connect(lambda: self._forget_apply_thread(thread))
|
|
|
|
|
|
self.apply_worker = worker
|
|
|
|
|
|
self.apply_thread = thread
|
|
|
|
|
|
self._set_apply_running(True)
|
2026-07-02 17:32:51 +08:00
|
|
|
|
self._reset_run_log(tasks, dry_run=dry_run, batch_size=batch_size)
|
2026-07-02 16:47:37 +08:00
|
|
|
|
if dry_run:
|
|
|
|
|
|
self._set_status(f"开始检查本轮更新:{len(tasks)} 条")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._set_status(f"开始更新:{len(tasks)} 条,按每批最多 {batch_size} 条执行")
|
|
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def stop_update(self, checked=False):
|
|
|
|
|
|
if self.apply_worker is not None:
|
|
|
|
|
|
self.apply_worker.cancel()
|
|
|
|
|
|
self._set_status("正在停止更新...")
|
|
|
|
|
|
|
|
|
|
|
|
def write_back_results(self, checked=False):
|
|
|
|
|
|
batch_ids = self._active_batch_ids()
|
|
|
|
|
|
if not batch_ids:
|
|
|
|
|
|
self._set_status("没有可回写结果的批次")
|
|
|
|
|
|
return
|
|
|
|
|
|
self._start_result_write_back(batch_ids, auto=False)
|
|
|
|
|
|
|
|
|
|
|
|
def show_task_context_menu(self, position):
|
|
|
|
|
|
index = self.task_table.indexAt(position)
|
|
|
|
|
|
if index.isValid():
|
|
|
|
|
|
self.task_table.setCurrentIndex(index)
|
|
|
|
|
|
menu = QMenu(self)
|
|
|
|
|
|
reset_action = menu.addAction("重置更新状态")
|
|
|
|
|
|
reset_action.setEnabled(
|
|
|
|
|
|
self.apply_thread is None
|
|
|
|
|
|
and self.result_write_back_thread is None
|
|
|
|
|
|
and self._selected_task() is not None
|
|
|
|
|
|
)
|
|
|
|
|
|
reset_action.triggered.connect(self.reset_apply_status)
|
|
|
|
|
|
menu.exec(self.task_table.viewport().mapToGlobal(position))
|
|
|
|
|
|
|
|
|
|
|
|
def reset_apply_status(self, checked=False):
|
|
|
|
|
|
if self.apply_thread is not None or self.result_write_back_thread is not None:
|
|
|
|
|
|
self._set_status("更新或回写正在进行,不能重置")
|
|
|
|
|
|
return
|
|
|
|
|
|
task = self._selected_task()
|
|
|
|
|
|
if task is None:
|
|
|
|
|
|
self._set_status("请选择要重置更新状态的任务")
|
|
|
|
|
|
return
|
|
|
|
|
|
if not (getattr(task, "new_title", None) or getattr(task, "new_cover_path", None)):
|
|
|
|
|
|
self._set_status("选中任务没有新标题或新封面,不能重置为可更新")
|
|
|
|
|
|
return
|
|
|
|
|
|
lines = [
|
|
|
|
|
|
"确定重置当前选中任务的本地更新状态吗?",
|
|
|
|
|
|
"",
|
|
|
|
|
|
f"商品ID:{task.item_id}",
|
|
|
|
|
|
f"店铺:{self.model.account_name_for(task)}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"将保留新标题和新封面路径,只把本地状态退回可更新。",
|
|
|
|
|
|
"不会触碰 Shopee,也不会自动回写 Excel。",
|
|
|
|
|
|
]
|
|
|
|
|
|
if getattr(task, "committed", 0):
|
|
|
|
|
|
lines.extend([
|
|
|
|
|
|
"",
|
|
|
|
|
|
"注意:该记录已经提交过线上。本地重置不会回滚 Shopee,重复更新会再次提交线上。",
|
|
|
|
|
|
])
|
|
|
|
|
|
answer = QMessageBox.question(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"重置更新状态",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
|
|
|
|
QMessageBox.No,
|
|
|
|
|
|
)
|
|
|
|
|
|
if answer != QMessageBox.Yes:
|
|
|
|
|
|
self._set_status("已取消重置更新状态")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
db.reset_apply_status(task.id, path=self.db_path)
|
|
|
|
|
|
message = (
|
|
|
|
|
|
"action=reset_apply_status step=db_write result=success "
|
|
|
|
|
|
f"detail=退回可更新 task_id={task.id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
run_id = _write_reset_run_log(self.db_path, task, "reset_apply_status", message)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "重置更新状态", str(exc))
|
|
|
|
|
|
self._set_status(f"重置更新状态失败:{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
self.refresh_tasks()
|
|
|
|
|
|
self._append_run_log(message)
|
|
|
|
|
|
self._set_status(
|
|
|
|
|
|
f"已重置更新状态:商品 {task.item_id},run_id={run_id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_task(self):
|
|
|
|
|
|
index = self.task_table.currentIndex()
|
|
|
|
|
|
if index.isValid():
|
|
|
|
|
|
return self.model.task_at(index.row())
|
|
|
|
|
|
if self.model.rowCount() > 0:
|
|
|
|
|
|
return self.model.task_at(0)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _is_actionable_task(self, task):
|
|
|
|
|
|
return (
|
|
|
|
|
|
getattr(task, "stage", None) == "generated"
|
|
|
|
|
|
and getattr(task, "status", None) in {"success", "pending", "failed"}
|
|
|
|
|
|
and bool(getattr(task, "new_title", None) or getattr(task, "new_cover_path", None))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _populate_batch_filter(self, batches, selected_batch):
|
|
|
|
|
|
previous = selected_batch if selected_batch in {batch.id for batch in batches} 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 _populate_shop_filter(self, tasks, account_rows, selected_shop):
|
|
|
|
|
|
account_by_alias = {
|
|
|
|
|
|
str(account.alias).strip(): account
|
|
|
|
|
|
for account in account_rows
|
|
|
|
|
|
if str(account.alias).strip()
|
|
|
|
|
|
}
|
|
|
|
|
|
aliases = []
|
|
|
|
|
|
for task in tasks:
|
|
|
|
|
|
alias = str(task.alias).strip()
|
|
|
|
|
|
if alias and alias not in aliases:
|
|
|
|
|
|
aliases.append(alias)
|
|
|
|
|
|
previous = selected_shop if selected_shop in aliases else None
|
|
|
|
|
|
self.shop_filter.blockSignals(True)
|
|
|
|
|
|
self.shop_filter.clear()
|
|
|
|
|
|
self.shop_filter.addItem("全部店铺", None)
|
|
|
|
|
|
for alias in sorted(aliases, key=lambda value: self._shop_label(value, account_by_alias)):
|
|
|
|
|
|
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 _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 _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 _status_label(self):
|
|
|
|
|
|
return self.status_filter.currentText() or "已生成"
|
|
|
|
|
|
|
|
|
|
|
|
def _batch_filter_label(self):
|
|
|
|
|
|
return self.batch_filter.currentText() or "全部批次"
|
|
|
|
|
|
|
|
|
|
|
|
def _shop_filter_label(self):
|
|
|
|
|
|
return self.shop_filter.currentText() or "全部店铺"
|
|
|
|
|
|
|
|
|
|
|
|
def _item_filter_label(self):
|
|
|
|
|
|
return self.item_filter.text().strip() or "全部商品"
|
|
|
|
|
|
|
|
|
|
|
|
def _is_update_task(self, task):
|
|
|
|
|
|
if task.stage in {"generated", "applied"}:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return bool((task.new_title or task.new_cover_path) and task.status in {"failed", "skipped"})
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
if selected_status in (None, "all"):
|
|
|
|
|
|
return True
|
|
|
|
|
|
if selected_status == "generated":
|
|
|
|
|
|
return task.stage == "generated" and task.status in {"success", "pending"}
|
|
|
|
|
|
if selected_status == "failed":
|
|
|
|
|
|
return task.status == "failed"
|
|
|
|
|
|
if selected_status == "applied":
|
|
|
|
|
|
return task.stage == "applied"
|
|
|
|
|
|
if selected_status == "skipped":
|
|
|
|
|
|
return task.status == "skipped"
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _confirmation_message(self, tasks, dry_run=False):
|
|
|
|
|
|
update_cfg = self._shopee_update_config()
|
|
|
|
|
|
cover_text = "允许" if update_cfg.get("allow_cover_update") else "不允许"
|
|
|
|
|
|
close_text = "是" if update_cfg.get("close_success_tab") else "否"
|
|
|
|
|
|
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
|
|
|
|
|
batch_count = (len(tasks) + batch_size - 1) // batch_size if tasks else 0
|
|
|
|
|
|
parallel_text = (
|
|
|
|
|
|
f"开启,最多 {update_cfg.get('max_parallel_accounts', 1)} 个账号"
|
|
|
|
|
|
if update_cfg.get("parallel_accounts")
|
|
|
|
|
|
else "关闭"
|
|
|
|
|
|
)
|
|
|
|
|
|
intro = (
|
|
|
|
|
|
"即将检查当前筛选结果。\n\n"
|
|
|
|
|
|
if dry_run
|
|
|
|
|
|
else "即将按当前筛选结果分批更新 Shopee 线上商品。\n\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
|
|
|
intro
|
|
|
|
|
|
+ f"批次:{self._batch_filter_label()}\n"
|
|
|
|
|
|
+ f"店铺:{self._shop_filter_label()}\n"
|
|
|
|
|
|
+ f"商品ID:{self._item_filter_label()}\n"
|
|
|
|
|
|
+ f"状态:{self._status_label()}\n"
|
|
|
|
|
|
+ f"任务数:{len(tasks)}\n"
|
|
|
|
|
|
+ f"预计批次:{batch_count}\n\n"
|
|
|
|
|
|
+ "安全设置:"
|
|
|
|
|
|
+ f"封面更新={cover_text},"
|
|
|
|
|
|
+ f"每批最大更新条数={batch_size},"
|
|
|
|
|
|
+ f"成功后关闭新页={close_text},"
|
|
|
|
|
|
+ f"多账号并行={parallel_text}\n\n"
|
|
|
|
|
|
+ (
|
|
|
|
|
|
"检查只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。"
|
|
|
|
|
|
if dry_run
|
|
|
|
|
|
else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_safety_error(self, tasks, dry_run=False):
|
|
|
|
|
|
update_cfg = self._shopee_update_config()
|
|
|
|
|
|
if dry_run:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if not update_cfg.get("allow_real_submit", False):
|
|
|
|
|
|
return (
|
|
|
|
|
|
"设置未开启「允许真实提交线上商品」,已阻止本次更新。\n"
|
|
|
|
|
|
"请到⑤设置 > Shopee 更新安全开启该开关后再开始更新。"
|
|
|
|
|
|
)
|
|
|
|
|
|
if not update_cfg.get("allow_cover_update", False):
|
|
|
|
|
|
cover_tasks = [
|
|
|
|
|
|
str(getattr(task, "item_id", ""))
|
|
|
|
|
|
for task in tasks
|
|
|
|
|
|
if getattr(task, "new_cover_path", None)
|
|
|
|
|
|
]
|
|
|
|
|
|
if cover_tasks:
|
|
|
|
|
|
return (
|
|
|
|
|
|
"设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。\n"
|
|
|
|
|
|
"请到⑤设置 > Shopee 更新安全开启该开关,或先筛掉含新封面的任务。"
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _show_update_safety_error(self, message):
|
|
|
|
|
|
box = QMessageBox(self)
|
|
|
|
|
|
box.setIcon(QMessageBox.Warning)
|
|
|
|
|
|
box.setWindowTitle("更新安全开关")
|
|
|
|
|
|
box.setText(str(message))
|
|
|
|
|
|
settings_button = None
|
|
|
|
|
|
if self.open_settings_callback is not None:
|
|
|
|
|
|
settings_button = box.addButton("前往设置", QMessageBox.ActionRole)
|
|
|
|
|
|
box.addButton(QMessageBox.Ok)
|
|
|
|
|
|
box.exec()
|
|
|
|
|
|
if settings_button is not None and box.clickedButton() is settings_button:
|
|
|
|
|
|
self.open_settings_callback()
|
|
|
|
|
|
|
|
|
|
|
|
def _shopee_update_config(self):
|
|
|
|
|
|
defaults = appconfig.default_config().get("shopee_update", {})
|
|
|
|
|
|
loaded = self.config.get("shopee_update", {})
|
|
|
|
|
|
if not isinstance(loaded, dict):
|
|
|
|
|
|
loaded = {}
|
|
|
|
|
|
merged = dict(defaults)
|
|
|
|
|
|
merged.update(loaded)
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
def _set_apply_running(self, running):
|
|
|
|
|
|
self.preview_update_button.setEnabled(not running)
|
|
|
|
|
|
self.start_update_button.setEnabled(not running)
|
|
|
|
|
|
self.stop_update_button.setEnabled(running)
|
|
|
|
|
|
self.reset_update_button.setEnabled(not running)
|
|
|
|
|
|
self.refresh_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_write_back_button()
|
|
|
|
|
|
|
|
|
|
|
|
def _set_result_write_back_running(self, running):
|
|
|
|
|
|
self.preview_update_button.setEnabled(not running)
|
|
|
|
|
|
self.start_update_button.setEnabled(not running)
|
|
|
|
|
|
self.reset_update_button.setEnabled(not running)
|
|
|
|
|
|
self.refresh_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.write_back_button.setEnabled(False if running else bool(self._active_batch_ids()))
|
|
|
|
|
|
|
|
|
|
|
|
def _forget_apply_thread(self, thread):
|
|
|
|
|
|
if self.apply_thread is thread:
|
|
|
|
|
|
self.apply_thread = None
|
|
|
|
|
|
self.apply_worker = None
|
|
|
|
|
|
|
|
|
|
|
|
def _forget_result_write_back_thread(self, thread):
|
|
|
|
|
|
if self.result_write_back_thread is thread:
|
|
|
|
|
|
self.result_write_back_thread = None
|
|
|
|
|
|
self.result_write_back_worker = None
|
|
|
|
|
|
self._update_write_back_button()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_progress(self, payload):
|
|
|
|
|
|
self._set_status("更新进度:" + self._apply_progress_text(payload))
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_log(self, message):
|
|
|
|
|
|
self._append_run_log(message)
|
|
|
|
|
|
self._set_status(message)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_row_updated(self, task_id, fields):
|
|
|
|
|
|
self.refresh_tasks()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_failed(self, task_id, error):
|
|
|
|
|
|
self._set_status(f"任务 {task_id} 更新失败:{error}")
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_finished(self, payload):
|
|
|
|
|
|
self._set_apply_running(False)
|
|
|
|
|
|
self.refresh_tasks()
|
|
|
|
|
|
if payload.get("blocked"):
|
|
|
|
|
|
self._show_apply_blocked(payload)
|
|
|
|
|
|
return
|
|
|
|
|
|
self.last_apply_summary = dict(payload)
|
|
|
|
|
|
prefix = "检查本轮更新完成:" if payload.get("dry_run") else "更新完成:"
|
|
|
|
|
|
message = prefix + self._apply_progress_text(payload)
|
|
|
|
|
|
batch_ids = payload.get("batch_ids") or self._active_batch_ids()
|
|
|
|
|
|
if (not payload.get("dry_run")) and payload.get("done", 0) > 0 and batch_ids:
|
|
|
|
|
|
if self._start_result_write_back(
|
|
|
|
|
|
batch_ids,
|
|
|
|
|
|
auto=True,
|
|
|
|
|
|
apply_summary=payload,
|
|
|
|
|
|
):
|
|
|
|
|
|
self._set_status(f"{message},正在自动回写结果到 Excel...")
|
|
|
|
|
|
return
|
|
|
|
|
|
self._set_status(message)
|
|
|
|
|
|
self._show_apply_summary(payload)
|
|
|
|
|
|
|
|
|
|
|
|
def _on_apply_cancelled(self, payload):
|
|
|
|
|
|
self._set_apply_running(False)
|
|
|
|
|
|
self.refresh_tasks()
|
|
|
|
|
|
self._set_status("更新已停止:" + self._apply_progress_text(payload))
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_progress_text(self, payload):
|
|
|
|
|
|
success_label = "可更新" if payload.get("dry_run") else "成功"
|
|
|
|
|
|
return "完成{done}/{total},{success_label}{applied},略过{skipped},失败{failed}".format(
|
|
|
|
|
|
done=payload.get("done", 0),
|
|
|
|
|
|
total=payload.get("total", 0),
|
|
|
|
|
|
success_label=success_label,
|
|
|
|
|
|
applied=payload.get("applied", 0),
|
|
|
|
|
|
skipped=payload.get("skipped", 0),
|
|
|
|
|
|
failed=payload.get("failed", 0),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _append_run_log(self, message):
|
|
|
|
|
|
self.run_log_view.appendPlainText(str(message))
|
2026-07-02 17:32:51 +08:00
|
|
|
|
scroll_bar = self.run_log_view.verticalScrollBar()
|
|
|
|
|
|
scroll_bar.setValue(scroll_bar.maximum())
|
|
|
|
|
|
|
|
|
|
|
|
def _show_current_run_log_empty(self):
|
|
|
|
|
|
self.run_log_view.setPlainText("本轮日志会在开始运行后显示")
|
|
|
|
|
|
|
|
|
|
|
|
def _reset_run_log(self, tasks, dry_run=False, batch_size=1):
|
|
|
|
|
|
self.run_log_view.clear()
|
|
|
|
|
|
action = "检查" if dry_run else "更新"
|
|
|
|
|
|
self._append_run_log(
|
|
|
|
|
|
f"本轮{action}开始:任务 {len(tasks)} 条,每批 {batch_size} 条"
|
|
|
|
|
|
)
|
2026-07-02 16:47:37 +08:00
|
|
|
|
|
|
|
|
|
|
def _load_latest_run_log(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
logs = db.list_run_logs(limit=1, run_type="apply", 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 _show_apply_blocked(self, payload):
|
|
|
|
|
|
lines = ["更新前检查未通过。"]
|
|
|
|
|
|
if payload.get("no_accounts"):
|
|
|
|
|
|
lines.append("当前没有配置账号。")
|
|
|
|
|
|
duplicate_ports = payload.get("duplicate_ports") or []
|
|
|
|
|
|
if duplicate_ports:
|
|
|
|
|
|
for item in duplicate_ports:
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
"以下账号调试端口冲突:端口 {port} -> {aliases}".format(
|
|
|
|
|
|
port=item.get("debug_port"),
|
|
|
|
|
|
aliases="、".join(item.get("aliases") or []),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
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(
|
|
|
|
|
|
"以下账号未登录 Shopee:"
|
|
|
|
|
|
+ "、".join(self._account_label(item) for item in logged_out)
|
|
|
|
|
|
)
|
|
|
|
|
|
self._show_account_guide("\n".join(lines))
|
|
|
|
|
|
|
|
|
|
|
|
def _show_account_guide(self, message):
|
|
|
|
|
|
full_message = (
|
|
|
|
|
|
f"{message}\n\n"
|
|
|
|
|
|
"本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n"
|
|
|
|
|
|
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。"
|
|
|
|
|
|
)
|
|
|
|
|
|
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 _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 _active_batch_ids(self):
|
|
|
|
|
|
selected_batch = self.batch_filter.currentData()
|
|
|
|
|
|
if selected_batch:
|
|
|
|
|
|
return [selected_batch]
|
|
|
|
|
|
batch_ids = []
|
|
|
|
|
|
for task in self.model.tasks:
|
|
|
|
|
|
batch_id = getattr(task, "batch_id", None)
|
|
|
|
|
|
if batch_id and batch_id not in batch_ids:
|
|
|
|
|
|
batch_ids.append(batch_id)
|
|
|
|
|
|
return batch_ids
|
|
|
|
|
|
|
|
|
|
|
|
def _update_write_back_button(self):
|
|
|
|
|
|
if getattr(self, "write_back_button", None) is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
enabled = (
|
|
|
|
|
|
self.apply_thread is None
|
|
|
|
|
|
and self.result_write_back_thread is None
|
|
|
|
|
|
and bool(self._active_batch_ids())
|
|
|
|
|
|
)
|
|
|
|
|
|
self.write_back_button.setEnabled(enabled)
|
|
|
|
|
|
|
|
|
|
|
|
def _start_result_write_back(self, batch_ids, auto=False, apply_summary=None):
|
|
|
|
|
|
if self.result_write_back_thread is not None:
|
|
|
|
|
|
self._set_status("Excel 结果回写正在进行...")
|
|
|
|
|
|
return False
|
|
|
|
|
|
worker = WriteBackWorker(
|
|
|
|
|
|
batch_ids,
|
|
|
|
|
|
db_path=self.db_path,
|
|
|
|
|
|
mode="results",
|
|
|
|
|
|
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
|
|
|
|
|
)
|
|
|
|
|
|
worker.failed.connect(
|
|
|
|
|
|
lambda task_id, error, auto=auto, apply_summary=apply_summary:
|
|
|
|
|
|
self._on_result_write_back_failed(
|
|
|
|
|
|
task_id,
|
|
|
|
|
|
error,
|
|
|
|
|
|
auto=auto,
|
|
|
|
|
|
apply_summary=apply_summary,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
worker.finished.connect(
|
|
|
|
|
|
lambda payload, auto=auto, apply_summary=apply_summary:
|
|
|
|
|
|
self._on_result_write_back_finished(
|
|
|
|
|
|
payload,
|
|
|
|
|
|
auto=auto,
|
|
|
|
|
|
apply_summary=apply_summary,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
thread = run_worker(worker, thread_name="ResultWriteBackWorker", start=False)
|
|
|
|
|
|
thread.finished.connect(lambda: self._forget_result_write_back_thread(thread))
|
|
|
|
|
|
self.result_write_back_worker = worker
|
|
|
|
|
|
self.result_write_back_thread = thread
|
|
|
|
|
|
self._set_result_write_back_running(True)
|
|
|
|
|
|
self._set_status("正在自动回写更新结果到 Excel..." if auto else "正在回写更新结果到 Excel...")
|
|
|
|
|
|
thread.start()
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _on_result_write_back_failed(self, task_id, error, auto=False, apply_summary=None):
|
|
|
|
|
|
message = f"Excel {'自动' if auto else ''}回写更新结果失败:{error}"
|
|
|
|
|
|
if "被占用" in str(error):
|
|
|
|
|
|
message += "\n请关闭原 Excel 后点击「回写结果到 Excel」手动重试;SQLite 已保留更新结果。"
|
|
|
|
|
|
if auto and apply_summary:
|
|
|
|
|
|
message = self._apply_summary_message(apply_summary, error=message)
|
|
|
|
|
|
QMessageBox.warning(self, "回写结果到 Excel", message)
|
|
|
|
|
|
self._set_status(message.replace("\n", " "))
|
|
|
|
|
|
|
|
|
|
|
|
def _on_result_write_back_finished(self, payload, auto=False, apply_summary=None):
|
|
|
|
|
|
self._set_result_write_back_running(False)
|
|
|
|
|
|
self.refresh_tasks()
|
|
|
|
|
|
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}")
|
|
|
|
|
|
return
|
|
|
|
|
|
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 apply_summary:
|
|
|
|
|
|
self._show_apply_summary(apply_summary, write_back_payload=payload)
|
|
|
|
|
|
elif not auto:
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"回写结果到 Excel",
|
|
|
|
|
|
"结果回写完成:文件{files},行{rows}".format(
|
|
|
|
|
|
files=payload.get("files", 0),
|
|
|
|
|
|
rows=payload.get("rows", 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _show_apply_summary(self, apply_summary, write_back_payload=None):
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"检查本轮更新完成" if apply_summary.get("dry_run") else "更新完成",
|
|
|
|
|
|
self._apply_summary_message(apply_summary, write_back_payload),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None):
|
|
|
|
|
|
dry_run = bool(apply_summary.get("dry_run"))
|
|
|
|
|
|
lines = [
|
|
|
|
|
|
"检查本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。"
|
|
|
|
|
|
if dry_run
|
|
|
|
|
|
else "更新完成。",
|
|
|
|
|
|
"{success_label}:{applied},失败:{failed},略过:{skipped}".format(
|
|
|
|
|
|
success_label="可更新" if dry_run else "成功",
|
|
|
|
|
|
applied=apply_summary.get("applied", 0),
|
|
|
|
|
|
failed=apply_summary.get("failed", 0),
|
|
|
|
|
|
skipped=apply_summary.get("skipped", 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
if write_back_payload:
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
"Excel 回写:文件{files},行{rows}".format(
|
|
|
|
|
|
files=write_back_payload.get("files", 0),
|
|
|
|
|
|
rows=write_back_payload.get("rows", 0),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if error:
|
|
|
|
|
|
lines.append(str(error))
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|