Add archived/tongtool-y2-lowest-shipping

This commit is contained in:
2026-07-10 16:11:02 +08:00
parent f1c657de75
commit 500fecaa5e
@@ -0,0 +1,379 @@
---
name: tongtool-y2-lowest-shipping
description: Batch process TEMU semi-managed orders in Tongtool ERP to calculate the lowest shipping price, skipping printed/shipped orders, and saving remarks using Web UI + API fallback.
---
# Tongtool TEMU Lowest Shipping Calculator & Remark
This skill automates the process of finding the lowest shipping method for TEMU semi-managed orders in Tongtool, skipping orders that are already processed, utilizing the TEMU platform API for calculation, and saving the result robustly using a Web UI + API Fallback mechanism.
## Prerequisites
- Python 3
- `DrissionPage`
- `ddddocr`
- `requests`
## Crucial Platform Integration Discoveries & Pitfalls
### 1. Tongtool Open API V3 `ordersQuery` Signature Bug
When querying `/openapi/tongtool/ordersQuery`, passing date filters (`saleDate`, `payDate`, `updatedDate`, etc.) frequently results in a **`519 sign fail`** or **`525 cannot be all empty`** error. The official V3 signature logic is broken for this specific endpoint when url-encoding date parameters.
**Workaround:** Fetch orders from pre-existing local caches (e.g., scraping results), or query by specific `orderId`, or query strictly by `orderStatus: "1"` (Processing) without any date parameters.
### 2. Business Logic for Skipping Orders (Order Status Rules)
Do **not** perform shipping calculations for orders if they meet any of these criteria based on the API `ordersQuery` details response:
* `orderStatus` is `shipped` (物流商发货), `completed` (签收), or `cancelled`.
* `printCompleteTime` has a value (通途已打印).
* `despatchCompleteTime` has a value (通途已发货).
* The order already has a remark containing `[H201试算]` (assuming it isn't an error marker).
### 3. Robust Remark Writing (Web UI + API Fallback)
Because Tongtool's Web UI is highly asynchronous, `textarea` injection and Javascript `.click()` on the save button occasionally fails silently. The robust pipeline must:
1. Write the remark via Web UI layout injection.
2. Wait a few seconds array.
3. Verify via API (`/openapi/tongtool/ordersRemarkQuery`) using `orderIdKey`.
4. If missing, Fallback to API write (`/openapi/tongtool/addOrderRemark`).
## The Pipeline Script
This script logs in, filters and sorts target orders (ascending by the last 4 digits of the order ID), extracts the cheapest price via the shipping analysis popup, and securely writes it back.
```python
import time, json, re, hashlib, requests, os
from DrissionPage import ChromiumPage, ChromiumOptions
import ddddocr
ACCESS_KEY = "b4163dfec4654fd480c1cadeafea8ae7"
SECRET_KEY = "0fe4f51c1b8840e6bd70394ce6ec74f638a25524decb45b2adbb7ae5d56bfcdf"
PARTNER_OPEN_ID = "ec1d0a0ac9bceca98ed62e1504351371"
BASE_URL = "https://open.tongtool.com/apiv3-service"
SUCCESS_PREFIX = '[H201试算]'
ERROR_REMARK = '[H201试算]出错没找到最低价'
def get_token_and_sign():
r1 = requests.get(
f"https://open.tongtool.com/open-platform-service/devApp/appToken?accessKey={ACCESS_KEY}&secretAccessKey={SECRET_KEY}",
headers={"Content-Type": "application/json"}
)
app_token = r1.json()["datas"]
timestamp = str(int(time.time() * 1000))
raw_sign = f"app_token{app_token}timestamp{timestamp}{SECRET_KEY}"
sign = hashlib.md5(raw_sign.encode()).hexdigest()
return {"app_token": app_token, "timestamp": timestamp, "sign": sign}
def api_call(path, body):
_params = get_token_and_sign()
headers = {"Content-Type": "application/json", "api_version": "3.0"}
for _ in range(3):
try:
resp = requests.post(f"{BASE_URL}{path}", params=_params, headers=headers, json=body, timeout=20)
return resp.json()
except:
time.sleep(2)
return {}
def filter_and_sort_orders(raw_orders_list, limit=5):
"""Filter out completed/shipped/printed orders, and sort by ID last 4 digits"""
unique_orders = list(set(raw_orders_list))
valid_pool = []
for oid in unique_orders:
res = api_call("/openapi/tongtool/ordersQuery", {
"merchantId": PARTNER_OPEN_ID, "pageNo": "1", "pageSize": "100", "orderId": oid
})
if res.get("code") != 200 or not res.get("datas", {}).get("array"):
continue
info = res["datas"]["array"][0]
# 1. Skip based on status and timestamps
if info.get("orderStatus") in ["shipped", "completed", "cancelled"]:
continue
if info.get("printCompleteTime"):
continue
if info.get("despatchCompleteTime"):
continue
# 2. Skip already processed ones
marks_res = api_call("/openapi/tongtool/ordersRemarkQuery", {
"merchantId": PARTNER_OPEN_ID, "orderIdKey": info.get("orderIdKey"),
"pageNo": "1", "pageSize": "10"
})
already_success = False
if marks_res.get("code") == 200 and marks_res.get("datas"):
for m in marks_res["datas"].get("array", []):
text = m.get("orderRemark", "")
if SUCCESS_PREFIX in text and '出错' not in text:
already_success = True
break
if already_success: continue
valid_pool.append(oid)
# Sort ascending by the last 4 digits
def last_4(o):
m = re.search(r'(\\d{4})$', o)
return int(m.group(1)) if m else 9999
valid_pool.sort(key=last_4)
return valid_pool[:limit]
def calculate_temu_lowest_shipping_and_remark(order_id: str, driver):
def nuke_popups(page):
try:
page.run_js("""
document.querySelectorAll('.layui-layer-shade, .layui-layer, .modal, .overlay, .jclose-blue, .close, [title="关闭"], .layui-layer-close').forEach(el => { try { el.remove(); } catch(e){} });
""")
except: pass
driver.get("https://yijiety.tongtool.com/dashboard/homepage/index.htm")
time.sleep(4)
nuke_popups(driver)
search_url = (
f"https://yijiety.tongtool.com/search/order.htm"
f"?search_text_value={order_id}&search_mark=salesRecordNumber&source=global"
)
driver.get(search_url)
time.sleep(8)
nuke_popups(driver)
def open_shipping_popup():
driver.run_js("""
for (const el of document.querySelectorAll('*')) {
if (el.textContent.trim() === '仓储物流' && el.offsetHeight > 0) { el.click(); return true; }
}
return false;
""")
time.sleep(3)
driver.run_js("""
for (const el of document.querySelectorAll('a, button, span')) {
if (el.textContent.trim() === '编辑') {
const r = el.getBoundingClientRect();
if (r.width > 0 && r.height > 0 && r.height < 80) { el.click(); return true; }
}
}
return false;
""")
time.sleep(4)
clicked_analysis = driver.run_js("""
for (const a of document.querySelectorAll('a')) {
if (a.textContent.trim() === '邮寄方式解析') { a.click(); return true; }
}
return false;
""")
if not clicked_analysis: return False
time.sleep(6)
return driver.run_js("return !!document.querySelector('div[windowid] #tabpanelDiv');")
if not open_shipping_popup(): return None
def extract_temu_cheapest():
try:
driver.run_js("""
const popups = document.querySelectorAll('div[windowid]');
let popup = null;
for (const p of popups) { if (p.querySelector('#tabpanelDiv')) { popup = p; break; } }
if (!popup) return;
const tab = popup.querySelector('li[tabtitle="TEMU平台接口试算"]');
if (tab) tab.click();
""")
time.sleep(4)
driver.run_js("""
const popups = document.querySelectorAll('div[windowid]');
let popup = null;
for (const p of popups) { if (p.querySelector('#tabpanelDiv')) { popup = p; break; } }
if (!popup) return;
const temuDiv = popup.querySelector('#temuShippingMethod');
if (!temuDiv || temuDiv.classList.contains('hide')) return;
for (const b of temuDiv.querySelectorAll('a, button')) {
if (b.textContent.includes('查询') || b.textContent.includes('试算')) { b.click(); return; }
}
""")
time.sleep(8)
prices = driver.run_js("""
const popup = Array.from(document.querySelectorAll('div[windowid]')).find(p => p.querySelector('#tabpanelDiv'));
if (!popup) return [];
const temuDiv = popup.querySelector('#temuShippingMethod') || popup;
const out = [];
for (const tr of temuDiv.querySelectorAll('tr')) {
const tds = tr.querySelectorAll('td');
if (tds.length < 3) continue;
const rowText = tr.textContent || '';
let priceMatch = rowText.match(/\\$\\s*(\\d+\\.\\d{2})/);
let currency = '$';
if (!priceMatch) {
priceMatch = rowText.match(/[¥¥]\\s*(\\d+\\.\\d{2})/);
currency = '¥';
}
if (!priceMatch) continue;
// Skip disabled options ("未启用") entirely
if (rowText.includes('未启用')) continue;
const price = parseFloat(priceMatch[1]);
if (isNaN(price) || price <= 0) continue;
let carrier = '', method = '';
for (const td of tds) {
const t = td.textContent.trim();
if (!t) continue;
if (t.includes(priceMatch[0])) continue;
if (!carrier) { carrier = t; continue; }
if (!method) { method = t; break; }
}
if (!carrier && !method) continue;
out.push({carrier, method, price, raw: priceMatch[0], currency});
}
return out;
""")
if not prices: return None
return min(prices, key=lambda x: x['price'])
except Exception as e: return None
cheapest = extract_temu_cheapest()
driver.run_js("""
document.querySelectorAll('.jclose-blue, .close, [title="关闭"], div[windowid] .layui-layer-close').forEach(b => { try { b.click(); } catch(e){} });
document.querySelectorAll('div[windowid], .layui-layer, .layui-layer-shade').forEach(e => e.remove());
""")
time.sleep(2)
if cheapest:
c = (cheapest.get('carrier') or '').strip()
m = (cheapest.get('method') or '').strip()
label = f"{c}>>{m}" if c and m else (c or m)
remark = f"{SUCCESS_PREFIX}{label} {cheapest['currency']}{cheapest['price']:.2f}"
# --- NEW LOGIC: Update order's actual shipping method in Tongtool based on lowest price ---
# NOTE: Tongtool automatically handles binding the top-level logistics provider (e.g. 湖北众信 for US East, 易世通达 for US West).
# We MUST NOT change the top-level provider. We only select the correct last-mile option (USPS, GOFO, Swift, etc.) associated with that existing provider.
def get_best_method_code(method_str):
# Method mapping: match common last-mile carrier names
method_upper = method_str.upper()
sub_method = None
if "USPS" in method_upper:
sub_method = "USPS"
elif "GOFO" in method_upper:
sub_method = "GOFO"
elif "SWIFT" in method_upper:
sub_method = "SwiftX" if "SWIFTX" in method_upper else "Swift"
elif "SPEED" in method_upper:
sub_method = "SpeedX" if "SPEEDX" in method_upper else "Speed"
else:
return method_str
return sub_method
def update_shipping_method(driver, best_sub_method):
if not best_sub_method:
return False
try:
# Check current selected method first
current_method_full = driver.run_js("""
// The shipping method text input
const inputs = document.querySelectorAll('input.layui-input');
for (const input of inputs) {
if (input.value.includes('>>')) {
return input.value;
}
}
return "";
""")
if current_method_full and best_sub_method.upper() in current_method_full.upper():
return True # Already correct
# Parse current provider to stay within it (e.g. "湖北众信" or "易世通达")
current_provider = None
if current_method_full:
if "湖北众信" in current_method_full:
current_provider = "湖北众信"
elif "易世通达" in current_method_full:
current_provider = "易世通达"
if not current_provider:
# If we can't extract the current provider, we can't safely ensure we stay within it.
return False
# Find dropdown trigger and click it
opened = driver.run_js("""
const inputs = document.querySelectorAll('input.layui-input');
for(const input of inputs) {
if(input.value.includes('>>') || input.placeholder.includes('方式')) {
input.click();
return true;
}
}
return false;
""")
if not opened: return False
time.sleep(1)
# Click the option (MUST contain both the current provider and the new best sub_method)
clicked = driver.run_js(f"""
const items = document.querySelectorAll('dd');
for (const item of items) {{
const text = item.textContent.toUpperCase();
if (text.includes('{current_provider}') && text.includes('{best_sub_method.upper()}')) {{
item.click();
return true;
}}
}}
return false;
""")
if clicked:
# Save changes for the entire order
driver.run_js("""
for (const b of document.querySelectorAll('button')) {
if (b.textContent.trim() === '保存' && !b.closest('#orderRemarkForm')) {
b.click();
return true;
}
}
return false;
""")
time.sleep(2)
return True
return False
except Exception as e:
return False
target_sub_method = get_best_method_code(m)
update_shipping_method(driver, target_sub_method)
# -----------------------------------------------------------------------------------------
else: remark = ERROR_REMARK
remark_btn = driver.ele('css:#addOrderRemark')
if remark_btn:
a_tag = remark_btn.ele('tag:a')
if a_tag:
a_tag.click()
time.sleep(2)
textarea = driver.ele('tag:textarea')
if textarea:
js = f'''
var ta = document.querySelector('textarea');
if(ta) {{ ta.value = '{remark}'; ta.dispatchEvent(new Event('input', {{bubbles: true}})); ta.dispatchEvent(new Event('change', {{bubbles: true}})); }}
'''
driver.run_js(js)
time.sleep(1)
for a in driver.eles('tag:a'):
if a.text.strip() == '保存':
a.click(by_js=True)
time.sleep(3)
break
return remark
# Example usage integrating verify & fallback...
# order_id_key can be fetched via generic ordersQuery beforehand.
# remark = calculate_temu_lowest_shipping_and_remark(order_id, driver)
# -> Verify -> api_call("/openapi/tongtool/ordersRemarkQuery", ...)
# -> Fallback -> api_call("/openapi/tongtool/addOrderRemark", ...)
```