feat: 完成T-403结果回写与汇总
新增excel.write_back_results,按原Excel文件、工作表和行号回写新标题、新封面图片路径与更新状态。 ③更新完成后自动用WriteBackWorker(mode=results)回写结果,并弹窗汇总成功、失败、略过数量;文件被占用时提示关闭Excel后手动重试。 补充Excel结果回写、GUI自动回写/汇总/锁文件提示和模块契约测试,同步任务看板、API、流程和当前状态文档。
This commit is contained in:
@@ -252,6 +252,55 @@ class ExcelImportTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_write_back_results_writes_new_fields_and_update_status(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
excel_path = os.path.join(temp_dir, "input.xlsx")
|
||||
self.save_workbook(
|
||||
excel_path,
|
||||
[
|
||||
(
|
||||
"待处理任务",
|
||||
[
|
||||
["账号名", "别名", "商品id"],
|
||||
["主店", "alias-a", "51100639510"],
|
||||
["副店", "alias-b", "51100639511"],
|
||||
["未知", "missing", "51100639512"],
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
result = excel.import_tasks([excel_path], path=db_path)
|
||||
tasks = db.list_tasks(batch_id=result["batch_id"], path=db_path)
|
||||
for task in tasks:
|
||||
db.set_collected(task.id, "旧标题", "old.jpg", path=db_path)
|
||||
db.set_generated(
|
||||
task.id,
|
||||
"新标题" + task.item_id[-2:],
|
||||
rf"D:\images\{task.item_id}_new.jpg",
|
||||
path=db_path,
|
||||
)
|
||||
refreshed = db.list_tasks(batch_id=result["batch_id"], path=db_path)
|
||||
db.set_applied(refreshed[0].id, True, path=db_path)
|
||||
db.set_applied(refreshed[1].id, False, "UPDATE_DISABLED", path=db_path)
|
||||
db.mark_skipped(refreshed[2].id, "别名未匹配账号", path=db_path)
|
||||
|
||||
summary = excel.write_back_results(result["batch_id"], path=db_path)
|
||||
|
||||
self.assertEqual(True, summary["ok"])
|
||||
self.assertEqual(1, summary["files"])
|
||||
self.assertEqual(3, summary["rows"])
|
||||
first = self.row_values_by_header(excel_path, row_number=2)
|
||||
second = self.row_values_by_header(excel_path, row_number=3)
|
||||
third = self.row_values_by_header(excel_path, row_number=4)
|
||||
self.assertEqual("新标题10", first["新标题"])
|
||||
self.assertEqual(r"D:\images\51100639510_new.jpg", first["新封面图片路径"])
|
||||
self.assertEqual("成功", first["更新状态"])
|
||||
self.assertEqual("失败:UPDATE_DISABLED", second["更新状态"])
|
||||
self.assertEqual("略过:别名未匹配账号", third["更新状态"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_export_copy_writes_copy_without_touching_original(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
db_path = os.path.join(temp_dir, "cmshopee.db")
|
||||
|
||||
+114
-1
@@ -515,7 +515,15 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assertEqual(["alias-a", "alias-b"], applied_aliases)
|
||||
self.assertEqual(
|
||||
{"ok": False, "total": 3, "done": 3, "applied": 1, "skipped": 1, "failed": 1},
|
||||
{
|
||||
"ok": False,
|
||||
"total": 3,
|
||||
"done": 3,
|
||||
"applied": 1,
|
||||
"skipped": 1,
|
||||
"failed": 1,
|
||||
"batch_ids": [batch_id],
|
||||
},
|
||||
summary,
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -538,6 +546,97 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_auto_starts_result_write_back_and_shows_summary(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
db.init_db(cfg["db_path"])
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
batch_id,
|
||||
[
|
||||
{
|
||||
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
|
||||
"source_sheet": "商品",
|
||||
"source_row": 2,
|
||||
"account_name": "Excel主店",
|
||||
"alias": "alias-a",
|
||||
"item_id": "51100639510",
|
||||
}
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
|
||||
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
|
||||
db.set_generated(task.id, "新标题", "new.jpg", path=cfg["db_path"])
|
||||
db.set_applied(task.id, True, path=cfg["db_path"])
|
||||
statuses = []
|
||||
tab = ApplyTab(config=cfg, status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
payload = {
|
||||
"done": 1,
|
||||
"total": 1,
|
||||
"applied": 1,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"batch_ids": [batch_id],
|
||||
}
|
||||
|
||||
with mock.patch.object(tab, "_start_result_write_back", return_value=True) as start_write_back:
|
||||
tab._on_apply_finished(payload)
|
||||
|
||||
start_write_back.assert_called_once_with(
|
||||
[batch_id],
|
||||
auto=True,
|
||||
apply_summary=payload,
|
||||
)
|
||||
self.assertIn("正在自动回写结果到 Excel", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_result_write_back_finished_shows_completion_popup(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
statuses = []
|
||||
tab = ApplyTab(config=self.make_config(temp_dir), status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
summary = {"applied": 2, "failed": 1, "skipped": 1}
|
||||
write_back_payload = {"ok": True, "files": 1, "rows": 4}
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.information") as info:
|
||||
tab._on_result_write_back_finished(
|
||||
write_back_payload,
|
||||
auto=True,
|
||||
apply_summary=summary,
|
||||
)
|
||||
|
||||
message = info.call_args[0][2]
|
||||
self.assertIn("成功:2,失败:1,略过:1", message)
|
||||
self.assertIn("Excel 回写:文件1,行4", message)
|
||||
self.assertIn("Excel 自动回写更新结果完成", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_result_write_back_locked_file_message_points_to_manual_retry(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
statuses = []
|
||||
tab = ApplyTab(config=self.make_config(temp_dir), status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
summary = {"applied": 1, "failed": 0, "skipped": 0}
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning:
|
||||
tab._on_result_write_back_failed(
|
||||
-1,
|
||||
"Excel 文件被占用,请关闭后重试: input.xlsx",
|
||||
auto=True,
|
||||
apply_summary=summary,
|
||||
)
|
||||
|
||||
message = warning.call_args[0][2]
|
||||
self.assertIn("成功:1,失败:0,略过:0", message)
|
||||
self.assertIn("点击「回写结果到 Excel」手动重试", message)
|
||||
self.assertIn("手动重试", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_worker_preflight_blocks_when_chrome_not_running(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
@@ -1246,6 +1345,20 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
path="db.sqlite",
|
||||
)
|
||||
|
||||
def test_write_back_worker_calls_result_write_back(self):
|
||||
with mock.patch(
|
||||
"app.gui.excel.write_back_results",
|
||||
return_value={"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2},
|
||||
) as write_back_results:
|
||||
summary = WriteBackWorker("batch-1", db_path="db.sqlite", mode="results").execute()
|
||||
|
||||
self.assertEqual({"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, summary)
|
||||
write_back_results.assert_called_once_with(
|
||||
"batch-1",
|
||||
excel_path=None,
|
||||
path="db.sqlite",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -21,7 +21,7 @@ def import_or_skip(module_name):
|
||||
class PendingModuleContractTests(unittest.TestCase):
|
||||
def test_excel_contract_when_module_exists(self):
|
||||
excel = import_or_skip("app.excel")
|
||||
for name in ("import_tasks", "match_summary", "write_back", "export_copy"):
|
||||
for name in ("import_tasks", "match_summary", "write_back", "write_back_results", "export_copy"):
|
||||
self.assertTrue(callable(getattr(excel, name, None)), name)
|
||||
|
||||
def test_prompts_contract_when_module_exists(self):
|
||||
|
||||
Reference in New Issue
Block a user