322 lines
10 KiB
Python
322 lines
10 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import requests
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parent
|
|
APP_DIR = ROOT_DIR / "app"
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
from amazon.main import TaskMonitor # noqa: E402
|
|
from amazon.patrol_delete import InventoryManage, PatrolDeleteTask # noqa: E402
|
|
from config import ( # noqa: E402
|
|
DELETE_BRAND_API_BASE,
|
|
JAVA_API_BASE,
|
|
JSON_TASK_QUEUE,
|
|
ZN_COMPANY,
|
|
ZN_PASSWORD,
|
|
ZN_USERNAME,
|
|
runing_task,
|
|
)
|
|
from main import WindowAPI # noqa: E402
|
|
|
|
|
|
SHOP_NAME = "郭亚芳"
|
|
USER_ID = 1
|
|
COUNTRIES = [item.strip() for item in os.environ.get("PATROL_COUNTRIES", "德国,西班牙,意大利").split(",") if item.strip()]
|
|
TS_MODE = os.environ.get("TS_MODE", "full").strip().lower()
|
|
|
|
|
|
class EventSlot:
|
|
def __iadd__(self, handler):
|
|
return self
|
|
|
|
|
|
class DummyWindow:
|
|
def __init__(self):
|
|
self.events = SimpleNamespace(maximized=EventSlot(), restored=EventSlot())
|
|
|
|
|
|
def log(message, payload=None):
|
|
print(f"[ts] {message}", flush=True)
|
|
if payload is not None:
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2), flush=True)
|
|
|
|
|
|
def unwrap(response):
|
|
log(f"HTTP {response.request.method} {response.url} -> {response.status_code}")
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
if not data.get("success"):
|
|
raise RuntimeError(data.get("message") or data.get("error") or response.text)
|
|
return data.get("data")
|
|
|
|
|
|
def get_match_item():
|
|
data = unwrap(
|
|
requests.post(
|
|
f"{JAVA_API_BASE}/api/patrol-delete/match-shops",
|
|
json={"user_id": USER_ID, "shop_names": [SHOP_NAME]},
|
|
timeout=40,
|
|
)
|
|
)
|
|
items = data.get("items") or []
|
|
if not items:
|
|
raise RuntimeError("match-shops did not return any items")
|
|
|
|
item = items[0]
|
|
log("match result", item)
|
|
if not item.get("matched"):
|
|
raise RuntimeError(f"shop is not matched: {item}")
|
|
return item
|
|
|
|
|
|
def create_java_task(match_item):
|
|
country_sections = [
|
|
{
|
|
"country": country,
|
|
"rows": [
|
|
{
|
|
"status": "全部",
|
|
"quantity": "",
|
|
"deleteQuantity": "",
|
|
"processStatus": "",
|
|
}
|
|
],
|
|
}
|
|
for country in COUNTRIES
|
|
]
|
|
cart_ratios = [{"country": country, "ratio": ""} for country in COUNTRIES]
|
|
task_item = {
|
|
"shopName": match_item.get("shopName") or SHOP_NAME,
|
|
"matched": bool(match_item.get("matched")),
|
|
"shopId": match_item.get("shopId"),
|
|
"platform": match_item.get("platform"),
|
|
"companyName": match_item.get("companyName"),
|
|
"matchStatus": match_item.get("matchStatus"),
|
|
"matchMessage": match_item.get("matchMessage"),
|
|
"countrySections": country_sections,
|
|
"cartRatios": cart_ratios,
|
|
}
|
|
data = unwrap(
|
|
requests.post(
|
|
f"{JAVA_API_BASE}/api/patrol-delete/tasks",
|
|
json={"user_id": USER_ID, "items": [task_item]},
|
|
timeout=40,
|
|
)
|
|
)
|
|
log("created task", data)
|
|
return data
|
|
|
|
|
|
def build_queue_payload(created_task):
|
|
task_id = created_task["taskId"]
|
|
created_item = (created_task.get("items") or [])[0]
|
|
sections = created_item.get("countrySections") or []
|
|
ratios = created_item.get("cartRatios") or []
|
|
payload = {
|
|
"type": "patrol-delete-run",
|
|
"ts": int(time.time() * 1000),
|
|
"data": {
|
|
"taskId": task_id,
|
|
"user_id": USER_ID,
|
|
"source": "ts-real-patrol-delete",
|
|
"items": [
|
|
{
|
|
"shopName": created_item.get("shopName") or SHOP_NAME,
|
|
"shopId": created_item.get("shopId"),
|
|
"platform": created_item.get("platform"),
|
|
"companyName": created_item.get("companyName"),
|
|
"matched": created_item.get("matched"),
|
|
"matchStatus": created_item.get("matchStatus"),
|
|
"matchMessage": created_item.get("matchMessage"),
|
|
}
|
|
],
|
|
"template_rows": [
|
|
{
|
|
"shopName": created_item.get("shopName") or SHOP_NAME,
|
|
"countries": [
|
|
{
|
|
"country": section.get("country"),
|
|
"status": (section.get("rows") or [{}])[0].get("status", ""),
|
|
"quantity": (section.get("rows") or [{}])[0].get("quantity", ""),
|
|
"deleteQuantity": (section.get("rows") or [{}])[0].get("deleteQuantity", ""),
|
|
"processStatus": (section.get("rows") or [{}])[0].get("processStatus", ""),
|
|
}
|
|
for section in sections
|
|
],
|
|
"cartRatios": ratios,
|
|
}
|
|
],
|
|
"country_sections": sections,
|
|
"cart_ratios": ratios,
|
|
},
|
|
}
|
|
log("queue payload", payload)
|
|
return payload
|
|
|
|
|
|
def make_monitor_without_startup_kill():
|
|
monitor = object.__new__(TaskMonitor)
|
|
monitor.running = True
|
|
monitor.user_info = {
|
|
"company": ZN_COMPANY,
|
|
"username": ZN_USERNAME,
|
|
"password": ZN_PASSWORD,
|
|
}
|
|
monitor.chunk_index = 1
|
|
monitor.max_workers = 1
|
|
monitor.executor = None
|
|
return monitor
|
|
|
|
|
|
def poll_java_task(task_id):
|
|
for _ in range(60):
|
|
try:
|
|
data = unwrap(
|
|
requests.post(
|
|
f"{JAVA_API_BASE}/api/patrol-delete/tasks/progress/batch",
|
|
json={"taskIds": [task_id]},
|
|
timeout=40,
|
|
)
|
|
)
|
|
log("java progress", data)
|
|
items = data.get("items") or []
|
|
if items and items[0].get("taskStatus") in {"SUCCESS", "FAILED", "COMPLETED"}:
|
|
return items[0]
|
|
except Exception as exc:
|
|
log(f"java progress poll failed: {exc}")
|
|
time.sleep(5)
|
|
return None
|
|
|
|
|
|
def drain_queue():
|
|
while True:
|
|
try:
|
|
JSON_TASK_QUEUE.get_nowait()
|
|
except Exception:
|
|
return
|
|
|
|
|
|
def make_user_info(company_name=None):
|
|
return {
|
|
"company": company_name or ZN_COMPANY,
|
|
"username": ZN_USERNAME,
|
|
"password": ZN_PASSWORD,
|
|
}
|
|
|
|
|
|
def complete_draft_payload_rows(tags):
|
|
rows = []
|
|
for item in tags:
|
|
rows.append(
|
|
{
|
|
"status": "补全草稿",
|
|
"tag": item.get("tag", ""),
|
|
"type": "completeDraft",
|
|
"quantity": str(item.get("quantity") if item.get("quantity") is not None else ""),
|
|
"deleteQuantity": str(item.get("quantity") if item.get("quantity") is not None else ""),
|
|
"processStatus": "已完成",
|
|
"raw_text": item.get("raw_text", ""),
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
def run_complete_draft_real_browser():
|
|
log("starting real complete-draft read test")
|
|
log("scope", {"shop": SHOP_NAME, "countries": COUNTRIES, "user_id": USER_ID})
|
|
match_item = get_match_item()
|
|
driver = InventoryManage(make_user_info(match_item.get("companyName")))
|
|
try:
|
|
browser = driver.open_shop(match_item.get("shopName") or SHOP_NAME)
|
|
if not browser or browser == "店铺不存在":
|
|
raise RuntimeError(f"open shop failed: {browser}")
|
|
if driver.need_login():
|
|
raise RuntimeError("shop requires login; complete-draft read test cannot continue safely")
|
|
|
|
normalized_sections = []
|
|
for country in COUNTRIES:
|
|
log(f"switching country for complete-draft read: {country}")
|
|
if not driver.switch_to_country(country):
|
|
raise RuntimeError(f"switch country failed: {country}")
|
|
tags = driver.get_complete_draft_quick_view_tags(match_item.get("shopName") or SHOP_NAME, country)
|
|
payload_rows = complete_draft_payload_rows(tags)
|
|
normalized = PatrolDeleteTask._normalize_country_sections_for_result(
|
|
[{"country": country, "rows": payload_rows}]
|
|
)[0]
|
|
normalized_sections.append(normalized)
|
|
log(
|
|
"complete draft country result",
|
|
{
|
|
"country": country,
|
|
"rawTags": tags,
|
|
"payloadRowsBeforeNormalize": payload_rows,
|
|
"normalizedForSubmit": normalized,
|
|
},
|
|
)
|
|
|
|
log("complete draft normalized sections", normalized_sections)
|
|
finally:
|
|
try:
|
|
driver.close_store()
|
|
except Exception as exc:
|
|
log(f"close store failed: {exc}")
|
|
|
|
|
|
def main():
|
|
if TS_MODE == "complete-draft":
|
|
run_complete_draft_real_browser()
|
|
return
|
|
|
|
log("starting real patrol-delete automation")
|
|
log("scope", {"shop": SHOP_NAME, "countries": COUNTRIES, "user_id": USER_ID})
|
|
log("java api base", {"JAVA_API_BASE": JAVA_API_BASE, "DELETE_BRAND_API_BASE": DELETE_BRAND_API_BASE})
|
|
log("ziniao env", {"company": bool(ZN_COMPANY), "username": bool(ZN_USERNAME), "password": bool(ZN_PASSWORD)})
|
|
|
|
drain_queue()
|
|
match_item = get_match_item()
|
|
created_task = create_java_task(match_item)
|
|
task_id = created_task["taskId"]
|
|
payload = build_queue_payload(created_task)
|
|
|
|
monitor = make_monitor_without_startup_kill()
|
|
monitor_thread = threading.Thread(target=monitor.start, daemon=True)
|
|
monitor_thread.start()
|
|
time.sleep(0.5)
|
|
|
|
enqueue_result = WindowAPI(DummyWindow()).enqueue_json(payload)
|
|
log("enqueue result", enqueue_result)
|
|
if not enqueue_result.get("success"):
|
|
raise RuntimeError(f"enqueue failed: {enqueue_result}")
|
|
|
|
deadline = time.time() + 60 * 45
|
|
last_status = None
|
|
while time.time() < deadline:
|
|
state = runing_task.get(task_id)
|
|
if state and state != last_status:
|
|
log("python task state", state)
|
|
last_status = dict(state)
|
|
if state and state.get("status") in {"completed", "failed", "stopped"}:
|
|
break
|
|
time.sleep(5)
|
|
|
|
monitor.running = False
|
|
JSON_TASK_QUEUE.put({"type": "__stop__"})
|
|
monitor_thread.join(timeout=10)
|
|
|
|
log("python final state", runing_task.get(task_id))
|
|
final_java = poll_java_task(task_id)
|
|
log("java final", final_java)
|
|
log("finished", {"taskId": task_id})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|