17 KiB
name, description
| name | description |
|---|---|
| tongtool-y2-lowest-shipping | 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
DrissionPageddddocrrequests
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:
orderStatusisshipped(物流商发货),completed(签收), orcancelled.printCompleteTimehas a value (通途已打印).despatchCompleteTimehas 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:
- Write the remark via Web UI layout injection.
- Wait a few seconds array.
- Verify via API (
/openapi/tongtool/ordersRemarkQuery) usingorderIdKey. - 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.
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", ...)