feat: 完成T-501c Shopee更新安全开关
新增 shopee_update 配置段,默认关闭真实提交和封面更新,限制测试商品ID、单次最大更新条数,并支持成功后关闭本轮自动新开编辑页。 Tab⑤ 设置页新增 Shopee 更新安全表单;Tab③ 开始更新前先读取安全配置并阻断未授权真实提交、超量、非测试商品和未允许的封面更新。 ApplyWorker 将 close_success_tab 传入 editor.apply_task;editor 仅在提交成功且页面为本轮自动新开时关闭 tab,失败和复用页面保留。 同步架构、API、路由、任务看板、当前状态与 progress 文档;补充 GUI 与 editor 单测覆盖安全设置保存、共享配置读取、安全拦截和 tab 关闭策略。
This commit is contained in:
+180
-2
@@ -57,6 +57,23 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"config_path": os.path.join(temp_dir, "config.json"),
|
||||
}
|
||||
|
||||
def allow_shopee_update(
|
||||
self,
|
||||
cfg,
|
||||
item_id="51100639510",
|
||||
allow_cover=True,
|
||||
max_items=1,
|
||||
close_success_tab=False,
|
||||
):
|
||||
cfg["shopee_update"] = {
|
||||
"test_item_id": item_id,
|
||||
"allow_real_submit": True,
|
||||
"allow_cover_update": allow_cover,
|
||||
"max_items_per_run": max_items,
|
||||
"close_success_tab": close_success_tab,
|
||||
}
|
||||
return cfg
|
||||
|
||||
def test_main_window_has_five_tabs_in_workflow_order(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
window = MainWindow(config=self.make_config(temp_dir))
|
||||
@@ -132,6 +149,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertEqual(QLineEdit.Password, tab.api_key_edit.echoMode())
|
||||
self.assertEqual(11, tab.connect_timeout_spin.value())
|
||||
self.assertFalse(tab.delete_model_button.isEnabled())
|
||||
self.assertEqual("51100639510", tab.test_item_id_edit.text())
|
||||
self.assertFalse(tab.allow_real_submit_checkbox.isChecked())
|
||||
self.assertFalse(tab.allow_cover_update_checkbox.isChecked())
|
||||
self.assertEqual(1, tab.max_items_per_run_spin.value())
|
||||
self.assertFalse(tab.close_success_tab_checkbox.isChecked())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
@@ -319,6 +341,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
tab.debug_port_start_spin.setValue(9300)
|
||||
tab.debug_port_end_spin.setValue(9350)
|
||||
tab.cdp_ready_timeout_spin.setValue(45)
|
||||
tab.test_item_id_edit.setText("51100639510")
|
||||
tab.allow_real_submit_checkbox.setChecked(True)
|
||||
tab.allow_cover_update_checkbox.setChecked(True)
|
||||
tab.max_items_per_run_spin.setValue(2)
|
||||
tab.close_success_tab_checkbox.setChecked(True)
|
||||
|
||||
tab.save_app_settings()
|
||||
|
||||
@@ -337,12 +364,50 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertEqual(9300, saved["default_debug_port"])
|
||||
self.assertEqual([9300, 9350], saved["debug_port_range"])
|
||||
self.assertEqual(45, saved["cdp_ready_timeout"])
|
||||
self.assertEqual(
|
||||
{
|
||||
"test_item_id": "51100639510",
|
||||
"allow_real_submit": True,
|
||||
"allow_cover_update": True,
|
||||
"max_items_per_run": 2,
|
||||
"close_success_tab": True,
|
||||
},
|
||||
saved["shopee_update"],
|
||||
)
|
||||
self.assertNotIn("ai_models_path", saved)
|
||||
self.assertNotIn("config_path", saved)
|
||||
self.assertIn("设置已保存", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_save_updates_apply_tab_shared_safety_config(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
window = MainWindow(
|
||||
config=cfg,
|
||||
config_path=cfg["config_path"],
|
||||
ai_models_path=cfg["ai_models_path"],
|
||||
)
|
||||
self.addCleanup(window.close)
|
||||
settings_tab = window.tabs.widget(TAB_TITLES.index("⑤ 设置"))
|
||||
apply_tab = window.tabs.widget(TAB_TITLES.index("③ 更新shopee"))
|
||||
|
||||
settings_tab.test_item_id_edit.setText("123456789")
|
||||
settings_tab.allow_real_submit_checkbox.setChecked(True)
|
||||
settings_tab.allow_cover_update_checkbox.setChecked(True)
|
||||
settings_tab.max_items_per_run_spin.setValue(3)
|
||||
settings_tab.close_success_tab_checkbox.setChecked(True)
|
||||
settings_tab.save_app_settings()
|
||||
|
||||
safety_cfg = apply_tab._shopee_update_config()
|
||||
self.assertEqual("123456789", safety_cfg["test_item_id"])
|
||||
self.assertTrue(safety_cfg["allow_real_submit"])
|
||||
self.assertTrue(safety_cfg["allow_cover_update"])
|
||||
self.assertEqual(3, safety_cfg["max_items_per_run"])
|
||||
self.assertTrue(safety_cfg["close_success_tab"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_tab_rejects_invalid_port_range(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
@@ -651,6 +716,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
def test_apply_tab_start_update_requires_confirmation_before_starting_worker(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
self.allow_shopee_update(cfg)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
@@ -684,6 +750,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertIn("任务数:1", message)
|
||||
self.assertIn("提交线上", message)
|
||||
self.assertIn("状态:已生成", message)
|
||||
self.assertIn("测试商品ID=51100639510", message)
|
||||
self.assertEqual("已取消开始更新", statuses[-1])
|
||||
apply_task.assert_not_called()
|
||||
|
||||
@@ -692,6 +759,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
def test_apply_tab_start_update_starts_apply_worker_after_confirmation(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
self.allow_shopee_update(cfg, close_success_tab=True)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
|
||||
db.insert_tasks(
|
||||
@@ -742,6 +810,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertIsInstance(tab.apply_worker, ApplyWorker)
|
||||
self.assertIs(tab.apply_thread, fake_thread)
|
||||
self.assertTrue(fake_thread.started)
|
||||
self.assertTrue(tab.apply_worker.close_success_tab)
|
||||
self.assertFalse(tab.start_update_button.isEnabled())
|
||||
self.assertTrue(tab.stop_update_button.isEnabled())
|
||||
self.assertEqual("开始更新:1 条", statuses[-1])
|
||||
@@ -752,6 +821,107 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_blocks_update_when_real_submit_switch_is_off(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
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"])
|
||||
statuses = []
|
||||
tab = ApplyTab(config=cfg, status_callback=statuses.append)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning, \
|
||||
mock.patch("app.gui.QMessageBox.question") as question, \
|
||||
mock.patch("app.gui.run_worker") as run_worker:
|
||||
tab.start_update()
|
||||
|
||||
message = warning.call_args[0][2]
|
||||
self.assertIn("允许真实提交线上商品", message)
|
||||
question.assert_not_called()
|
||||
run_worker.assert_not_called()
|
||||
self.assertIn("已阻止本次更新", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_blocks_cover_update_when_cover_switch_is_off(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
self.allow_shopee_update(cfg, allow_cover=False)
|
||||
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
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"])
|
||||
tab = ApplyTab(config=cfg)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning, \
|
||||
mock.patch("app.gui.QMessageBox.question") as question, \
|
||||
mock.patch("app.gui.run_worker") as run_worker:
|
||||
tab.start_update()
|
||||
|
||||
self.assertIn("允许更新封面", warning.call_args[0][2])
|
||||
question.assert_not_called()
|
||||
run_worker.assert_not_called()
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_tab_safety_error_limits_max_count_and_test_item(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
self.allow_shopee_update(cfg, item_id="51100639510", max_items=1)
|
||||
tab = ApplyTab(config=cfg)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
class Task:
|
||||
def __init__(self, item_id, new_cover_path=None):
|
||||
self.item_id = item_id
|
||||
self.new_cover_path = new_cover_path
|
||||
|
||||
max_error = tab._update_safety_error(
|
||||
[
|
||||
Task("51100639510"),
|
||||
Task("51100639510"),
|
||||
]
|
||||
)
|
||||
item_error = tab._update_safety_error([Task("51100639511")])
|
||||
|
||||
self.assertIn("超过单次最大更新条数", max_error)
|
||||
self.assertIn("非测试商品ID", item_error)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_apply_worker_applies_success_failure_and_unmatched_serially(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
@@ -793,11 +963,13 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
db.set_generated(task.id, "新标题", "new.jpg", path=cfg["db_path"])
|
||||
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
|
||||
applied_aliases = []
|
||||
close_flags = []
|
||||
progress = []
|
||||
rows = []
|
||||
|
||||
def fake_apply(account, task):
|
||||
def fake_apply(account, task, close_success_tab=False):
|
||||
applied_aliases.append(account.alias)
|
||||
close_flags.append(close_success_tab)
|
||||
if account.alias == "alias-a":
|
||||
return {"committed": True, "error": None}
|
||||
if account.alias == "alias-b":
|
||||
@@ -809,12 +981,18 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"app.gui.accounts.detect_login",
|
||||
return_value={"logged_in": True, "reason": None},
|
||||
), mock.patch("app.gui.editor.apply_task", side_effect=fake_apply):
|
||||
worker = ApplyWorker(tasks, db_path=cfg["db_path"], config=cfg)
|
||||
worker = ApplyWorker(
|
||||
tasks,
|
||||
db_path=cfg["db_path"],
|
||||
config=cfg,
|
||||
close_success_tab=True,
|
||||
)
|
||||
worker.progress.connect(progress.append)
|
||||
worker.row_updated.connect(lambda task_id, fields: rows.append((task_id, fields)))
|
||||
summary = worker.execute()
|
||||
|
||||
self.assertEqual(["alias-a", "alias-b"], applied_aliases)
|
||||
self.assertEqual([True, True], close_flags)
|
||||
self.assertEqual(
|
||||
{
|
||||
"ok": False,
|
||||
|
||||
Reference in New Issue
Block a user