feat: 完成T-403结果回写与汇总
新增excel.write_back_results,按原Excel文件、工作表和行号回写新标题、新封面图片路径与更新状态。 ③更新完成后自动用WriteBackWorker(mode=results)回写结果,并弹窗汇总成功、失败、略过数量;文件被占用时提示关闭Excel后手动重试。 补充Excel结果回写、GUI自动回写/汇总/锁文件提示和模块契约测试,同步任务看板、API、流程和当前状态文档。
This commit is contained in:
+206
-9
@@ -966,6 +966,9 @@ if QT_IMPORT_ERROR is None:
|
||||
self.open_accounts_callback = open_accounts_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")
|
||||
@@ -1023,6 +1026,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||
self.start_update_button.clicked.connect(self.start_update)
|
||||
self.stop_update_button.clicked.connect(self.stop_update)
|
||||
self.write_back_button.clicked.connect(self.write_back_results)
|
||||
|
||||
self.refresh_tasks()
|
||||
|
||||
@@ -1060,6 +1064,7 @@ if QT_IMPORT_ERROR is None:
|
||||
self.summary_label.setText(
|
||||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||||
)
|
||||
self._update_write_back_button()
|
||||
|
||||
def start_update(self, checked=False):
|
||||
if self.apply_thread is not None:
|
||||
@@ -1102,6 +1107,13 @@ if QT_IMPORT_ERROR is 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 _is_actionable_task(self, task):
|
||||
return (
|
||||
getattr(task, "stage", None) == "generated"
|
||||
@@ -1199,13 +1211,27 @@ if QT_IMPORT_ERROR is None:
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_filter.setEnabled(not running)
|
||||
self.status_filter.setEnabled(not running)
|
||||
self.write_back_button.setEnabled(False)
|
||||
self._update_write_back_button()
|
||||
|
||||
def _set_result_write_back_running(self, running):
|
||||
self.start_update_button.setEnabled(not running)
|
||||
self.refresh_button.setEnabled(not running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_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))
|
||||
|
||||
@@ -1221,7 +1247,19 @@ if QT_IMPORT_ERROR is None:
|
||||
if payload.get("blocked"):
|
||||
self._show_apply_blocked(payload)
|
||||
return
|
||||
self._set_status("更新完成:" + self._apply_progress_text(payload))
|
||||
self.last_apply_summary = dict(payload)
|
||||
message = "更新完成:" + self._apply_progress_text(payload)
|
||||
batch_ids = payload.get("batch_ids") or self._active_batch_ids()
|
||||
if 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)
|
||||
@@ -1277,6 +1315,121 @@ if QT_IMPORT_ERROR is 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")
|
||||
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,
|
||||
"更新完成",
|
||||
self._apply_summary_message(apply_summary, write_back_payload),
|
||||
)
|
||||
|
||||
def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None):
|
||||
lines = [
|
||||
"更新完成。",
|
||||
"成功:{applied},失败:{failed},略过:{skipped}".format(
|
||||
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)
|
||||
|
||||
|
||||
class CollectTab(QWidget):
|
||||
"""Tab 1: import Excel files and list imported tasks."""
|
||||
@@ -1827,6 +1980,7 @@ if QT_IMPORT_ERROR is None:
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
eligible = [task for task in self.tasks if self._is_actionable_task(task)]
|
||||
batch_ids = self._batch_ids(eligible)
|
||||
total = len(eligible)
|
||||
applied = 0
|
||||
skipped = 0
|
||||
@@ -1845,6 +1999,7 @@ if QT_IMPORT_ERROR is None:
|
||||
"applied": 0,
|
||||
"skipped": 0,
|
||||
"failed": 0,
|
||||
"batch_ids": batch_ids,
|
||||
}
|
||||
)
|
||||
return blocked
|
||||
@@ -1909,6 +2064,7 @@ if QT_IMPORT_ERROR is None:
|
||||
"applied": applied,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"batch_ids": batch_ids,
|
||||
}
|
||||
|
||||
def _is_actionable_task(self, task):
|
||||
@@ -1985,6 +2141,14 @@ if QT_IMPORT_ERROR is None:
|
||||
reason = status.get("reason")
|
||||
return f"账号未登录: {reason}" if reason else "账号未登录"
|
||||
|
||||
def _batch_ids(self, tasks):
|
||||
batch_ids = []
|
||||
for task in 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
|
||||
|
||||
|
||||
class CollectWorker(BaseWorker):
|
||||
"""Collect old title and cover for imported tasks."""
|
||||
@@ -2176,20 +2340,21 @@ if QT_IMPORT_ERROR is None:
|
||||
|
||||
|
||||
class WriteBackWorker(BaseWorker):
|
||||
"""Write collected old fields back to Excel in a background thread."""
|
||||
"""Write Excel fields back in a background thread."""
|
||||
|
||||
def __init__(self, batch_id, db_path=None, excel_path=None):
|
||||
def __init__(self, batch_id, db_path=None, excel_path=None, mode="old"):
|
||||
super().__init__()
|
||||
self.batch_id = batch_id
|
||||
self.db_path = db_path
|
||||
self.excel_path = excel_path
|
||||
self.mode = mode
|
||||
|
||||
def execute(self):
|
||||
result = excel.write_back(
|
||||
self.batch_id,
|
||||
excel_path=self.excel_path,
|
||||
path=self.db_path,
|
||||
)
|
||||
results = [
|
||||
self._write_one(batch_id)
|
||||
for batch_id in self._batch_ids()
|
||||
]
|
||||
result = results[0] if len(results) == 1 else self._combined_result(results)
|
||||
self.progress.emit(
|
||||
{
|
||||
"done": result.get("rows", 0),
|
||||
@@ -2199,6 +2364,38 @@ if QT_IMPORT_ERROR is None:
|
||||
)
|
||||
return result
|
||||
|
||||
def _batch_ids(self):
|
||||
if isinstance(self.batch_id, (list, tuple, set)):
|
||||
return list(self.batch_id)
|
||||
return [self.batch_id]
|
||||
|
||||
def _write_one(self, batch_id):
|
||||
if self.mode == "results":
|
||||
return excel.write_back_results(
|
||||
batch_id,
|
||||
excel_path=self.excel_path,
|
||||
path=self.db_path,
|
||||
)
|
||||
return excel.write_back(
|
||||
batch_id,
|
||||
excel_path=self.excel_path,
|
||||
path=self.db_path,
|
||||
)
|
||||
|
||||
def _combined_result(self, results):
|
||||
written_files = []
|
||||
for result in results:
|
||||
for file_path in result.get("written_files", []):
|
||||
if file_path not in written_files:
|
||||
written_files.append(file_path)
|
||||
return {
|
||||
"ok": all(result.get("ok", False) for result in results),
|
||||
"batch_id": [result.get("batch_id") for result in results],
|
||||
"files": sum(result.get("files", 0) for result in results),
|
||||
"rows": sum(result.get("rows", 0) for result in results),
|
||||
"written_files": written_files,
|
||||
}
|
||||
|
||||
|
||||
class AccountLoginCheckWorker(BaseWorker):
|
||||
def __init__(self, account, db_path=None, config=None, timeout=8):
|
||||
|
||||
Reference in New Issue
Block a user