From 500fecaa5eaae43a2294c009dbb7779fb5eeb138 Mon Sep 17 00:00:00 2001 From: admin9webs Date: Fri, 10 Jul 2026 16:11:02 +0800 Subject: [PATCH] Add archived/tongtool-y2-lowest-shipping --- .../tongtool-y2-lowest-shipping/SKILL.md | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 skills/archived/tongtool-y2-lowest-shipping/SKILL.md diff --git a/skills/archived/tongtool-y2-lowest-shipping/SKILL.md b/skills/archived/tongtool-y2-lowest-shipping/SKILL.md new file mode 100644 index 0000000..c38caaf --- /dev/null +++ b/skills/archived/tongtool-y2-lowest-shipping/SKILL.md @@ -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", ...) +``` \ No newline at end of file