Fix patrol delete condition handling
This commit is contained in:
@@ -4,7 +4,7 @@ import time
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
@@ -38,6 +38,19 @@ def _load_patrol_delete_script(name: str) -> str:
|
||||
return (SCRIPT_DIR / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _new_trace_id(prefix: str = "trace") -> str:
|
||||
return f"{prefix}-{int(time.time() * 1000)}"
|
||||
|
||||
|
||||
def _safe_log_text(value: Any, limit: int = 2000) -> str:
|
||||
try:
|
||||
text = json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||
except Exception:
|
||||
text = repr(value)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
return text if len(text) <= limit else text[: limit - 3] + "..."
|
||||
|
||||
|
||||
LISTING_STATUS_DROPDOWN_OPEN_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_open.js")
|
||||
LISTING_STATUS_DROPDOWN_OPTIONS_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_options.js")
|
||||
LISTING_STATUS_DROPDOWN_CLOSE_SCRIPT = _load_patrol_delete_script("listing_status_dropdown_close.js")
|
||||
@@ -49,6 +62,9 @@ DELETE_CONFIRM_MODAL_CLICK_SCRIPT = _load_patrol_delete_script("delete_confirm_m
|
||||
DELETE_SUCCESS_MESSAGES_SCRIPT = _load_patrol_delete_script("delete_success_messages.js")
|
||||
COMPLETE_DRAFT_QUICK_VIEW_TAGS_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_tags.js")
|
||||
COMPLETE_DRAFT_QUICK_VIEW_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_diagnostic.js")
|
||||
COMPLETE_DRAFT_SELECT_TAG_SCRIPT = _load_patrol_delete_script("complete_draft_select_tag.js")
|
||||
COMPLETE_DRAFT_SELECT_ALL_SCRIPT = _load_patrol_delete_script("complete_draft_select_all.js")
|
||||
COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT = _load_patrol_delete_script("complete_draft_delete_selected.js")
|
||||
RECOMMENDED_OFFER_PERCENTAGE_OPEN_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_open.js")
|
||||
RECOMMENDED_OFFER_PERCENTAGE_ROWS_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_rows.js")
|
||||
RECOMMENDED_OFFER_PERCENTAGE_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_diagnostic.js")
|
||||
@@ -65,6 +81,10 @@ DELETE_SUCCESS_MESSAGE_PARTS = (
|
||||
re.compile(r"\d+\s*个商品(?:已删除|已经删除)"),
|
||||
re.compile(r"所[作做]更改需要\s*15\s*分钟才会显示在商品详情页面上"),
|
||||
)
|
||||
COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS = (
|
||||
re.compile(r"商品信息草稿(?:已删除|已经删除)"),
|
||||
re.compile(r"已删除\s*\d*\s*个?\s*商品信息草稿"),
|
||||
)
|
||||
LISTING_STATUS_DELETE_WHITELIST = {
|
||||
"全部",
|
||||
"定价问题",
|
||||
@@ -153,6 +173,229 @@ class InventoryManage(AmamzonBase):
|
||||
raise RuntimeError(f"补全草稿快速查看已打开但未读取到标签: {open_result}")
|
||||
return parsed_rows
|
||||
|
||||
def delete_complete_drafts_from_non_whitelisted_tags(
|
||||
self,
|
||||
shop_name: str,
|
||||
country: str,
|
||||
max_delete_operations: Optional[int] = None,
|
||||
trace_id: str = "",
|
||||
allowed_statuses: Optional[Iterable[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""按补全草稿快速查看的准确标签数量,删除非白名单标签下方商品。"""
|
||||
trace_id = trace_id or _new_trace_id("draft")
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 开始删除非白名单补全草稿: "
|
||||
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
|
||||
f"allowed_statuses={list(allowed_statuses) if allowed_statuses is not None else None}"
|
||||
)
|
||||
initial_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
|
||||
delete_candidates = self._build_deletable_complete_draft_tag_rows(initial_tags, allowed_statuses)
|
||||
results: List[Dict[str, Any]] = []
|
||||
delete_counts: Dict[str, int] = {}
|
||||
total_deleted = 0
|
||||
stop_reason = "no-candidates"
|
||||
|
||||
if not delete_candidates:
|
||||
logger.info(f"trace_id={trace_id} 补全草稿没有可删除标签: shop={shop_name}, country={country}")
|
||||
return {
|
||||
"shopName": shop_name,
|
||||
"country": country,
|
||||
"draftRows": initial_tags,
|
||||
"deleteCandidates": delete_candidates,
|
||||
"results": results,
|
||||
"deleteCounts": delete_counts,
|
||||
"totalDeleted": total_deleted,
|
||||
"stopReason": stop_reason,
|
||||
}
|
||||
|
||||
stop_reason = "processed-candidates"
|
||||
for candidate in delete_candidates:
|
||||
tag = candidate["tag"]
|
||||
quantity = candidate["quantity"]
|
||||
canonical_tag = candidate["canonicalTag"]
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 开始删除补全草稿标签下方商品: "
|
||||
f"shop={shop_name}, country={country}, tag={tag}, quantity={quantity}"
|
||||
)
|
||||
self.select_complete_draft_tag(tag, trace_id=trace_id)
|
||||
deleted_for_tag = 0
|
||||
|
||||
while quantity <= 0 or deleted_for_tag < quantity:
|
||||
remaining_limit = None
|
||||
if max_delete_operations is not None:
|
||||
remaining_limit = max(0, max_delete_operations - total_deleted)
|
||||
if remaining_limit <= 0:
|
||||
stop_reason = "max-delete-operations"
|
||||
break
|
||||
select_result = self._select_all_complete_draft_rows(
|
||||
tag,
|
||||
trace_id=trace_id,
|
||||
max_select_count=remaining_limit,
|
||||
)
|
||||
selected_count = int(select_result.get("selectedCount") or select_result.get("rowCount") or 0)
|
||||
if select_result.get("empty"):
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿标签下方已无商品,跳过删除: "
|
||||
f"shop={shop_name}, country={country}, tag={tag}"
|
||||
)
|
||||
break
|
||||
if selected_count <= 0:
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿标签下方没有可删除商品: "
|
||||
f"shop={shop_name}, country={country}, tag={tag}"
|
||||
)
|
||||
break
|
||||
|
||||
delete_result = self._click_complete_draft_delete_selected(tag, trace_id=trace_id)
|
||||
self._confirm_delete_modal(tag, trace_id=trace_id)
|
||||
self._wait_complete_draft_rows_after_delete(tag, select_result, trace_id=trace_id)
|
||||
deleted_count = min(selected_count, quantity - deleted_for_tag) if quantity > 0 else selected_count
|
||||
deleted_for_tag += deleted_count
|
||||
total_deleted += deleted_count
|
||||
delete_counts[canonical_tag] = delete_counts.get(canonical_tag, 0) + deleted_count
|
||||
results.append(
|
||||
{
|
||||
"tag": tag,
|
||||
"canonicalTag": canonical_tag,
|
||||
"tagQuantity": quantity,
|
||||
"deletedCount": deleted_count,
|
||||
"selectResult": select_result,
|
||||
"deleteResult": delete_result,
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿标签删除成功: "
|
||||
f"shop={shop_name}, country={country}, tag={tag}, "
|
||||
f"deleted_count={deleted_count}, tag_deleted={deleted_for_tag}, total_deleted={total_deleted}"
|
||||
)
|
||||
|
||||
if max_delete_operations is not None and total_deleted >= max_delete_operations:
|
||||
stop_reason = "max-delete-operations"
|
||||
break
|
||||
if quantity > 0 and deleted_for_tag >= quantity:
|
||||
break
|
||||
|
||||
if stop_reason == "max-delete-operations":
|
||||
break
|
||||
|
||||
final_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
|
||||
return {
|
||||
"shopName": shop_name,
|
||||
"country": country,
|
||||
"draftRows": final_tags,
|
||||
"deleteCandidates": delete_candidates,
|
||||
"results": results,
|
||||
"deleteCounts": delete_counts,
|
||||
"totalDeleted": total_deleted,
|
||||
"stopReason": stop_reason,
|
||||
}
|
||||
|
||||
def select_complete_draft_tag(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
|
||||
normalized_tag = self._normalize_option_text(tag)
|
||||
if not normalized_tag:
|
||||
raise RuntimeError("补全草稿标签为空,无法执行筛选")
|
||||
trace_id = trace_id or _new_trace_id("draft")
|
||||
result = self._run_js(COMPLETE_DRAFT_SELECT_TAG_SCRIPT, {"tag": normalized_tag}) or {}
|
||||
if not result.get("ok"):
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿快速查看标签未命中: "
|
||||
f"tag={normalized_tag}, result={_safe_log_text(result)}"
|
||||
)
|
||||
raise RuntimeError(f"未找到补全草稿标签[{normalized_tag}]: {result}")
|
||||
time.sleep(LISTING_STATUS_SELECT_DELAY)
|
||||
return result
|
||||
|
||||
def _select_all_complete_draft_rows(
|
||||
self,
|
||||
tag: str,
|
||||
trace_id: str = "",
|
||||
max_select_count: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
trace_id = trace_id or _new_trace_id("draft")
|
||||
deadline = time.time() + INVENTORY_ROW_TIMEOUT
|
||||
result: Dict[str, Any] = {}
|
||||
saw_empty = False
|
||||
while time.time() < deadline:
|
||||
payload = {"maxSelectCount": max_select_count} if max_select_count is not None else None
|
||||
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, payload) if payload else self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT)
|
||||
result = result or {}
|
||||
if result.get("ok"):
|
||||
time.sleep(0.5)
|
||||
logger.info(f"trace_id={trace_id} 补全草稿已全选: tag={tag}, result={_safe_log_text(result)}")
|
||||
return result
|
||||
if result.get("reason") == "no complete draft rows":
|
||||
saw_empty = True
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
if result.get("reason") and result.get("reason") != "no complete draft rows":
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
if not result.get("ok"):
|
||||
if saw_empty and result.get("reason") == "no complete draft rows":
|
||||
logger.info(f"trace_id={trace_id} 补全草稿标签下方无商品行: tag={tag}, result={_safe_log_text(result)}")
|
||||
return {**result, "ok": True, "empty": True, "selectedCount": 0, "rowCount": 0}
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿全选失败: tag={tag}, result={_safe_log_text(result)}"
|
||||
)
|
||||
raise RuntimeError(f"补全草稿标签[{tag}]全选失败: {result}")
|
||||
return result
|
||||
|
||||
def _click_complete_draft_delete_selected(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
|
||||
trace_id = trace_id or _new_trace_id("draft")
|
||||
result: Dict[str, Any] = {}
|
||||
for attempt in range(8):
|
||||
result = self._run_js(COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT) or {}
|
||||
if result.get("ok"):
|
||||
break
|
||||
if result.get("menuOpened"):
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿选择组操作菜单已打开,等待删除选项出现: "
|
||||
f"tag={tag}, attempt={attempt + 1}, result={_safe_log_text(result)}"
|
||||
)
|
||||
time.sleep(0.8)
|
||||
continue
|
||||
break
|
||||
if not result.get("ok"):
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿点击删除失败: tag={tag}, result={_safe_log_text(result)}"
|
||||
)
|
||||
raise RuntimeError(f"补全草稿标签[{tag}]点击删除失败: {result}")
|
||||
time.sleep(0.5)
|
||||
logger.info(f"trace_id={trace_id} 补全草稿删除按钮点击成功: tag={tag}, result={_safe_log_text(result)}")
|
||||
return result
|
||||
|
||||
def _wait_complete_draft_rows_after_delete(
|
||||
self,
|
||||
tag: str,
|
||||
before_result: Dict[str, Any],
|
||||
trace_id: str = "",
|
||||
) -> None:
|
||||
trace_id = trace_id or _new_trace_id("draft")
|
||||
before_count = int(before_result.get("rowCount") or 0)
|
||||
deadline = time.time() + DELETE_SUCCESS_TIMEOUT
|
||||
last_result: Dict[str, Any] = {}
|
||||
while time.time() < deadline:
|
||||
time.sleep(0.5)
|
||||
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, {"probeOnly": True}) or {}
|
||||
last_result = result
|
||||
row_count = int(result.get("rowCount") or 0)
|
||||
if row_count < before_count or row_count == 0:
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿删除后行数已变化: "
|
||||
f"tag={tag}, before={before_count}, after={row_count}"
|
||||
)
|
||||
return
|
||||
for text in self._collect_delete_success_messages():
|
||||
if self._is_complete_draft_delete_success_message(text) or self._is_delete_success_message(text):
|
||||
logger.info(f"trace_id={trace_id} 补全草稿删除读取到成功提示: tag={tag}, message={_safe_log_text(text)}")
|
||||
return
|
||||
logger.info(
|
||||
f"trace_id={trace_id} 补全草稿删除后未确认行数变化: "
|
||||
f"tag={tag}, before={before_count}, last_result={_safe_log_text(last_result)}"
|
||||
)
|
||||
raise RuntimeError("补全草稿删除后未确认行数变化")
|
||||
|
||||
def open_recommended_offer_percentage_detail(self) -> Dict[str, Any]:
|
||||
"""打开 Seller Central 首页中的推荐报价百分比明细。"""
|
||||
if self.tab is None:
|
||||
@@ -206,13 +449,15 @@ class InventoryManage(AmamzonBase):
|
||||
shop_name: str,
|
||||
country: str,
|
||||
max_delete_operations: Optional[int] = None,
|
||||
allowed_statuses: Optional[Iterable[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
||||
logger.info(
|
||||
"开始批量删除非白名单商品: shop={}, country={}, max_delete_operations={}",
|
||||
"开始批量删除非白名单商品: shop={}, country={}, max_delete_operations={}, allowed_statuses={}",
|
||||
shop_name,
|
||||
country,
|
||||
max_delete_operations,
|
||||
list(allowed_statuses) if allowed_statuses is not None else None,
|
||||
)
|
||||
all_results: List[Dict[str, Any]] = []
|
||||
delete_counts: Dict[str, int] = {}
|
||||
@@ -222,7 +467,7 @@ class InventoryManage(AmamzonBase):
|
||||
stop_reason = "no-candidates"
|
||||
|
||||
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country)
|
||||
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows)
|
||||
latest_delete_candidates = self._build_deletable_status_rows(latest_status_rows, allowed_statuses)
|
||||
if not latest_delete_candidates:
|
||||
logger.info("未找到可删除商品分类: shop={}, country={}", shop_name, country)
|
||||
final_status_rows = latest_status_rows
|
||||
@@ -456,7 +701,7 @@ class InventoryManage(AmamzonBase):
|
||||
text = ""
|
||||
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:
|
||||
js_result = self._click_confirm_delete_modal_with_js()
|
||||
if js_result.get("ok"):
|
||||
logger.info("已通过 JS 确认删除弹窗: status={}, result={}", status, js_result)
|
||||
@@ -544,6 +789,11 @@ class InventoryManage(AmamzonBase):
|
||||
normalized = re.sub(r"\s+", "", text or "")
|
||||
return all(pattern.search(normalized) for pattern in DELETE_SUCCESS_MESSAGE_PARTS)
|
||||
|
||||
@staticmethod
|
||||
def _is_complete_draft_delete_success_message(text: str) -> bool:
|
||||
normalized = re.sub(r"\s+", "", text or "")
|
||||
return any(pattern.search(normalized) for pattern in COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS)
|
||||
|
||||
@staticmethod
|
||||
def _extract_delete_success_count(text: str) -> int:
|
||||
match = re.search(r"(\d+)\s*个商品(?:已删除|已经删除)", text or "")
|
||||
@@ -733,7 +983,12 @@ class InventoryManage(AmamzonBase):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _build_deletable_status_rows(cls, rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
def _build_deletable_status_rows(
|
||||
cls,
|
||||
rows: Iterable[Dict[str, Any]],
|
||||
allowed_statuses: Optional[Iterable[str]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
allowed_status_set = cls._normalize_allowed_status_set(allowed_statuses)
|
||||
candidates = []
|
||||
for row in rows:
|
||||
status = cls._normalize_option_text(row.get("status") or "")
|
||||
@@ -743,6 +998,8 @@ class InventoryManage(AmamzonBase):
|
||||
quantity_text = cls._normalize_quantity_text(row.get("quantity"))
|
||||
quantity = int(quantity_text) if quantity_text else 0
|
||||
canonical_status = cls._canonical_listing_status(status)
|
||||
if allowed_status_set is not None and canonical_status not in allowed_status_set:
|
||||
continue
|
||||
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
||||
continue
|
||||
|
||||
@@ -755,6 +1012,37 @@ class InventoryManage(AmamzonBase):
|
||||
)
|
||||
return candidates
|
||||
|
||||
@classmethod
|
||||
def _build_deletable_complete_draft_tag_rows(
|
||||
cls,
|
||||
rows: Iterable[Dict[str, Any]],
|
||||
allowed_statuses: Optional[Iterable[str]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
allowed_status_set = cls._normalize_allowed_status_set(allowed_statuses)
|
||||
complete_draft_enabled = bool(
|
||||
allowed_status_set and {"补全草稿", "商品信息草稿"}.intersection(allowed_status_set)
|
||||
)
|
||||
candidates = []
|
||||
for row in rows:
|
||||
tag = cls._normalize_option_text(row.get("tag") or row.get("status") or "")
|
||||
if not tag:
|
||||
continue
|
||||
quantity_text = cls._normalize_quantity_text(row.get("quantity"))
|
||||
quantity = int(quantity_text) if quantity_text else 0
|
||||
canonical_tag = cls._canonical_listing_status(tag)
|
||||
if allowed_status_set is not None and not complete_draft_enabled and canonical_tag not in allowed_status_set:
|
||||
continue
|
||||
if not quantity_text or quantity <= 0 or canonical_tag in LISTING_STATUS_DELETE_WHITELIST:
|
||||
continue
|
||||
candidates.append(
|
||||
{
|
||||
"tag": tag,
|
||||
"canonicalTag": canonical_tag,
|
||||
"quantity": quantity,
|
||||
}
|
||||
)
|
||||
return candidates
|
||||
|
||||
@staticmethod
|
||||
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
|
||||
summary = []
|
||||
@@ -769,6 +1057,21 @@ class InventoryManage(AmamzonBase):
|
||||
normalized = cls._normalize_option_text(status)
|
||||
return LISTING_STATUS_COMPATIBILITY_MAP.get(normalized, normalized)
|
||||
|
||||
@classmethod
|
||||
def _normalize_allowed_status_set(cls, statuses: Optional[Iterable[str]]) -> Optional[Set[str]]:
|
||||
if statuses is None:
|
||||
return None
|
||||
allowed = set()
|
||||
for status in statuses:
|
||||
text = cls._normalize_option_text(str(status or ""))
|
||||
if not text:
|
||||
continue
|
||||
for part in re.split(r"[,,;;、\r\n]+", text):
|
||||
normalized = cls._normalize_option_text(part)
|
||||
if normalized:
|
||||
allowed.add(cls._canonical_listing_status(normalized))
|
||||
return allowed
|
||||
|
||||
@staticmethod
|
||||
def _normalize_option_text(text: str) -> str:
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
@@ -1249,6 +1552,7 @@ class PatrolDeleteTask:
|
||||
items = data.get("items", [])
|
||||
country_sections = data.get("country_sections", [])
|
||||
cart_ratios = data.get("cart_ratios", [])
|
||||
delete_conditions = self._resolve_delete_condition_texts(data)
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
@@ -1274,7 +1578,10 @@ class PatrolDeleteTask:
|
||||
"stop_requested": False,
|
||||
}
|
||||
|
||||
self.log(f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家")
|
||||
self.log(
|
||||
f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家,"
|
||||
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
|
||||
)
|
||||
for index, shop_item in enumerate(items, 1):
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
@@ -1283,7 +1590,7 @@ class PatrolDeleteTask:
|
||||
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
self.log(f"[{index}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
self.process_shop(task_id, shop_item, country_sections, cart_ratios)
|
||||
self.process_shop(task_id, shop_item, country_sections, cart_ratios, delete_conditions)
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
@@ -1309,6 +1616,7 @@ class PatrolDeleteTask:
|
||||
shop_item: Dict[str, Any],
|
||||
country_sections: List[Dict[str, Any]],
|
||||
cart_ratios: List[Dict[str, Any]],
|
||||
delete_conditions: Optional[List[str]] = None,
|
||||
):
|
||||
"""处理单个店铺。"""
|
||||
from config import runing_shop, runing_task
|
||||
@@ -1334,7 +1642,10 @@ class PatrolDeleteTask:
|
||||
country_names = self._ordered_countries(payload_country_sections)
|
||||
aggregated_sections = self._build_country_section_templates(payload_country_sections)
|
||||
aggregated_cart_ratios = self._build_cart_ratio_templates(country_names, payload_cart_ratios)
|
||||
self.log(f"店铺 {shop_name} 待处理国家: {', '.join(country_names)}")
|
||||
self.log(
|
||||
f"店铺 {shop_name} 待处理国家: {', '.join(country_names)},"
|
||||
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
|
||||
)
|
||||
|
||||
driver: Optional[InventoryManage] = None
|
||||
requires_browser_reset = False
|
||||
@@ -1362,6 +1673,7 @@ class PatrolDeleteTask:
|
||||
task_id=task_id,
|
||||
shop_name=shop_name,
|
||||
country_name=country_name,
|
||||
delete_conditions=delete_conditions,
|
||||
)
|
||||
country_results.append(country_result)
|
||||
aggregated_sections[country_name] = country_result["countrySection"]
|
||||
@@ -1472,6 +1784,7 @@ class PatrolDeleteTask:
|
||||
task_id: int,
|
||||
shop_name: str,
|
||||
country_name: str,
|
||||
delete_conditions: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
from config import runing_task
|
||||
|
||||
@@ -1487,7 +1800,11 @@ class PatrolDeleteTask:
|
||||
driver.switch_to_manage_inventory()
|
||||
self.log(f"国家 {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,
|
||||
allowed_statuses=delete_conditions,
|
||||
)
|
||||
latest_status_rows = delete_result.get("statusRows") or []
|
||||
self.log(
|
||||
f"国家 {country_name} 删除步骤完成,deleted={delete_result.get('totalDeleted', 0)},"
|
||||
@@ -1499,17 +1816,26 @@ class PatrolDeleteTask:
|
||||
delete_counts=delete_result.get("deleteCounts") or {},
|
||||
default_process_status="无可删商品" if delete_result.get("totalDeleted", 0) == 0 else "",
|
||||
)
|
||||
complete_draft_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
|
||||
complete_draft_delete_result = self._delete_complete_draft_country_section_safe(
|
||||
driver,
|
||||
shop_name,
|
||||
country_name,
|
||||
delete_conditions,
|
||||
)
|
||||
complete_draft_section = complete_draft_delete_result.get("countrySection")
|
||||
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
|
||||
|
||||
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
||||
self.log(f"国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
|
||||
|
||||
total_deleted = int(delete_result.get("totalDeleted") or 0) + int(
|
||||
complete_draft_delete_result.get("deletedCount") or 0
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"countrySection": country_section,
|
||||
"cartRatio": cart_ratio,
|
||||
"deletedCount": delete_result.get("totalDeleted", 0),
|
||||
"deletedCount": total_deleted,
|
||||
"reopenRequired": False,
|
||||
}
|
||||
except Exception as exc:
|
||||
@@ -1566,6 +1892,51 @@ class PatrolDeleteTask:
|
||||
self.log(f"读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
||||
return None
|
||||
|
||||
def _delete_complete_draft_country_section_safe(
|
||||
self,
|
||||
driver: InventoryManage,
|
||||
shop_name: str,
|
||||
country_name: str,
|
||||
delete_conditions: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
trace_id = _new_trace_id("draft")
|
||||
try:
|
||||
self.log(f"trace_id={trace_id} 开始处理国家 {country_name} 补全草稿删除")
|
||||
delete_result = driver.delete_complete_drafts_from_non_whitelisted_tags(
|
||||
shop_name,
|
||||
country_name,
|
||||
trace_id=trace_id,
|
||||
allowed_statuses=delete_conditions,
|
||||
)
|
||||
rows = []
|
||||
delete_counts = delete_result.get("deleteCounts") or {}
|
||||
for item in delete_result.get("draftRows") or []:
|
||||
status = PatrolDeleteTask._string_result_field(item.get("tag") or item.get("status"))
|
||||
canonical_status = InventoryManage._canonical_listing_status(status)
|
||||
deleted_count = int(delete_counts.get(canonical_status) or 0)
|
||||
rows.append(
|
||||
{
|
||||
"type": "completeDraft",
|
||||
"status": status,
|
||||
"quantity": InventoryManage._normalize_quantity_text(item.get("quantity")),
|
||||
"deleteQuantity": str(deleted_count),
|
||||
}
|
||||
)
|
||||
section = {"country": country_name, "rows": rows} if rows else None
|
||||
self.log(
|
||||
f"trace_id={trace_id} 国家 {country_name} 补全草稿删除处理完成: "
|
||||
f"deleted={delete_result.get('totalDeleted', 0)}, rows={rows}"
|
||||
)
|
||||
return {
|
||||
"countrySection": section,
|
||||
"deletedCount": int(delete_result.get("totalDeleted") or 0),
|
||||
"deleteResult": delete_result,
|
||||
}
|
||||
except Exception as draft_exc:
|
||||
self.log(f"trace_id={trace_id} 删除国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
||||
fallback_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
|
||||
return {"countrySection": fallback_section, "deletedCount": 0, "error": str(draft_exc)}
|
||||
|
||||
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
|
||||
for retry in range(PRODUCT_TASK_MAX_RETRIES):
|
||||
try:
|
||||
@@ -1585,6 +1956,41 @@ class PatrolDeleteTask:
|
||||
time.sleep(PRODUCT_TASK_RETRY_DELAY)
|
||||
raise RuntimeError(f"切换国家 {country_name} 失败")
|
||||
|
||||
@classmethod
|
||||
def _resolve_delete_condition_texts(cls, data: Dict[str, Any]) -> Optional[List[str]]:
|
||||
if "delete_conditions" in data:
|
||||
raw_conditions = data.get("delete_conditions")
|
||||
elif "deleteConditions" in data:
|
||||
raw_conditions = data.get("deleteConditions")
|
||||
else:
|
||||
return []
|
||||
return cls._parse_delete_condition_texts(raw_conditions)
|
||||
|
||||
@classmethod
|
||||
def _parse_delete_condition_texts(cls, raw_conditions: Any) -> List[str]:
|
||||
if raw_conditions is None:
|
||||
return []
|
||||
if not isinstance(raw_conditions, list):
|
||||
raw_conditions = [raw_conditions]
|
||||
|
||||
texts: List[str] = []
|
||||
for item in raw_conditions:
|
||||
text = ""
|
||||
if isinstance(item, dict):
|
||||
for key in ("conditionText", "condition_text", "status", "text", "label", "name"):
|
||||
value = item.get(key)
|
||||
if value:
|
||||
text = str(value)
|
||||
break
|
||||
elif item is not None:
|
||||
text = str(item)
|
||||
|
||||
for part in re.split(r"[,,;;、\r\n]+", text):
|
||||
normalized = InventoryManage._normalize_option_text(part)
|
||||
if normalized and normalized not in texts:
|
||||
texts.append(normalized)
|
||||
return texts
|
||||
|
||||
def post_result(
|
||||
self,
|
||||
task_id: int,
|
||||
|
||||
Reference in New Issue
Block a user