feat(collect): support soft deletion of one task
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user