feat: polish Shopee update checks
This commit is contained in:
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user