Merge patrol delete changes into master
This commit is contained in:
@@ -34,6 +34,10 @@ RECOMMENDED_OFFER_EXPAND_XPATHS = [
|
|||||||
SCRIPT_DIR = Path(__file__).with_name("scripts") / "patrol_delete"
|
SCRIPT_DIR = Path(__file__).with_name("scripts") / "patrol_delete"
|
||||||
|
|
||||||
|
|
||||||
|
def _new_trace_id(prefix: str) -> str:
|
||||||
|
return f"{prefix}-{int(time.time() * 1000) % 1000000:06d}"
|
||||||
|
|
||||||
|
|
||||||
def _load_patrol_delete_script(name: str) -> str:
|
def _load_patrol_delete_script(name: str) -> str:
|
||||||
return (SCRIPT_DIR / name).read_text(encoding="utf-8")
|
return (SCRIPT_DIR / name).read_text(encoding="utf-8")
|
||||||
|
|
||||||
@@ -61,9 +65,12 @@ PRODUCT_TASK_MAX_RETRIES = 3
|
|||||||
PRODUCT_TASK_RETRY_DELAY = 2
|
PRODUCT_TASK_RETRY_DELAY = 2
|
||||||
PRODUCT_TASK_OPEN_SHOP_DELAY = 3
|
PRODUCT_TASK_OPEN_SHOP_DELAY = 3
|
||||||
PRODUCT_TASK_BROWSER_RESET_DELAY = 2
|
PRODUCT_TASK_BROWSER_RESET_DELAY = 2
|
||||||
DELETE_SUCCESS_MESSAGE_PARTS = (
|
DELETE_SUCCESS_COUNT_PATTERNS = (
|
||||||
re.compile(r"\d+\s*个商品(?:已删除|已经删除)"),
|
re.compile(r"(\d+)\s*个商品(?:已删除|已经删除)"),
|
||||||
re.compile(r"所[作做]更改需要\s*15\s*分钟才会显示在商品详情页面上"),
|
re.compile(r"(?:已删除|已经删除)\s*(\d+)\s*个商品"),
|
||||||
|
)
|
||||||
|
DELETE_SUCCESS_TEXT_PATTERNS = (
|
||||||
|
re.compile(r"一个或多个商品(?:已删除|已经删除)"),
|
||||||
)
|
)
|
||||||
LISTING_STATUS_DELETE_WHITELIST = {
|
LISTING_STATUS_DELETE_WHITELIST = {
|
||||||
"全部",
|
"全部",
|
||||||
@@ -195,10 +202,17 @@ class InventoryManage(AmamzonBase):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
def get_listing_status_country_sections(self, shop_name: str, country: str) -> List[Dict[str, Any]]:
|
def get_listing_status_country_sections(self, shop_name: str, country: str) -> List[Dict[str, Any]]:
|
||||||
|
trace_id = _new_trace_id("status")
|
||||||
|
logger.info(f"trace_id={trace_id} 开始读取商品状态统计: shop={shop_name}, country={country}")
|
||||||
rows = self._wait_listing_status_section_rows()
|
rows = self._wait_listing_status_section_rows()
|
||||||
parsed_rows = self._parse_listing_status_section_rows(rows)
|
parsed_rows = self._parse_listing_status_section_rows(rows)
|
||||||
if not parsed_rows:
|
if not parsed_rows:
|
||||||
|
logger.info(f"trace_id={trace_id} 商品状态统计读取为空: raw_rows={json.dumps(rows, ensure_ascii=False)}")
|
||||||
raise RuntimeError(f"未读取到商品状态统计标签: {rows}")
|
raise RuntimeError(f"未读取到商品状态统计标签: {rows}")
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 商品状态统计读取完成: "
|
||||||
|
f"shop={shop_name}, country={country}, row_count={len(parsed_rows)}"
|
||||||
|
)
|
||||||
return [{"country": country, "rows": self._build_country_section_rows(parsed_rows)}]
|
return [{"country": country, "rows": self._build_country_section_rows(parsed_rows)}]
|
||||||
|
|
||||||
def delete_all_listings_from_non_whitelisted_statuses(
|
def delete_all_listings_from_non_whitelisted_statuses(
|
||||||
@@ -206,13 +220,13 @@ class InventoryManage(AmamzonBase):
|
|||||||
shop_name: str,
|
shop_name: str,
|
||||||
country: str,
|
country: str,
|
||||||
max_delete_operations: Optional[int] = None,
|
max_delete_operations: Optional[int] = None,
|
||||||
|
trace_id: str = "",
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
||||||
|
trace_id = trace_id or _new_trace_id("del")
|
||||||
logger.info(
|
logger.info(
|
||||||
"开始批量删除非白名单商品: shop={}, country={}, max_delete_operations={}",
|
f"trace_id={trace_id} 开始批量删除非白名单商品: "
|
||||||
shop_name,
|
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}"
|
||||||
country,
|
|
||||||
max_delete_operations,
|
|
||||||
)
|
)
|
||||||
all_results: List[Dict[str, Any]] = []
|
all_results: List[Dict[str, Any]] = []
|
||||||
delete_counts: Dict[str, int] = {}
|
delete_counts: Dict[str, int] = {}
|
||||||
@@ -221,10 +235,10 @@ class InventoryManage(AmamzonBase):
|
|||||||
total_deleted = 0
|
total_deleted = 0
|
||||||
stop_reason = "no-candidates"
|
stop_reason = "no-candidates"
|
||||||
|
|
||||||
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country)
|
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
||||||
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows)
|
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows)
|
||||||
if not latest_delete_candidates:
|
if not latest_delete_candidates:
|
||||||
logger.info("未找到可删除商品分类: shop={}, country={}", shop_name, country)
|
logger.info(f"trace_id={trace_id} 未找到可删除商品分类: shop={shop_name}, country={country}")
|
||||||
final_status_rows = latest_status_rows
|
final_status_rows = latest_status_rows
|
||||||
return {
|
return {
|
||||||
"shopName": shop_name,
|
"shopName": shop_name,
|
||||||
@@ -238,34 +252,27 @@ class InventoryManage(AmamzonBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"可删除商品分类已生成: shop={}, country={}, candidates={}",
|
f"trace_id={trace_id} 可删除商品分类已生成: shop={shop_name}, country={country}, "
|
||||||
shop_name,
|
f"candidates={self._format_deletable_candidates(latest_delete_candidates)}"
|
||||||
country,
|
|
||||||
self._format_deletable_candidates(latest_delete_candidates),
|
|
||||||
)
|
)
|
||||||
stop_reason = "processed-candidates"
|
stop_reason = "processed-candidates"
|
||||||
for candidate in latest_delete_candidates:
|
for candidate in latest_delete_candidates:
|
||||||
logger.info(
|
logger.info(
|
||||||
"开始处理可删除分类: shop={}, country={}, status={}, quantity={}",
|
f"trace_id={trace_id} 开始处理可删除分类: shop={shop_name}, country={country}, "
|
||||||
shop_name,
|
f"status={candidate['status']}, quantity={candidate['quantity']}"
|
||||||
country,
|
|
||||||
candidate["status"],
|
|
||||||
candidate["quantity"],
|
|
||||||
)
|
)
|
||||||
self.select_listing_status(candidate["status"])
|
self.select_listing_status(candidate["status"], trace_id=trace_id)
|
||||||
deleted_for_status = 0
|
deleted_for_status = 0
|
||||||
status_quantity = candidate["quantity"]
|
status_quantity = candidate["quantity"]
|
||||||
|
|
||||||
while status_quantity <= 0 or deleted_for_status < status_quantity:
|
while status_quantity <= 0 or deleted_for_status < status_quantity:
|
||||||
bulk_result = self._delete_selected_filtered_inventory_rows(candidate["status"])
|
bulk_result = self._delete_selected_filtered_inventory_rows(candidate["status"], trace_id=trace_id)
|
||||||
deleted_count = int(bulk_result.get("deletedCount") or 0)
|
deleted_count = int(bulk_result.get("deletedCount") or 0)
|
||||||
if deleted_count <= 0:
|
if deleted_count <= 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"当前分类未删除到商品,结束分类处理: shop={}, country={}, status={}, deleted_for_status={}",
|
f"trace_id={trace_id} 当前分类未删除到商品,结束分类处理: "
|
||||||
shop_name,
|
f"shop={shop_name}, country={country}, status={candidate['status']}, "
|
||||||
country,
|
f"deleted_for_status={deleted_for_status}"
|
||||||
candidate["status"],
|
|
||||||
deleted_for_status,
|
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -283,23 +290,18 @@ class InventoryManage(AmamzonBase):
|
|||||||
total_deleted += deleted_count
|
total_deleted += deleted_count
|
||||||
deleted_for_status += deleted_count
|
deleted_for_status += deleted_count
|
||||||
logger.info(
|
logger.info(
|
||||||
"批量删除成功: shop={}, country={}, status={}, deleted_count={}, status_deleted={}, total_deleted={}",
|
f"trace_id={trace_id} 批量删除成功: shop={shop_name}, country={country}, "
|
||||||
shop_name,
|
f"status={candidate['status']}, deleted_count={deleted_count}, "
|
||||||
country,
|
f"status_deleted={deleted_for_status}, total_deleted={total_deleted}, "
|
||||||
candidate["status"],
|
f"success_message={bulk_result.get('successMessage', '')}"
|
||||||
deleted_count,
|
|
||||||
deleted_for_status,
|
|
||||||
total_deleted,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if max_delete_operations is not None and total_deleted >= max_delete_operations:
|
if max_delete_operations is not None and total_deleted >= max_delete_operations:
|
||||||
stop_reason = "max-delete-operations"
|
stop_reason = "max-delete-operations"
|
||||||
logger.info(
|
logger.info(
|
||||||
"达到最大删除次数,准备停止: shop={}, country={}, total_deleted={}, limit={}",
|
f"trace_id={trace_id} 达到最大删除次数,准备停止: "
|
||||||
shop_name,
|
f"shop={shop_name}, country={country}, total_deleted={total_deleted}, "
|
||||||
country,
|
f"limit={max_delete_operations}"
|
||||||
total_deleted,
|
|
||||||
max_delete_operations,
|
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -308,20 +310,14 @@ class InventoryManage(AmamzonBase):
|
|||||||
if stop_reason == "max-delete-operations":
|
if stop_reason == "max-delete-operations":
|
||||||
break
|
break
|
||||||
logger.info(
|
logger.info(
|
||||||
"可删除分类处理完成: shop={}, country={}, status={}, deleted_for_status={}",
|
f"trace_id={trace_id} 可删除分类处理完成: shop={shop_name}, country={country}, "
|
||||||
shop_name,
|
f"status={candidate['status']}, deleted_for_status={deleted_for_status}"
|
||||||
country,
|
|
||||||
candidate["status"],
|
|
||||||
deleted_for_status,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
final_status_rows = self._read_listing_status_rows_for_delete(shop_name, country)
|
final_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
"批量删除非白名单商品完成: shop={}, country={}, total_deleted={}, stop_reason={}",
|
f"trace_id={trace_id} 批量删除非白名单商品完成: "
|
||||||
shop_name,
|
f"shop={shop_name}, country={country}, total_deleted={total_deleted}, stop_reason={stop_reason}"
|
||||||
country,
|
|
||||||
total_deleted,
|
|
||||||
stop_reason,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -335,30 +331,42 @@ class InventoryManage(AmamzonBase):
|
|||||||
"stopReason": stop_reason,
|
"stopReason": stop_reason,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _read_listing_status_rows_for_delete(self, shop_name: str, country: str) -> List[Dict[str, str]]:
|
def _read_listing_status_rows_for_delete(
|
||||||
logger.info("开始读取商品状态行: shop={}, country={}", shop_name, country)
|
self,
|
||||||
|
shop_name: str,
|
||||||
|
country: str,
|
||||||
|
trace_id: str = "",
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
trace_id = trace_id or _new_trace_id("rows")
|
||||||
|
logger.info(f"trace_id={trace_id} 开始读取商品状态行: shop={shop_name}, country={country}")
|
||||||
option_rows = self.get_listing_status_dropdown_options(shop_name, country)
|
option_rows = self.get_listing_status_dropdown_options(shop_name, country)
|
||||||
rows = self._build_country_section_rows(option_rows)
|
rows = self._build_country_section_rows(option_rows)
|
||||||
logger.info("商品状态行读取完成: shop={}, country={}, row_count={}", shop_name, country, len(rows))
|
logger.info(f"trace_id={trace_id} 商品状态行读取完成: shop={shop_name}, country={country}, row_count={len(rows)}")
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def select_listing_status(self, status: str) -> Dict[str, Any]:
|
def select_listing_status(self, status: str, trace_id: str = "") -> Dict[str, Any]:
|
||||||
normalized_status = self._normalize_option_text(status)
|
normalized_status = self._normalize_option_text(status)
|
||||||
if not normalized_status:
|
if not normalized_status:
|
||||||
raise RuntimeError("商品状态为空,无法执行筛选")
|
raise RuntimeError("商品状态为空,无法执行筛选")
|
||||||
|
|
||||||
logger.info("开始筛选商品状态: status={}", normalized_status)
|
trace_id = trace_id or _new_trace_id("status")
|
||||||
|
logger.info(f"trace_id={trace_id} 开始筛选商品状态: status={normalized_status}")
|
||||||
open_result = self.open_listing_status_dropdown()
|
open_result = self.open_listing_status_dropdown()
|
||||||
option_rows = self._wait_listing_status_option_rows()
|
option_rows = self._wait_listing_status_option_rows()
|
||||||
if not option_rows:
|
if not option_rows:
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 商品状态下拉选项为空: "
|
||||||
|
f"open_result={json.dumps(open_result, ensure_ascii=False)}"
|
||||||
|
)
|
||||||
raise RuntimeError(f"商品状态下拉框已打开但未读取到选项: {open_result}")
|
raise RuntimeError(f"商品状态下拉框已打开但未读取到选项: {open_result}")
|
||||||
|
|
||||||
result = self._run_js(LISTING_STATUS_SELECT_OPTION_SCRIPT, {"status": normalized_status}) or {}
|
result = self._run_js(LISTING_STATUS_SELECT_OPTION_SCRIPT, {"status": normalized_status}) or {}
|
||||||
if not result.get("ok"):
|
if not result.get("ok"):
|
||||||
|
logger.info(f"trace_id={trace_id} 商品状态选项未命中: status={normalized_status}, result={result}")
|
||||||
raise RuntimeError(f"未找到商品状态选项[{normalized_status}]: {result}")
|
raise RuntimeError(f"未找到商品状态选项[{normalized_status}]: {result}")
|
||||||
time.sleep(LISTING_STATUS_SELECT_DELAY)
|
time.sleep(LISTING_STATUS_SELECT_DELAY)
|
||||||
self._wait_inventory_loader()
|
self._wait_inventory_loader()
|
||||||
logger.info("商品状态筛选完成: status={}, result={}", normalized_status, result)
|
logger.info(f"trace_id={trace_id} 商品状态筛选完成: status={normalized_status}, result={result}")
|
||||||
return {"mode": "dropdown-option", **result}
|
return {"mode": "dropdown-option", **result}
|
||||||
|
|
||||||
def _close_listing_status_dropdown(self) -> None:
|
def _close_listing_status_dropdown(self) -> None:
|
||||||
@@ -394,22 +402,22 @@ class InventoryManage(AmamzonBase):
|
|||||||
logger.info("状态[{}]未找到可删除商品行: {}", status, exc)
|
logger.info("状态[{}]未找到可删除商品行: {}", status, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _delete_selected_filtered_inventory_rows(self, status: str) -> Dict[str, Any]:
|
def _delete_selected_filtered_inventory_rows(self, status: str, trace_id: str = "") -> Dict[str, Any]:
|
||||||
logger.info("开始批量删除当前筛选结果: status={}", status)
|
trace_id = trace_id or _new_trace_id("del")
|
||||||
|
logger.info(f"trace_id={trace_id} 开始批量删除当前筛选结果: status={status}")
|
||||||
row = self._get_first_inventory_row_or_none(status)
|
row = self._get_first_inventory_row_or_none(status)
|
||||||
if row is None:
|
if row is None:
|
||||||
logger.info("当前筛选结果没有可删除商品: status={}", status)
|
logger.info(f"trace_id={trace_id} 当前筛选结果没有可删除商品: status={status}")
|
||||||
return {"deletedCount": 0, "target": {}, "successMessage": ""}
|
return {"deletedCount": 0, "target": {}, "successMessage": ""}
|
||||||
|
|
||||||
target = self._summarize_inventory_row(row)
|
target = self._summarize_inventory_row(row)
|
||||||
select_result = self._run_js(INVENTORY_SELECT_ALL_SCRIPT) or {}
|
select_result = self._run_js(INVENTORY_SELECT_ALL_SCRIPT) or {}
|
||||||
if not select_result.get("ok"):
|
if not select_result.get("ok"):
|
||||||
|
logger.info(f"trace_id={trace_id} 全选商品失败: status={status}, result={select_result}")
|
||||||
raise RuntimeError(f"状态[{status}]全选商品失败: {select_result}")
|
raise RuntimeError(f"状态[{status}]全选商品失败: {select_result}")
|
||||||
logger.info(
|
logger.info(
|
||||||
"当前筛选结果已全选: status={}, row_count={}, first_sku={}",
|
f"trace_id={trace_id} 当前筛选结果已全选: status={status}, "
|
||||||
status,
|
f"row_count={select_result.get('rowCount')}, first_sku={target.get('sku', '')}"
|
||||||
select_result.get("rowCount"),
|
|
||||||
target.get("sku", ""),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
delete_result: Dict[str, Any] = {}
|
delete_result: Dict[str, Any] = {}
|
||||||
@@ -419,18 +427,22 @@ class InventoryManage(AmamzonBase):
|
|||||||
if delete_result.get("ok"):
|
if delete_result.get("ok"):
|
||||||
break
|
break
|
||||||
if delete_result.get("menuOpened"):
|
if delete_result.get("menuOpened"):
|
||||||
logger.info("状态[{}]已点击选择组操作,等待删除菜单展开: {}", status, delete_result)
|
logger.info(f"trace_id={trace_id} 状态[{status}]已点击选择组操作,等待删除菜单展开: {delete_result}")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
logger.info("状态[{}]第{}次点击批量删除未命中: {}", status, attempt + 1, delete_result)
|
logger.info(f"trace_id={trace_id} 状态[{status}]第{attempt + 1}次点击批量删除未命中: {delete_result}")
|
||||||
if not delete_result.get("ok"):
|
if not delete_result.get("ok"):
|
||||||
|
logger.info(f"trace_id={trace_id} 点击批量删除失败: status={status}, result={delete_result}")
|
||||||
raise RuntimeError(f"状态[{status}]点击批量删除失败: {delete_result}")
|
raise RuntimeError(f"状态[{status}]点击批量删除失败: {delete_result}")
|
||||||
logger.info("批量删除按钮点击成功: status={}, result={}", status, delete_result)
|
logger.info(f"trace_id={trace_id} 批量删除按钮点击成功: status={status}, result={delete_result}")
|
||||||
|
|
||||||
self._confirm_delete_modal(status)
|
self._confirm_delete_modal(status, trace_id=trace_id)
|
||||||
success_message = self._wait_delete_success_message(status)
|
success_message = self._wait_delete_success_message(status, trace_id=trace_id)
|
||||||
deleted_count = self._extract_delete_success_count(success_message) or int(select_result.get("rowCount") or 1)
|
deleted_count = self._extract_delete_success_count(success_message) or int(select_result.get("rowCount") or 1)
|
||||||
logger.info("批量删除确认完成: status={}, deleted_count={}, success_message={}", status, deleted_count, success_message)
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 批量删除确认完成: status={status}, "
|
||||||
|
f"deleted_count={deleted_count}, success_message={success_message}"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"deletedCount": deleted_count,
|
"deletedCount": deleted_count,
|
||||||
"target": {
|
"target": {
|
||||||
@@ -456,24 +468,26 @@ class InventoryManage(AmamzonBase):
|
|||||||
text = ""
|
text = ""
|
||||||
return {"sku": sku, "rawText": text[:400]}
|
return {"sku": sku, "rawText": text[:400]}
|
||||||
|
|
||||||
def _confirm_delete_modal(self, status: str) -> None:
|
def _confirm_delete_modal(self, status: str, trace_id: str = "") -> None:
|
||||||
|
trace_id = trace_id or _new_trace_id("modal")
|
||||||
js_result = self._click_confirm_delete_modal_with_js()
|
js_result = self._click_confirm_delete_modal_with_js()
|
||||||
if js_result.get("ok"):
|
if js_result.get("ok"):
|
||||||
logger.info("已通过 JS 确认删除弹窗: status={}, result={}", status, js_result)
|
logger.info(f"trace_id={trace_id} 已通过 JS 确认删除弹窗: status={status}, result={js_result}")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("JS 确认删除弹窗未命中,尝试驱动点击: status={}, result={}", status, js_result)
|
logger.info(f"trace_id={trace_id} JS 确认删除弹窗未命中,尝试驱动点击: status={status}, result={js_result}")
|
||||||
confirm_button = self.tab.ele(
|
confirm_button = self.tab.ele(
|
||||||
'xpath://kat-modal[@data-testid="action-modal"]//kat-button[@variant="primary"]',
|
'xpath://kat-modal[@data-testid="action-modal"]//kat-button[@variant="primary"]',
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
if not confirm_button:
|
if not confirm_button:
|
||||||
|
logger.info(f"trace_id={trace_id} 删除确认弹窗未出现: status={status}")
|
||||||
raise RuntimeError(f"状态[{status}]未出现删除确认弹窗")
|
raise RuntimeError(f"状态[{status}]未出现删除确认弹窗")
|
||||||
|
|
||||||
confirm_button.wait.displayed(raise_err=False)
|
confirm_button.wait.displayed(raise_err=False)
|
||||||
confirm_button.wait.enabled()
|
confirm_button.wait.enabled()
|
||||||
confirm_button.click()
|
confirm_button.click()
|
||||||
logger.info("已通过驱动确认删除弹窗: status={}", status)
|
logger.info(f"trace_id={trace_id} 已通过驱动确认删除弹窗: status={status}")
|
||||||
|
|
||||||
def _click_confirm_delete_modal_with_js(self) -> Dict[str, Any]:
|
def _click_confirm_delete_modal_with_js(self) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
@@ -482,7 +496,8 @@ class InventoryManage(AmamzonBase):
|
|||||||
logger.info("JS 点击删除确认按钮失败,回退到驱动点击: {}", exc)
|
logger.info("JS 点击删除确认按钮失败,回退到驱动点击: {}", exc)
|
||||||
return {"ok": False, "reason": str(exc)}
|
return {"ok": False, "reason": str(exc)}
|
||||||
|
|
||||||
def _wait_delete_success_message(self, status: str) -> str:
|
def _wait_delete_success_message(self, status: str, trace_id: str = "") -> str:
|
||||||
|
trace_id = trace_id or _new_trace_id("toast")
|
||||||
deadline = time.time() + DELETE_SUCCESS_TIMEOUT
|
deadline = time.time() + DELETE_SUCCESS_TIMEOUT
|
||||||
seen_messages = []
|
seen_messages = []
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -492,10 +507,11 @@ class InventoryManage(AmamzonBase):
|
|||||||
if text not in seen_messages:
|
if text not in seen_messages:
|
||||||
seen_messages.append(text)
|
seen_messages.append(text)
|
||||||
if self._is_delete_success_message(text):
|
if self._is_delete_success_message(text):
|
||||||
logger.info("读取到删除成功提示: status={}, message={}", status, text)
|
logger.info(f"trace_id={trace_id} 读取到删除成功提示: status={status}, message={text}")
|
||||||
return text
|
return text
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
raise RuntimeError(f"状态[{status}]删除后未读取到成功提示: {seen_messages}")
|
logger.info(f"trace_id={trace_id} 删除后未读取到成功提示: status={status}, seen_messages={seen_messages}")
|
||||||
|
raise RuntimeError("删除后未读取到成功提示")
|
||||||
|
|
||||||
def _collect_delete_success_messages(self) -> List[str]:
|
def _collect_delete_success_messages(self) -> List[str]:
|
||||||
messages: List[str] = []
|
messages: List[str] = []
|
||||||
@@ -542,12 +558,18 @@ class InventoryManage(AmamzonBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_delete_success_message(text: str) -> bool:
|
def _is_delete_success_message(text: str) -> bool:
|
||||||
normalized = re.sub(r"\s+", "", text or "")
|
normalized = re.sub(r"\s+", "", text or "")
|
||||||
return all(pattern.search(normalized) for pattern in DELETE_SUCCESS_MESSAGE_PARTS)
|
if InventoryManage._extract_delete_success_count(normalized) > 0:
|
||||||
|
return True
|
||||||
|
return any(pattern.search(normalized) for pattern in DELETE_SUCCESS_TEXT_PATTERNS)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_delete_success_count(text: str) -> int:
|
def _extract_delete_success_count(text: str) -> int:
|
||||||
match = re.search(r"(\d+)\s*个商品(?:已删除|已经删除)", text or "")
|
normalized = re.sub(r"\s+", "", text or "")
|
||||||
return int(match.group(1)) if match else 0
|
for pattern in DELETE_SUCCESS_COUNT_PATTERNS:
|
||||||
|
match = pattern.search(normalized)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
return 0
|
||||||
|
|
||||||
def _wait_complete_draft_quick_view_tag_rows(self) -> List[Dict[str, Any]]:
|
def _wait_complete_draft_quick_view_tag_rows(self) -> List[Dict[str, Any]]:
|
||||||
deadline = time.time() + QUICK_VIEW_TAGS_TIMEOUT
|
deadline = time.time() + QUICK_VIEW_TAGS_TIMEOUT
|
||||||
@@ -1237,8 +1259,8 @@ class PatrolDeleteTask:
|
|||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
def log(self, message: str, level: str = "INFO"):
|
def log(self, message: str, level: str = "INFO"):
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
level_name = str(level or "INFO").upper()
|
||||||
print(f"[{timestamp}] [ProductTask] [{level}] {message}")
|
logger.log(level_name, f"[ProductTask] {message}")
|
||||||
|
|
||||||
def process_task(self, task_data: dict):
|
def process_task(self, task_data: dict):
|
||||||
"""处理巡店删除任务主入口。"""
|
"""处理巡店删除任务主入口。"""
|
||||||
@@ -1475,22 +1497,28 @@ class PatrolDeleteTask:
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
from config import runing_task
|
from config import runing_task
|
||||||
|
|
||||||
self.log(f"开始处理国家: {country_name}")
|
trace_id = _new_trace_id("country")
|
||||||
|
self.log(f"trace_id={trace_id} 开始处理国家: task_id={task_id}, shop={shop_name}, country={country_name}")
|
||||||
show_notification(f"开始处理巡店删除国家: {country_name}", "info")
|
show_notification(f"开始处理巡店删除国家: {country_name}", "info")
|
||||||
if task_id in runing_task:
|
if task_id in runing_task:
|
||||||
runing_task[task_id]["current_country"] = country_name
|
runing_task[task_id]["current_country"] = country_name
|
||||||
|
|
||||||
latest_status_rows: List[Dict[str, Any]] = []
|
latest_status_rows: List[Dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
|
self.log(f"trace_id={trace_id} 准备切换国家: country={country_name}")
|
||||||
self._switch_country_with_retry(driver, country_name)
|
self._switch_country_with_retry(driver, country_name)
|
||||||
self.log(f"国家 {country_name} 切换完成,准备进入管理所有库存页面")
|
self.log(f"trace_id={trace_id} 国家切换完成,准备进入管理所有库存页面: country={country_name}")
|
||||||
driver.switch_to_manage_inventory()
|
driver.switch_to_manage_inventory()
|
||||||
self.log(f"国家 {country_name} 已进入管理所有库存页面,开始删除非白名单商品")
|
self.log(f"trace_id={trace_id} 已进入管理所有库存页面,开始删除非白名单商品: country={country_name}")
|
||||||
|
|
||||||
delete_result = driver.delete_all_listings_from_non_whitelisted_statuses(shop_name, country_name)
|
delete_result = driver.delete_all_listings_from_non_whitelisted_statuses(
|
||||||
|
shop_name,
|
||||||
|
country_name,
|
||||||
|
trace_id=trace_id,
|
||||||
|
)
|
||||||
latest_status_rows = delete_result.get("statusRows") or []
|
latest_status_rows = delete_result.get("statusRows") or []
|
||||||
self.log(
|
self.log(
|
||||||
f"国家 {country_name} 删除步骤完成,deleted={delete_result.get('totalDeleted', 0)},"
|
f"trace_id={trace_id} 国家 {country_name} 删除步骤完成,deleted={delete_result.get('totalDeleted', 0)},"
|
||||||
f"stopReason={delete_result.get('stopReason', '')}"
|
f"stopReason={delete_result.get('stopReason', '')}"
|
||||||
)
|
)
|
||||||
country_section = self._build_country_section_result(
|
country_section = self._build_country_section_result(
|
||||||
@@ -1503,7 +1531,7 @@ class PatrolDeleteTask:
|
|||||||
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
|
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
|
||||||
|
|
||||||
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
||||||
self.log(f"国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
|
self.log(f"trace_id={trace_id} 国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -1513,7 +1541,7 @@ class PatrolDeleteTask:
|
|||||||
"reopenRequired": False,
|
"reopenRequired": False,
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log(f"处理国家 {country_name} 失败: {str(exc)}", "ERROR")
|
self.log(f"trace_id={trace_id} 处理国家 {country_name} 失败: {str(exc)}", "ERROR")
|
||||||
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
||||||
failure_rows = latest_status_rows or [{"status": "全部", "quantity": "", "deleteQuantity": "", "processStatus": ""}]
|
failure_rows = latest_status_rows or [{"status": "全部", "quantity": "", "deleteQuantity": "", "processStatus": ""}]
|
||||||
country_section = self._build_country_section_result(
|
country_section = self._build_country_section_result(
|
||||||
@@ -1531,14 +1559,17 @@ class PatrolDeleteTask:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _read_country_cart_ratio_safe(self, driver: InventoryManage, country_name: str) -> Dict[str, str]:
|
def _read_country_cart_ratio_safe(self, driver: InventoryManage, country_name: str) -> Dict[str, str]:
|
||||||
|
trace_id = _new_trace_id("cart")
|
||||||
try:
|
try:
|
||||||
self.log(f"开始读取国家 {country_name} 购物车比例")
|
self.log(f"trace_id={trace_id} 开始读取国家 {country_name} 购物车比例")
|
||||||
return self._extract_country_cart_ratio(
|
cart_ratio = self._extract_country_cart_ratio(
|
||||||
driver.get_recommended_offer_cart_ratios().get("cartRatios") or [],
|
driver.get_recommended_offer_cart_ratios().get("cartRatios") or [],
|
||||||
country_name,
|
country_name,
|
||||||
)
|
)
|
||||||
|
self.log(f"trace_id={trace_id} 国家 {country_name} 购物车比例读取成功: {cart_ratio.get('ratio', '')}")
|
||||||
|
return cart_ratio
|
||||||
except Exception as cart_exc:
|
except Exception as cart_exc:
|
||||||
self.log(f"读取国家 {country_name} 购物车比例失败: {str(cart_exc)}", "WARNING")
|
self.log(f"trace_id={trace_id} 读取国家 {country_name} 购物车比例失败: {str(cart_exc)}", "WARNING")
|
||||||
return self._empty_cart_ratio(country_name)
|
return self._empty_cart_ratio(country_name)
|
||||||
|
|
||||||
def _read_complete_draft_country_section_safe(
|
def _read_complete_draft_country_section_safe(
|
||||||
@@ -1547,8 +1578,9 @@ class PatrolDeleteTask:
|
|||||||
shop_name: str,
|
shop_name: str,
|
||||||
country_name: str,
|
country_name: str,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
trace_id = _new_trace_id("draft")
|
||||||
try:
|
try:
|
||||||
self.log(f"开始读取国家 {country_name} 补全草稿")
|
self.log(f"trace_id={trace_id} 开始读取国家 {country_name} 补全草稿")
|
||||||
tags = driver.get_complete_draft_quick_view_tags(shop_name, country_name)
|
tags = driver.get_complete_draft_quick_view_tags(shop_name, country_name)
|
||||||
rows = []
|
rows = []
|
||||||
for item in tags:
|
for item in tags:
|
||||||
@@ -1556,22 +1588,26 @@ class PatrolDeleteTask:
|
|||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"type": "completeDraft",
|
"type": "completeDraft",
|
||||||
|
"status": PatrolDeleteTask._string_result_field(item.get("tag")),
|
||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
"deleteQuantity": quantity,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.log(f"国家 {country_name} 补全草稿读取完成: rows={rows}")
|
self.log(f"trace_id={trace_id} 国家 {country_name} 补全草稿读取完成: rows={rows}")
|
||||||
return {"country": country_name, "rows": rows} if rows else None
|
return {"country": country_name, "rows": rows} if rows else None
|
||||||
except Exception as draft_exc:
|
except Exception as draft_exc:
|
||||||
self.log(f"读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
self.log(f"trace_id={trace_id} 读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
|
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
|
||||||
|
trace_id = _new_trace_id("switch")
|
||||||
for retry in range(PRODUCT_TASK_MAX_RETRIES):
|
for retry in range(PRODUCT_TASK_MAX_RETRIES):
|
||||||
try:
|
try:
|
||||||
self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{PRODUCT_TASK_MAX_RETRIES} 次)")
|
self.log(
|
||||||
|
f"trace_id={trace_id} 尝试切换到国家 {country_name} "
|
||||||
|
f"(第 {retry + 1}/{PRODUCT_TASK_MAX_RETRIES} 次)"
|
||||||
|
)
|
||||||
if driver.switch_to_country(country_name):
|
if driver.switch_to_country(country_name):
|
||||||
self.log(f"成功切换到国家 {country_name}")
|
self.log(f"trace_id={trace_id} 成功切换到国家 {country_name}")
|
||||||
return
|
return
|
||||||
if retry < PRODUCT_TASK_MAX_RETRIES - 1:
|
if retry < PRODUCT_TASK_MAX_RETRIES - 1:
|
||||||
try:
|
try:
|
||||||
@@ -1582,6 +1618,7 @@ class PatrolDeleteTask:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if retry >= PRODUCT_TASK_MAX_RETRIES - 1:
|
if retry >= PRODUCT_TASK_MAX_RETRIES - 1:
|
||||||
raise RuntimeError(f"切换国家 {country_name} 失败: {exc}") from exc
|
raise RuntimeError(f"切换国家 {country_name} 失败: {exc}") from exc
|
||||||
|
self.log(f"trace_id={trace_id} 切换国家 {country_name} 异常,准备重试: {str(exc)}", "WARNING")
|
||||||
time.sleep(PRODUCT_TASK_RETRY_DELAY)
|
time.sleep(PRODUCT_TASK_RETRY_DELAY)
|
||||||
raise RuntimeError(f"切换国家 {country_name} 失败")
|
raise RuntimeError(f"切换国家 {country_name} 失败")
|
||||||
|
|
||||||
@@ -1597,6 +1634,7 @@ class PatrolDeleteTask:
|
|||||||
"""回传巡店删除结果到 Java API。"""
|
"""回传巡店删除结果到 Java API。"""
|
||||||
from config import DELETE_BRAND_API_BASE
|
from config import DELETE_BRAND_API_BASE
|
||||||
|
|
||||||
|
trace_id = _new_trace_id("post")
|
||||||
shop_payload: Dict[str, Any] = {
|
shop_payload: Dict[str, Any] = {
|
||||||
"shopName": shop_name,
|
"shopName": shop_name,
|
||||||
"shopDone": shop_done,
|
"shopDone": shop_done,
|
||||||
@@ -1613,13 +1651,14 @@ class PatrolDeleteTask:
|
|||||||
|
|
||||||
payload = {"shops": [shop_payload]}
|
payload = {"shops": [shop_payload]}
|
||||||
url = f"{DELETE_BRAND_API_BASE}/api/patrol-delete/tasks/{task_id}/result"
|
url = f"{DELETE_BRAND_API_BASE}/api/patrol-delete/tasks/{task_id}/result"
|
||||||
|
self._log_result_payload_summary(task_id, shop_payload, trace_id=trace_id)
|
||||||
|
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
for retry in range(max_retries):
|
for retry in range(max_retries):
|
||||||
try:
|
try:
|
||||||
self.log(f"尝试回传巡店删除结果 (第 {retry + 1}/{max_retries} 次)")
|
self.log(f"trace_id={trace_id} 尝试回传巡店删除结果 (第 {retry + 1}/{max_retries} 次)")
|
||||||
self.log(f"回传URL: {url}")
|
self.log(f"trace_id={trace_id} 回传URL: {url}")
|
||||||
self.log(f"回传数据: {payload}")
|
self.log(f"trace_id={trace_id} 回传数据: {payload}")
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url,
|
url,
|
||||||
json=payload,
|
json=payload,
|
||||||
@@ -1627,27 +1666,60 @@ class PatrolDeleteTask:
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
verify=False,
|
verify=False,
|
||||||
)
|
)
|
||||||
self.log(f"回传结果: {response.text}")
|
self.log(f"trace_id={trace_id} 回传结果: {response.text}")
|
||||||
try:
|
try:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
except (AttributeError, ValueError):
|
except (AttributeError, ValueError):
|
||||||
response_data = None
|
response_data = None
|
||||||
api_success = not (isinstance(response_data, dict) and response_data.get("success") is False)
|
api_success = not (isinstance(response_data, dict) and response_data.get("success") is False)
|
||||||
|
self.log(
|
||||||
|
f"trace_id={trace_id} 巡店删除结果回传响应: status_code={response.status_code}, "
|
||||||
|
f"api_success={api_success}, response={response.text}"
|
||||||
|
)
|
||||||
if response.status_code == 200 and api_success:
|
if response.status_code == 200 and api_success:
|
||||||
self.log(f"巡店删除结果回传成功: {shop_name}")
|
self.log(f"trace_id={trace_id} 巡店删除结果回传成功: {shop_name}")
|
||||||
return
|
return
|
||||||
self.log(
|
self.log(
|
||||||
f"巡店删除结果回传失败,状态码: {response.status_code}, 响应: {response.text}",
|
f"trace_id={trace_id} 巡店删除结果回传失败,状态码: {response.status_code}, 响应: {response.text}",
|
||||||
"WARNING",
|
"WARNING",
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log(f"调用巡店删除结果API异常: {str(exc)}", "ERROR")
|
self.log(f"trace_id={trace_id} 调用巡店删除结果API异常: {str(exc)}", "ERROR")
|
||||||
|
|
||||||
if retry < max_retries - 1:
|
if retry < max_retries - 1:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
raise RuntimeError("巡店删除结果回传最终失败")
|
raise RuntimeError("巡店删除结果回传最终失败")
|
||||||
|
|
||||||
|
def _log_result_payload_summary(self, task_id: int, shop_payload: Dict[str, Any], trace_id: str = "") -> None:
|
||||||
|
trace_id = trace_id or _new_trace_id("post")
|
||||||
|
country_sections = shop_payload.get("countrySections") or []
|
||||||
|
cart_ratios = shop_payload.get("cartRatios") or []
|
||||||
|
countries = [str(section.get("country") or "").strip() for section in country_sections]
|
||||||
|
row_count = sum(len(section.get("rows") or []) for section in country_sections)
|
||||||
|
deleted_total = self._sum_result_delete_quantity(country_sections)
|
||||||
|
self.log(
|
||||||
|
f"trace_id={trace_id} 巡店删除结果回传摘要: "
|
||||||
|
f"task_id={task_id}, shop={shop_payload.get('shopName', '')}, "
|
||||||
|
f"shopDone={shop_payload.get('shopDone', False)}, "
|
||||||
|
f"hasError={bool(shop_payload.get('error'))}, "
|
||||||
|
f"countryCount={len(country_sections)}, countries={','.join(countries)}, "
|
||||||
|
f"rowCount={row_count}, cartRatioCount={len(cart_ratios)}, deletedTotal={deleted_total}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sum_result_delete_quantity(country_sections: List[Dict[str, Any]]) -> int:
|
||||||
|
total = 0
|
||||||
|
for section in country_sections or []:
|
||||||
|
for row in section.get("rows") or []:
|
||||||
|
text = str(row.get("deleteQuantity") or "").replace(",", "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
match = re.search(r"\d+", text)
|
||||||
|
if match:
|
||||||
|
total += int(match.group(0))
|
||||||
|
return total
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_country_sections_for_result(
|
def _normalize_country_sections_for_result(
|
||||||
cls, country_sections: Optional[List[Dict[str, Any]]]
|
cls, country_sections: Optional[List[Dict[str, Any]]]
|
||||||
@@ -1657,14 +1729,16 @@ class PatrolDeleteTask:
|
|||||||
country = str(section.get("country") or "").strip()
|
country = str(section.get("country") or "").strip()
|
||||||
rows = []
|
rows = []
|
||||||
for row in section.get("rows") or []:
|
for row in section.get("rows") or []:
|
||||||
normalized_row = {
|
if cls._is_complete_draft_result_row(row):
|
||||||
"quantity": cls._string_result_field(row.get("quantity")),
|
|
||||||
"deleteQuantity": cls._string_result_field(row.get("deleteQuantity")),
|
|
||||||
}
|
|
||||||
if not cls._is_complete_draft_result_row(row):
|
|
||||||
normalized_row = {
|
normalized_row = {
|
||||||
"status": cls._string_result_field(row.get("status")),
|
"status": cls._string_result_field(row.get("status")),
|
||||||
**normalized_row,
|
"quantity": cls._string_result_field(row.get("quantity")),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
normalized_row = {
|
||||||
|
"status": cls._string_result_field(row.get("status")),
|
||||||
|
"quantity": cls._string_result_field(row.get("quantity")),
|
||||||
|
"deleteQuantity": cls._string_result_field(row.get("deleteQuantity")),
|
||||||
"processStatus": cls._string_result_field(row.get("processStatus")),
|
"processStatus": cls._string_result_field(row.get("processStatus")),
|
||||||
}
|
}
|
||||||
rows.append(normalized_row)
|
rows.append(normalized_row)
|
||||||
@@ -1820,8 +1894,11 @@ class PatrolDeleteTask:
|
|||||||
if not process_status and default_process_status and not default_status_applied:
|
if not process_status and default_process_status and not default_status_applied:
|
||||||
process_status = default_process_status
|
process_status = default_process_status
|
||||||
default_status_applied = True
|
default_status_applied = True
|
||||||
delete_quantity = str(delete_count) if delete_count > 0 else ""
|
if canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
||||||
if default_process_status == "无可删商品":
|
delete_quantity = "无需删除"
|
||||||
|
else:
|
||||||
|
delete_quantity = str(delete_count) if delete_count > 0 else ""
|
||||||
|
if default_process_status == "无可删商品" and canonical_status not in LISTING_STATUS_DELETE_WHITELIST:
|
||||||
delete_quantity = "0"
|
delete_quantity = "0"
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
@@ -1837,7 +1914,7 @@ class PatrolDeleteTask:
|
|||||||
{
|
{
|
||||||
"status": "全部",
|
"status": "全部",
|
||||||
"quantity": "",
|
"quantity": "",
|
||||||
"deleteQuantity": "0" if default_process_status == "无可删商品" else "",
|
"deleteQuantity": "无需删除" if default_process_status == "无可删商品" else "",
|
||||||
"processStatus": default_process_status,
|
"processStatus": default_process_status,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1866,6 +1943,33 @@ class PatrolDeleteTask:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _short_error(message: str, limit: int = 80) -> str:
|
def _short_error(message: str, limit: int = 80) -> str:
|
||||||
compact = re.sub(r"\s+", " ", message or "").strip()
|
compact = re.sub(r"\s+", " ", message or "").strip()
|
||||||
|
if "删除后未读取到成功提示" in compact:
|
||||||
|
return "删除后未读取到成功提示"
|
||||||
|
if any(
|
||||||
|
keyword in compact
|
||||||
|
for keyword in ("与页面的连接已断开", "target frame detached", "disconnected", "connection disconnected")
|
||||||
|
):
|
||||||
|
return "浏览器连接中断"
|
||||||
|
if any(keyword in compact for keyword in ("stop_requested", "暂停请求", "用户取消", "任务取消", "取消任务", "interrupted")):
|
||||||
|
return "任务已中断"
|
||||||
|
if "切换国家" in compact and "失败" in compact:
|
||||||
|
return "切换国家失败"
|
||||||
|
|
||||||
|
simplified_errors = (
|
||||||
|
("浏览器页面不存在", "浏览器页面不存在"),
|
||||||
|
("当前页面对象不支持执行 JavaScript", "当前页面无法执行 JavaScript"),
|
||||||
|
("未找到可用商品状态下拉框", "未找到商品状态下拉框"),
|
||||||
|
("商品状态下拉框已打开但未读取到选项", "未读取到商品状态下拉选项"),
|
||||||
|
("未读取到商品状态统计标签", "未读取到商品状态统计标签"),
|
||||||
|
("未找到商品状态选项", "未找到商品状态选项"),
|
||||||
|
("商品状态为空", "商品状态为空"),
|
||||||
|
("全选商品失败", "全选商品失败"),
|
||||||
|
("点击批量删除失败", "点击批量删除失败"),
|
||||||
|
("未出现删除确认弹窗", "未出现删除确认弹窗"),
|
||||||
|
)
|
||||||
|
for keyword, label in simplified_errors:
|
||||||
|
if keyword in compact:
|
||||||
|
return label
|
||||||
if len(compact) <= limit:
|
if len(compact) <= limit:
|
||||||
return compact
|
return compact
|
||||||
return compact[: limit - 3] + "..."
|
return compact[: limit - 3] + "..."
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def test_patrol_delete_task_post_result_retries_business_failure(monkeypatch):
|
|||||||
assert len(calls) == 3
|
assert len(calls) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_country_sections_omits_status_fields_for_complete_draft_rows():
|
def test_normalize_country_sections_keeps_only_status_and_quantity_for_complete_draft_rows():
|
||||||
rows = PatrolDeleteTask._normalize_country_sections_for_result(
|
rows = PatrolDeleteTask._normalize_country_sections_for_result(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -132,7 +132,7 @@ def test_normalize_country_sections_omits_status_fields_for_complete_draft_rows(
|
|||||||
{
|
{
|
||||||
"country": "意大利",
|
"country": "意大利",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"quantity": "12", "deleteQuantity": "3"},
|
{"status": "商品信息草稿", "quantity": "12"},
|
||||||
{"status": "缺少的信息", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
{"status": "缺少的信息", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -143,15 +143,15 @@ def test_replace_complete_draft_rows_uses_quick_view_rows():
|
|||||||
country_section = {
|
country_section = {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
||||||
{"status": "商品信息草稿", "quantity": "", "deleteQuantity": "0", "processStatus": ""},
|
{"status": "商品信息草稿", "quantity": "", "deleteQuantity": "0", "processStatus": ""},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
complete_draft_section = {
|
complete_draft_section = {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"type": "completeDraft", "quantity": "1", "deleteQuantity": "1"},
|
{"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"},
|
||||||
{"type": "completeDraft", "quantity": "0", "deleteQuantity": "0"},
|
{"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,18 +160,18 @@ def test_replace_complete_draft_rows_uses_quick_view_rows():
|
|||||||
assert merged == {
|
assert merged == {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
||||||
{"type": "completeDraft", "quantity": "1", "deleteQuantity": "1"},
|
{"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"},
|
||||||
{"type": "completeDraft", "quantity": "0", "deleteQuantity": "0"},
|
{"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
assert PatrolDeleteTask._normalize_country_sections_for_result([merged]) == [
|
assert PatrolDeleteTask._normalize_country_sections_for_result([merged]) == [
|
||||||
{
|
{
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
||||||
{"quantity": "1", "deleteQuantity": "1"},
|
{"status": "未提交的草稿", "quantity": "1"},
|
||||||
{"quantity": "0", "deleteQuantity": "0"},
|
{"status": "已提交:提供缺少的信息", "quantity": "0"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -356,15 +356,15 @@ def test_delete_all_listings_from_non_whitelisted_statuses_deletes_each_row_in_s
|
|||||||
]
|
]
|
||||||
state = {"index": 0, "selected": [], "deleted": 0}
|
state = {"index": 0, "selected": [], "deleted": 0}
|
||||||
|
|
||||||
def fake_read_listing_status_rows_for_delete(shop_name, country):
|
def fake_read_listing_status_rows_for_delete(shop_name, country, **kwargs):
|
||||||
current = status_rows[min(state["index"], len(status_rows) - 1)]
|
current = status_rows[min(state["index"], len(status_rows) - 1)]
|
||||||
state["index"] += 1
|
state["index"] += 1
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def fake_select_listing_status(status):
|
def fake_select_listing_status(status, **kwargs):
|
||||||
state["selected"].append(status)
|
state["selected"].append(status)
|
||||||
|
|
||||||
def fake_delete_selected_filtered_inventory_rows(status):
|
def fake_delete_selected_filtered_inventory_rows(status, **kwargs):
|
||||||
state["deleted"] += 2
|
state["deleted"] += 2
|
||||||
return {
|
return {
|
||||||
"deletedCount": 2,
|
"deletedCount": 2,
|
||||||
@@ -656,10 +656,62 @@ def test_is_delete_success_message_accepts_both_copy_variants():
|
|||||||
assert InventoryManage._is_delete_success_message("所做更改需要 15 分钟才会显示在商品详情页面上。 1 个商品已删除。")
|
assert InventoryManage._is_delete_success_message("所做更改需要 15 分钟才会显示在商品详情页面上。 1 个商品已删除。")
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_delete_success_message_accepts_deleted_count_without_delay_notice():
|
||||||
|
assert InventoryManage._is_delete_success_message("1 个商品已删除。")
|
||||||
|
assert InventoryManage._is_delete_success_message("已删除 2 个商品。")
|
||||||
|
assert not InventoryManage._is_delete_success_message("0 个商品已删除。")
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_delete_success_message_accepts_deleted_without_exact_count():
|
||||||
|
assert InventoryManage._is_delete_success_message("一个或多个商品已删除。")
|
||||||
|
assert InventoryManage._is_delete_success_message("一个或多个商品已经删除")
|
||||||
|
assert InventoryManage._extract_delete_success_count("一个或多个商品已删除。") == 0
|
||||||
|
|
||||||
|
|
||||||
def test_extract_delete_success_count_accepts_success_message():
|
def test_extract_delete_success_count_accepts_success_message():
|
||||||
assert InventoryManage._extract_delete_success_count("所做更改需要 15 分钟才会显示在商品详情页面上。 3 个商品已删除。") == 3
|
assert InventoryManage._extract_delete_success_count("所做更改需要 15 分钟才会显示在商品详情页面上。 3 个商品已删除。") == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_error_simplifies_delete_success_message_timeout():
|
||||||
|
message = (
|
||||||
|
"状态[详情页面已删除]删除后未读取到成功提示: "
|
||||||
|
"['所做更改需要 15 分钟才会显示在商品详情页面上。 一个或多个商品已删除', "
|
||||||
|
"'莆田市城厢区从奇偶贸易...']"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PatrolDeleteTask._short_error(message) == "删除后未读取到成功提示"
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_error_simplifies_common_patrol_delete_failures():
|
||||||
|
examples = {
|
||||||
|
"检测到任务 12 的暂停请求,停止处理": "任务已中断",
|
||||||
|
"用户取消任务": "任务已中断",
|
||||||
|
"与页面的连接已断开: target frame detached": "浏览器连接中断",
|
||||||
|
"切换国家 德国 失败: element not found": "切换国家失败",
|
||||||
|
"未找到可用商品状态下拉框: {'ok': False, 'text': '...'}": "未找到商品状态下拉框",
|
||||||
|
"商品状态下拉框已打开但未读取到选项: {'ok': True}": "未读取到商品状态下拉选项",
|
||||||
|
"未找到商品状态选项[详情页面已删除]: {'rows': ['...']}": "未找到商品状态选项",
|
||||||
|
"状态[详情页面已删除]全选商品失败: {'ok': False, 'reason': '...'}": "全选商品失败",
|
||||||
|
"状态[详情页面已删除]点击批量删除失败: {'menuOpened': False, 'html': '...'}": "点击批量删除失败",
|
||||||
|
"状态[详情页面已删除]未出现删除确认弹窗": "未出现删除确认弹窗",
|
||||||
|
"当前页面对象不支持执行 JavaScript,无法探测 Shadow DOM": "当前页面无法执行 JavaScript",
|
||||||
|
}
|
||||||
|
|
||||||
|
for message, expected in examples.items():
|
||||||
|
assert PatrolDeleteTask._short_error(message) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_country_section_result_uses_simplified_delete_timeout_failure_reason():
|
||||||
|
section = PatrolDeleteTask._build_country_section_result(
|
||||||
|
country_name="英国",
|
||||||
|
status_rows=[{"status": "详情页面已删除", "quantity": "1"}],
|
||||||
|
delete_counts={},
|
||||||
|
default_process_status=f"处理失败: {PatrolDeleteTask._short_error('状态[详情页面已删除]删除后未读取到成功提示: []')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert section["rows"][0]["processStatus"] == "处理失败: 删除后未读取到成功提示"
|
||||||
|
|
||||||
|
|
||||||
def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletable_products():
|
def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletable_products():
|
||||||
section = PatrolDeleteTask._build_country_section_result(
|
section = PatrolDeleteTask._build_country_section_result(
|
||||||
country_name="德国",
|
country_name="德国",
|
||||||
@@ -670,7 +722,7 @@ def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletabl
|
|||||||
|
|
||||||
assert section == {
|
assert section == {
|
||||||
"country": "德国",
|
"country": "德国",
|
||||||
"rows": [{"status": "全部", "quantity": "12", "deleteQuantity": "0", "processStatus": "无可删商品"}],
|
"rows": [{"status": "全部", "quantity": "12", "deleteQuantity": "无需删除", "processStatus": "无可删商品"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -684,10 +736,36 @@ def test_build_country_section_result_sets_zero_delete_quantity_for_empty_no_del
|
|||||||
|
|
||||||
assert section == {
|
assert section == {
|
||||||
"country": "德国",
|
"country": "德国",
|
||||||
"rows": [{"status": "全部", "quantity": "", "deleteQuantity": "0", "processStatus": "无可删商品"}],
|
"rows": [{"status": "全部", "quantity": "", "deleteQuantity": "无需删除", "processStatus": "无可删商品"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_country_section_result_marks_all_whitelisted_statuses_as_no_delete_needed():
|
||||||
|
whitelist_statuses = [
|
||||||
|
"全部",
|
||||||
|
"定价问题",
|
||||||
|
"需要批准",
|
||||||
|
"在搜索结果中禁止显示",
|
||||||
|
"配送问题",
|
||||||
|
"缺少报价",
|
||||||
|
"已停售",
|
||||||
|
"不可售",
|
||||||
|
"在售",
|
||||||
|
]
|
||||||
|
|
||||||
|
section = PatrolDeleteTask._build_country_section_result(
|
||||||
|
country_name="爱尔兰",
|
||||||
|
status_rows=[{"status": status, "quantity": "1"} for status in whitelist_statuses]
|
||||||
|
+ [{"status": "缺少的信息", "quantity": "2"}],
|
||||||
|
delete_counts={"缺少的信息": 2},
|
||||||
|
)
|
||||||
|
|
||||||
|
rows_by_status = {row["status"]: row for row in section["rows"]}
|
||||||
|
for status in whitelist_statuses:
|
||||||
|
assert rows_by_status[status]["deleteQuantity"] == "无需删除"
|
||||||
|
assert rows_by_status["缺少的信息"]["deleteQuantity"] == "2"
|
||||||
|
|
||||||
|
|
||||||
def test_parse_label_quantity_text_accepts_complete_draft_sample():
|
def test_parse_label_quantity_text_accepts_complete_draft_sample():
|
||||||
assert InventoryManage._parse_label_quantity_text("未提交的草稿 (0)") == ("未提交的草稿", 0)
|
assert InventoryManage._parse_label_quantity_text("未提交的草稿 (0)") == ("未提交的草稿", 0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user