feat(collect): support soft deletion of one task

This commit is contained in:
chengma
2026-07-20 10:27:21 +08:00
parent a92f1c86f4
commit 9852b1c9c1
8 changed files with 333 additions and 10 deletions
+49
View File
@@ -161,6 +161,8 @@ class Task:
generated_at: Optional[str]
applied_at: Optional[str]
updated_at: str
deleted_at: Optional[str]
deleted_reason: Optional[str]
@dataclass(frozen=True)
@@ -261,6 +263,8 @@ CREATE TABLE IF NOT EXISTS tasks (
generated_at TEXT,
applied_at TEXT,
updated_at TEXT NOT NULL,
deleted_at TEXT,
deleted_reason TEXT,
UNIQUE(batch_id, source_file_abs, source_sheet, source_row)
);
@@ -495,6 +499,7 @@ def init_db(path=None, conn=None) -> None:
_ensure_task_image_task_columns(database)
_ensure_task_cover_reset_columns(database)
_ensure_task_product_status_columns(database)
_ensure_task_delete_columns(database)
_migrate_legacy_product_status_defaults(database)
_ensure_image_studio_project_suite_columns(database)
_ensure_image_studio_project_draft_columns(database)
@@ -537,6 +542,15 @@ def _ensure_task_product_status_columns(database):
database.execute("ALTER TABLE tasks ADD COLUMN product_status_at TEXT")
def _ensure_task_delete_columns(database):
columns = {row["name"] for row in database.execute("PRAGMA table_info(tasks)").fetchall()}
if "deleted_at" not in columns:
database.execute("ALTER TABLE tasks ADD COLUMN deleted_at TEXT")
if "deleted_reason" not in columns:
database.execute("ALTER TABLE tasks ADD COLUMN deleted_reason TEXT")
database.execute("CREATE INDEX IF NOT EXISTS idx_tasks_deleted_at ON tasks(deleted_at)")
def _migrate_legacy_product_status_defaults(database):
"""Treat pre-status-feature active batches as historically on-shelf once."""
@@ -548,6 +562,7 @@ def _migrate_legacy_product_status_defaults(database):
product_status_at = NULL,
updated_at = ?
WHERE (product_status IS NULL OR TRIM(product_status) = '')
AND deleted_at IS NULL
AND batch_id IN (
SELECT id
FROM batches
@@ -880,6 +895,7 @@ def list_tasks(
params.append(value)
if not include_deleted:
clauses.append("b.deleted_at IS NULL")
clauses.append("t.deleted_at IS NULL")
sql = "SELECT t.* FROM tasks t JOIN batches b ON b.id = t.batch_id"
if clauses:
sql += " WHERE " + " AND ".join(clauses)
@@ -920,11 +936,44 @@ def delete_batch(batch_id, reason=None, path=None, conn=None) -> dict:
"image_paths": image_paths,
}
def delete_task(task_id, reason=None, path=None, conn=None) -> dict:
"""Soft delete one inactive task without touching its source files or images."""
task_id = int(task_id)
with _connection(conn, path) as database:
task = get_task(task_id, conn=database)
if task is None:
raise DbError(f"任务不存在或已删除: {task_id}")
if task.status == "running":
raise DbError("任务正在处理,不能删除")
now = _now()
with database:
cursor = database.execute(
"""
UPDATE tasks
SET deleted_at = ?, deleted_reason = ?, updated_at = ?
WHERE id = ? AND deleted_at IS NULL AND status <> 'running'
""",
(now, str(reason or ""), now, task_id),
)
if cursor.rowcount != 1:
raise DbError("任务已删除或正在处理,不能删除")
return {
"task_id": task_id,
"batch_id": task.batch_id,
"item_id": task.item_id,
"committed": int(task.committed or 0),
"deleted_at": now,
}
def get_task(task_id, path=None, conn=None, include_deleted=False):
clauses = ["t.id = ?"]
params = [int(task_id)]
if not include_deleted:
clauses.append("b.deleted_at IS NULL")
clauses.append("t.deleted_at IS NULL")
sql = "SELECT t.* FROM tasks t JOIN batches b ON b.id = t.batch_id"
sql += " WHERE " + " AND ".join(clauses)
with _connection(conn, path) as database:
+81
View File
@@ -178,6 +178,7 @@ class CollectTab(QWidget):
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
for column in (3, 4):
self.table.horizontalHeader().setSectionResizeMode(column, QHeaderView.ResizeToContents)
@@ -219,6 +220,7 @@ class CollectTab(QWidget):
self.product_status_filter.currentIndexChanged.connect(self.refresh_tasks)
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
self.delete_batch_button.clicked.connect(self.delete_current_batch)
self.table.customContextMenuRequested.connect(self._show_task_context_menu)
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)
@@ -682,6 +684,85 @@ class CollectTab(QWidget):
running = bool(self.collect_thread or self.write_back_thread)
self.delete_batch_button.setEnabled((not running) and bool(self._selected_batch_id()))
def _show_task_context_menu(self, position):
index = self.table.indexAt(position)
if not index.isValid():
return
self.table.selectRow(index.row())
self.table.setCurrentIndex(index)
task = self.model.task_at(index.row())
if task is None:
return
menu = QMenu(self.table)
delete_action = menu.addAction("删除本条记录")
delete_action.setEnabled(
not bool(self.collect_thread or self.write_back_thread)
and getattr(task, "status", "") != "running"
)
delete_action.triggered.connect(self.delete_selected_task)
menu.exec(self.table.viewport().mapToGlobal(position))
def delete_selected_task(self, checked=False):
if self.collect_thread or self.write_back_thread:
self._set_status("采集或回写正在进行,不能删除记录", level="warning")
return
index = self.table.currentIndex()
task = self.model.task_at(index.row()) if index.isValid() else None
if task is None:
self._set_status("请选择要删除的记录", level="warning")
return
if getattr(task, "status", "") == "running":
self._set_status("任务正在处理,不能删除", level="warning")
return
batch = db.get_batch(task.batch_id, path=self.db_path)
batch_text = self._batch_label(batch) if batch is not None else task.batch_id
lines = [
"确定要删除本条本地记录吗?",
f"商品ID:{task.item_id}",
f"店铺:{task.account_name or task.alias}({task.alias})",
f"所属批次:{batch_text}",
]
if int(getattr(task, "committed", 0) or 0) == 1:
lines.extend(
[
"",
"该商品已提交过蝦皮。本次仅删除本地记录,不会回滚蝦皮线上商品。",
]
)
lines.extend(
[
"",
"删除后,该记录不会出现在①、②、③的列表、筛选、采集、生成、更新或回写范围。",
"不会删除原 Excel、本地图片或账号 Chrome 数据。",
]
)
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_task(
task.id,
reason="用户在导入采集页删除单条记录",
path=self.db_path,
)
except Exception as exc:
QMessageBox.warning(self, "删除本条记录", str(exc))
self._set_status(f"删除本条记录失败:{exc}", level="danger")
return
self.refresh_tasks()
if self.refresh_workflow_callback is not None:
self.refresh_workflow_callback()
message = f"已删除本地记录:商品 {result['item_id']}"
self._set_status(message, level="success")
QMessageBox.information(self, "删除本条记录", message)
def delete_current_batch(self, checked=False):
batch_id = self._selected_batch_id()
if not batch_id: