feat: 完成T-204旧字段回写

实现 Excel 旧标题与旧封面路径回写原文件,按 source_file_abs/source_sheet/source_row 定位行,支持输出列自动追加、xlsm 保留 VBA、文件占用中文错误提示与 export_copy 另存副本。

Tab① 增加回写旧数据按钮与 WriteBackWorker,后台调用 excel.write_back,锁文件时提示关闭后重试;补充 Excel 与 GUI 单测覆盖回写、锁文件、另存副本和 worker 调用。

同步 harness 文档:T-204 标记完成,新增 T-204b 记录采集完成自动回写缺口,扩展 T-205 覆盖 Chrome 未启动/未登录引导保护,并更新 current-state、routes、api、architecture、requirements 与 progress。
This commit is contained in:
chengma
2026-06-27 14:37:58 +08:00
parent 8fe979942c
commit b9d84e71ec
11 changed files with 503 additions and 29 deletions
+91
View File
@@ -206,11 +206,14 @@ if QT_IMPORT_ERROR is None:
self.last_import_stats = None
self.collect_worker = None
self.collect_thread = None
self.write_back_worker = None
self.write_back_thread = None
self.import_button = QPushButton("导入 Excel...")
self.refresh_button = QPushButton("刷新")
self.collect_button = QPushButton("采集旧标题/旧封面")
self.stop_collect_button = QPushButton("停止")
self.write_back_button = QPushButton("回写旧数据到 Excel")
self.stop_collect_button.setEnabled(False)
toolbar = QHBoxLayout()
@@ -218,6 +221,7 @@ if QT_IMPORT_ERROR is None:
toolbar.addWidget(self.refresh_button)
toolbar.addWidget(self.collect_button)
toolbar.addWidget(self.stop_collect_button)
toolbar.addWidget(self.write_back_button)
toolbar.addStretch(1)
self.summary_label = QLabel("未导入任务")
@@ -254,6 +258,7 @@ if QT_IMPORT_ERROR is None:
self.refresh_button.clicked.connect(self.refresh_tasks)
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)
self.show_all_button.clicked.connect(self.show_all_tasks)
self.show_unmatched_button.clicked.connect(self.show_unmatched_tasks)
@@ -339,17 +344,57 @@ if QT_IMPORT_ERROR is None:
self.collect_worker.cancel()
self._set_status("正在停止采集...")
def write_back_old_data(self, checked=False):
batch_id = self._active_batch_id()
if not batch_id:
self._set_status("没有可回写批次")
return
worker = WriteBackWorker(batch_id, db_path=self.db_path)
worker.failed.connect(self._on_write_back_failed)
worker.finished.connect(self._on_write_back_finished)
thread = run_worker(worker, thread_name="WriteBackWorker", start=False)
thread.finished.connect(lambda: self._forget_write_back_thread(thread))
self.write_back_worker = worker
self.write_back_thread = thread
self._set_write_back_running(True)
self._set_status("正在回写旧数据到 Excel...")
thread.start()
def _active_batch_id(self):
if self.current_batch_id:
return self.current_batch_id
batch_ids = {
task.batch_id
for task in self.model.all_tasks
if getattr(task, "batch_id", None)
}
if len(batch_ids) == 1:
return next(iter(batch_ids))
return None
def _set_collect_running(self, running):
self.import_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.collect_button.setEnabled(not running)
self.write_back_button.setEnabled(not running)
self.stop_collect_button.setEnabled(running)
def _set_write_back_running(self, running):
self.import_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.collect_button.setEnabled(not running)
self.write_back_button.setEnabled(not running)
def _forget_collect_thread(self, thread):
if self.collect_thread is thread:
self.collect_thread = None
self.collect_worker = None
def _forget_write_back_thread(self, thread):
if self.write_back_thread is thread:
self.write_back_thread = None
self.write_back_worker = None
def _on_collect_progress(self, payload):
self._set_status(
"采集进度:{done}/{total},成功{collected},略过{skipped},失败{failed}".format(
@@ -388,6 +433,27 @@ if QT_IMPORT_ERROR is None:
)
)
def _on_write_back_failed(self, task_id, error):
message = f"Excel 回写失败:{error}"
if "被占用" in str(error):
message += "\n请关闭原 Excel 后重试;SQLite 已保留采集结果,也可另存副本。"
QMessageBox.warning(self, "回写旧数据", message)
self._set_status(message.replace("\n", " "))
def _on_write_back_finished(self, payload):
self._set_write_back_running(False)
if payload.get("ok") is False:
error = payload.get("error") or "未知错误"
self._set_status(f"Excel 回写失败:{error}")
return
self.refresh_tasks()
self._set_status(
"Excel 回写完成:文件{files},行{rows}".format(
files=payload.get("files", 0),
rows=payload.get("rows", 0),
)
)
def show_all_tasks(self, checked=False):
self.model.set_filter_mode("all")
self._update_empty_label(len(self.model.all_tasks))
@@ -669,6 +735,31 @@ if QT_IMPORT_ERROR is None:
)
class WriteBackWorker(BaseWorker):
"""Write collected old fields back to Excel in a background thread."""
def __init__(self, batch_id, db_path=None, excel_path=None):
super().__init__()
self.batch_id = batch_id
self.db_path = db_path
self.excel_path = excel_path
def execute(self):
result = excel.write_back(
self.batch_id,
excel_path=self.excel_path,
path=self.db_path,
)
self.progress.emit(
{
"done": result.get("rows", 0),
"total": result.get("rows", 0),
"files": result.get("files", 0),
}
)
return result
class AccountLoginCheckWorker(BaseWorker):
def __init__(self, account, db_path=None, config=None, timeout=8):
super().__init__()