feat: polish Shopee update checks

This commit is contained in:
chengma
2026-07-01 14:30:49 +08:00
parent cc277366ed
commit 84b35f1cdc
12 changed files with 301 additions and 83 deletions
+29
View File
@@ -678,6 +678,35 @@ def set_generated(task_id, new_title, new_cover_path, path=None, conn=None) -> N
(new_title, new_cover_path, now, now, int(task_id)),
)
def update_generated_title(task_id, new_title, path=None, conn=None) -> None:
"""Update a generated title manually without touching Shopee or local cover files."""
title = str(new_title or "").strip()
if not title:
raise DbError("新标题不能为空")
with _connection(conn, path) as database:
task = get_task(task_id, conn=database)
if task is None:
raise DbError(f"任务不存在或批次已删除: {task_id}")
if int(task.committed or 0) == 1 or task.stage == "applied":
raise DbError("已提交线上商品不能在本地直接修改新标题")
if task.stage != "generated":
raise DbError("只有已生成且未提交线上的任务可以修改新标题")
if task.status == "running":
raise DbError("任务正在运行,不能修改新标题")
now = _now()
with database:
database.execute(
"""
UPDATE tasks
SET new_title = ?,
status = 'pending',
last_error = NULL,
updated_at = ?
WHERE id = ?
""",
(title, now, int(task_id)),
)
def set_applied(task_id, committed, error=None, path=None, conn=None) -> None:
now = _now()
+119 -27
View File
@@ -27,6 +27,7 @@ try:
QLabel,
QLineEdit,
QMainWindow,
QMenu,
QMessageBox,
QPlainTextEdit,
QPushButton,
@@ -270,10 +271,13 @@ if QT_IMPORT_ERROR is None:
"applied": "已更新",
}
def __init__(self, parent=None):
def __init__(self, parent=None, db_path=None, status_callback=None):
super().__init__(parent)
self.tasks = []
self.account_by_alias = {}
self.db_path = db_path
self.status_callback = status_callback
self.last_edit_error = None
def set_tasks(self, tasks, accounts):
self.beginResetModel()
@@ -302,16 +306,57 @@ if QT_IMPORT_ERROR is None:
if not index.isValid():
return None
task = self.tasks[index.row()]
if role == Qt.DisplayRole:
if role in (Qt.DisplayRole, Qt.EditRole):
return self._display_value(task, index.column())
if role == Qt.ToolTipRole and task.last_error:
return task.last_error
if role == Qt.ToolTipRole:
if index.column() == 3 and self._can_edit_title(task):
return "双击可微调新标题,只修改本地待更新内容"
if task.last_error:
return task.last_error
return None
def setData(self, index, value, role=Qt.EditRole):
if role != Qt.EditRole or not index.isValid() or index.column() != 3:
return False
task = self.tasks[index.row()]
if not self._can_edit_title(task):
self._set_status("该任务不能修改新标题")
return False
title = str(value or "").strip()
if title == str(task.new_title or ""):
return True
try:
db.update_generated_title(task.id, title, path=self.db_path)
updated = db.get_task(task.id, path=self.db_path)
except Exception as exc:
self.last_edit_error = str(exc)
self._set_status(f"新标题修改失败:{exc}")
return False
self.tasks[index.row()] = updated
self.last_edit_error = None
self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole])
self._set_status(f"已修改新标题:商品 {task.item_id}")
return True
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 3 and self._can_edit_title(self.tasks[index.row()]):
flags |= Qt.ItemIsEditable
return flags
def _can_edit_title(self, task):
return (
getattr(task, "stage", None) == "generated"
and getattr(task, "status", None) != "running"
and int(getattr(task, "committed", 0) or 0) == 0
and bool(getattr(task, "new_title", None))
)
def _set_status(self, message):
if self.status_callback is not None:
self.status_callback(message)
def task_at(self, row):
if row < 0 or row >= len(self.tasks):
@@ -434,7 +479,6 @@ if QT_IMPORT_ERROR is None:
return "待更新"
return self.STATUS_TEXT.get(task.status, task.status)
class GenerateTab(QWidget):
"""Tab 2: prompt area plus generation task filters/list."""
@@ -539,11 +583,11 @@ if QT_IMPORT_ERROR is None:
self.summary_label = QLabel("任务 0 条")
self.task_table = QTableView()
self.model = GenerateTaskTableModel(self.task_table)
self.model = GenerateTaskTableModel(self.task_table, db_path=self.db_path, status_callback=self._set_status)
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.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.task_table.verticalHeader().setVisible(False)
@@ -870,6 +914,8 @@ if QT_IMPORT_ERROR is None:
)
def show_task_images(self, index):
if index.isValid() and index.column() == 3:
return
task = self.model.task_at(index.row()) if index.isValid() else self._selected_task()
if task is None:
self._set_status("没有可预览的任务")
@@ -1120,12 +1166,14 @@ if QT_IMPORT_ERROR is 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
@@ -1157,13 +1205,14 @@ if QT_IMPORT_ERROR is None:
filter_layout.addWidget(self.refresh_button)
self.summary_label = QLabel("任务 0 条")
self.risk_label = QLabel("可先点击「预览本轮更新」检查当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
self.risk_label = QLabel("可先点击「检查本轮更新」确认当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。")
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()
@@ -1172,12 +1221,18 @@ if QT_IMPORT_ERROR is None:
self.run_log_view.setMaximumHeight(128)
self.run_log_view.setPlaceholderText("运行日志")
self.preview_update_button = QPushButton("预览本轮更新")
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(
"QPushButton#startUpdateButton { font-weight: 600; padding: 6px 16px; }"
)
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)
@@ -1186,7 +1241,6 @@ if QT_IMPORT_ERROR is None:
action_layout.addWidget(self.preview_update_button)
action_layout.addWidget(self.start_update_button)
action_layout.addWidget(self.stop_update_button)
action_layout.addWidget(self.reset_update_button)
action_layout.addStretch(1)
action_layout.addWidget(self.write_back_button)
@@ -1209,6 +1263,7 @@ if QT_IMPORT_ERROR is None:
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()
@@ -1273,18 +1328,18 @@ if QT_IMPORT_ERROR is None:
dry_run = bool(dry_run)
safety_error = self._update_safety_error(tasks, dry_run=dry_run)
if safety_error:
QMessageBox.warning(self, "更新安全开关", 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 "确认开始更新",
"确认检查本轮更新" 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 "已取消开始更新")
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(
@@ -1313,7 +1368,7 @@ if QT_IMPORT_ERROR is None:
self._set_apply_running(True)
self.run_log_view.clear()
if dry_run:
self._set_status(f"开始预览本轮更新:{len(tasks)} 条")
self._set_status(f"开始检查本轮更新:{len(tasks)} 条")
else:
self._set_status(f"开始更新:{len(tasks)} 条,按每批最多 {batch_size} 条执行")
thread.start()
@@ -1330,6 +1385,20 @@ if QT_IMPORT_ERROR is None:
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("更新或回写正在进行,不能重置")
@@ -1490,7 +1559,7 @@ if QT_IMPORT_ERROR is None:
else "关闭"
)
intro = (
"即将预览当前筛选结果。\n\n"
"即将检查当前筛选结果。\n\n"
if dry_run
else "即将按当前筛选结果分批更新 Shopee 线上商品。\n\n"
)
@@ -1508,7 +1577,7 @@ if QT_IMPORT_ERROR is None:
+ f"成功后关闭新页={close_text},"
+ f"多账号并行={parallel_text}\n\n"
+ (
"预览只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。"
"检查只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。"
if dry_run
else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。"
)
@@ -1519,7 +1588,10 @@ if QT_IMPORT_ERROR is None:
if dry_run:
return None
if not update_cfg.get("allow_real_submit", False):
return "设置未开启「允许真实提交线上商品」,已阻止本次更新。"
return (
"设置未开启「允许真实提交线上商品」,已阻止本次更新。\n"
"请到⑤设置 > Shopee 更新安全开启该开关后再开始更新。"
)
if not update_cfg.get("allow_cover_update", False):
cover_tasks = [
str(getattr(task, "item_id", ""))
@@ -1527,9 +1599,25 @@ if QT_IMPORT_ERROR is None:
if getattr(task, "new_cover_path", None)
]
if cover_tasks:
return "设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。"
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", {})
@@ -1593,7 +1681,7 @@ if QT_IMPORT_ERROR is None:
self._show_apply_blocked(payload)
return
self.last_apply_summary = dict(payload)
prefix = "预览本轮更新完成:" if payload.get("dry_run") else "更新完成:"
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:
@@ -1781,14 +1869,14 @@ if QT_IMPORT_ERROR is None:
def _show_apply_summary(self, apply_summary, write_back_payload=None):
QMessageBox.information(
self,
"预览本轮更新完成" if apply_summary.get("dry_run") else "更新完成",
"检查本轮更新完成" 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、未提交线上、未改任务状态。"
"检查本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。"
if dry_run
else "更新完成。",
"{success_label}:{applied},失败:{failed},略过:{skipped}".format(
@@ -2718,7 +2806,7 @@ if QT_IMPORT_ERROR is None:
self._run_id = self._create_run_log(eligible, batch_ids)
self._log_run_event(
"运行开始:{mode},任务{total},每批最多{batch_size},批次{batch_count},{parallel}".format(
mode="预览本轮更新" if self.dry_run else "真实更新",
mode="检查本轮更新" if self.dry_run else "真实更新",
total=total,
batch_size=batch_size,
batch_count=len(batches),
@@ -2856,7 +2944,7 @@ if QT_IMPORT_ERROR is None:
def _log_batch_start(self, batch_index, batch_count, batch_tasks, counters, total):
first = counters["done"] + 1
last = min(first + len(batch_tasks) - 1, total)
label = "预览批次" if self.dry_run else "更新批次"
label = "检查批次" if self.dry_run else "更新批次"
self._log_run_event(
f"{label} {batch_index}/{batch_count} 开始:任务 {first}-{last}/{total}"
)
@@ -2905,7 +2993,7 @@ if QT_IMPORT_ERROR is None:
if account is None:
reason = "别名未匹配账号"
self._log_run_event(
f"预览:任务 {task.id} 商品 {task.item_id} 将略过:{reason}",
f"检查:任务 {task.id} 商品 {task.item_id} 将略过:{reason}",
task=task,
level="warning",
)
@@ -2917,7 +3005,7 @@ if QT_IMPORT_ERROR is None:
action_parts.append("封面")
action_text = "+".join(action_parts) or "无变更"
self._log_run_event(
"预览:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format(
"检查:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format(
task_id=task.id,
item_id=task.item_id,
alias=account.alias,
@@ -3773,7 +3861,7 @@ if QT_IMPORT_ERROR is None:
self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。")
self.close_success_tab_checkbox = QCheckBox("成功后关闭本次新开编辑页")
self.close_success_tab_checkbox.setObjectName("closeSuccessTabCheckbox")
self.dry_run_checkbox = QCheckBox("预览本轮更新")
self.dry_run_checkbox = QCheckBox("检查本轮更新")
self.dry_run_checkbox.setObjectName("dryRunCheckbox")
self.dry_run_checkbox.setVisible(False)
self.parallel_accounts_checkbox = QCheckBox("多账号并行更新")
@@ -4737,6 +4825,7 @@ if QT_IMPORT_ERROR is None:
config=self.config,
status_callback=self.statusBar().showMessage,
open_accounts_callback=lambda: self.open_accounts_tab(),
open_settings_callback=lambda: self.open_settings_tab(),
)
if title == "④ 账号管理":
return AccountsTab(
@@ -4762,6 +4851,9 @@ if QT_IMPORT_ERROR is None:
def open_accounts_tab(self):
self.tabs.setCurrentIndex(TAB_TITLES.index("④ 账号管理"))
def open_settings_tab(self):
self.tabs.setCurrentIndex(TAB_TITLES.index("⑤ 设置"))
else:
class MainWindow(QMainWindow):
def __init__(self):