暢流 WMS API 串接
暢流(ChangLiu)是台灣的倉儲管理系統(WMS),透過 RESTful API 提供電商訂單、庫存、入庫等資料。 本 skill 提供串接所需的完整規格、可重用的 Python 客戶端,以及實測得出的關鍵眉角。
⚠️ 實測範圍但書(全檔適用):本檔標「✅ 實測」的數字與值域,多數只驗過一家廠商的 API 帳號 (2026-08)。它們是「正常時長什麼樣」的參考值,不是暢流全域鐵律;換廠商、換租戶請重驗。
開始前先讀
- 完整 API 規格:
references/api-reference.md(端點、參數、回傳欄位、狀態碼、平台代碼、錯誤排除)。 寫任何串接前先查這份,它是已實測 + 對照官方文件的權威來源。 - 可重用客戶端:
scripts/wms_client.py(認證快取、瀏覽器 UA、強制 IPv4、翻頁、個資解密都寫好了)。 不要從頭手刻 HTTP 請求,直接用這支。 ⚠️ 「會翻頁」不等於「一定拉完整」:_paged()要回的是(rows, 完不完整, 原因), 高階查詢(query_orders/query_inbound)拉不完整就該丟WMSIncomplete。 拿結果去做覆蓋刪除/對帳前,先讀下面〈拉單完整性〉那一節。 (query_stock()打的是非分頁版端點,本來就不翻頁。) 👉 完整性把關、timeout參數、token 快取綁憑證版本這幾個介面屬新版客戶端; 手上的檔案若還沒有,就照本檔對應章節的做法自己補上。 - 平台代碼對照:
scripts/source_keys.py(source_key ↔ 中文名,85 個代碼/84 個名稱: 官方 84 個代碼=83 家平台(711go與iyugo同為「i預購」), 另加 1 個官方清單外、實測遇過的outflow=倉庫出庫單(不是電商平台))。
五個一定要記住的眉角(不照做會卡住)
這幾點是實測踩過的雷,文件不一定講清楚,但每次串接都會遇到:
必帶瀏覽器 User-Agent。API 前面有 Cloudflare,用 Python 預設 UA 會被擋,回
HTTP 403 + error code: 1010。 這不是暢流的錯誤而是 Cloudflare 的。一定要帶User-Agent: Mozilla/5.0 ... Chrome/...。來源 IP 要在白名單內,而且是「一個廠商帳號一份」。暢流後台「白名單管理」要加入呼叫端的對外公網 IP。 未授權時
result.ok=false,message 實測原文為「您的IP[x.x.x.x]未在白名單中」, 而且是在取 token 那一步就被擋(token/authorize.php直接失敗),連線測試第一發就會現形。 🔴 白名單不是全域一份:同一個 IP 對 A 廠商通,不代表對 B 廠商通。即使兩家共用同一個{租戶}.wms.changliu.com.tw網域、只靠 API_ID/API_KEY 區分,白名單仍是各帳號各一份, 要到每一家的後台各加一次。多帳號串接前先逐家跑一次取 token 測試,否則資料會在你不知情的 狀況下只涵蓋通得了的那一家(少加一家的症狀=那家一律取 token 失敗、其他家照常,很容易被 誤讀成「那家憑證壞了」)。細節與實測範圍見references/api-reference.md§0。 ⚠️ 浮動 IP 會變動(實測家用線路 2026-09-08~11 四天換三次、一天內可換兩次),正式環境建議用固定的對外 IP; 為了開發/除錯臨時加進去的浮動 IP,事情做完要回頭請對方移除(過幾天那個 IP 可能已經換人用)。雙棧網路要強制 IPv4(2026-07 實測)。中華電信等雙棧網路下,Python 預設可能走 IPv6 出去, 白名單錯誤訊息會出現
2001:b011:...這種 IPv6 位址——IPv6 位址常輪換,不要拿去加白名單, 正解是強制客戶端走 IPv4。做法有兩種,能用第二種就別用第一種:scripts/wms_client.py已內建(建構參數叫force_ipv4_dns=True,預設開啟)。 ⚠️ 它的手法是 patchsocket.getaddrinfo只回AF_INET,副作用及於整個 process 且無法還原——只要 new 過一個WMSClient,同一支程式裡其他函式庫的連線也一起被改掉。 一次性腳本沒差;長駐服務、或程式裡還有別的對外連線時要避開。- 用 httpx/aiohttp 就改綁本地端 IPv4:
httpx.AsyncHTTPTransport(local_address="0.0.0.0")。 本地端綁 IPv4 wildcard 就只建得起 IPv4 連線,效果等同只回AF_INET,但不動全域、 不影響同一個 process 裡其他連線。(實戰專案的兩處正式呼叫點都用這招,並有守衛測試盯著。)
個資不只密文那兩欄。
receiver_name、receiver_phone(收件人姓名、電話)為 AES-128-ECB 加密(見下); 若 API 帳號沒有「個資存取權限」,解出來會是****(源頭遮罩,非解密失敗)。 ⚠️ 但「只有這兩欄加密」不等於「其餘欄位可以整包安心存檔」,至少還有這幾類要防:- 入庫單的
sender_name/sender_phone(見references/api-reference.md第 4 節): 欄名字面就是寄件人姓名與電話,不在加密清單裡。 - 自由文字備註欄:入庫單
demo(單頭)/products[].notice(品項), 訂單buyer_msg/remark/notice——依欄位定義就是讓人自由打字的格子,有被填入姓名電話的可能。 - 收件地址
receiver_info(宅配單的 address/city/area/street)也是個資。 👉 回應原文要存快照、寫進資料庫、寫 log 或貼進錯誤訊息之前,先把這些欄位剔除; 清單與做法見〈安全須知〉與references/api-reference.md第 5 節。 (範圍但書:目前唯一的入庫實拉樣本sender_*/demo/notice全是 null, 所以「是不是明碼」「實際會被填什麼」都尚未實測,這是照欄位語意給的預防性提醒。)
- 入庫單的
有請求頻率上限:每秒 3 發、可暫存 3 發,超過回 HTTP 409(✅ 2026-09-13 實測)。回應標頭
x-ratelimit-limit: 3/x-ratelimit-burst: 3/x-ratelimit-remaining;超一點被延遲放行、再超回 409 (不是 JSON 錯誤,raise_for_status()直接炸)。額度很可能整組 API 帳號共用——壓到每秒 1.7 發的 逐日拉單仍偶爾撞到,因為正式服務用同一組憑證在分同一桶。做法:批次每發間隔 ≥ 0.6 秒、撞 409 退避重試 (5→10→20 秒)、即時查詢至少自動重試一次、大量拉單放離峰。細節與範圍但書見references/api-reference.md§0。 ⚠ 同批實測還有三件:order/count.php帶任何條件都回 0(不是歷史統計);訂單狀態碼O=倉庫出庫單會混在 訂單查詢裡;庫存查詢沒有按庫位查(帶庫位類參數全被靜默忽略,wh亦然)——各見 §6、附錄 A、§3。
🔴 拉單完整性:這次到底拉完了嗎(實測踩過,會安靜漏單)
暢流不會為錯誤的查詢報錯——它安靜地回一份看起來很正常的資料。三種形狀都實際踩過:
| 踩到什麼 | 實際發生的事 | 怎麼防 |
|---|---|---|
分頁參數寫成 page(正確是 nowpage) |
未知參數被靜默忽略,每次都回第 1 頁;照 maxpage 跑 66~70 圈=同一頁複製 66~70 份,算出來的統計全數作廢 | 每頁回檢暢流回的 nowpage 有沒有跟著走、整頁資料是不是全都收過了;總筆數剛好是「頁數×每頁筆數」的整數(3,500=70×50、3,300=66×50)就要起疑 |
帶了參數表以外的參數(例如想用 send_num 查訂單) |
條件被忽略、照樣回整批:帶與不帶回應一模一樣(50 筆、maxpage 48),沒有一筆是靶 | 只送 references/api-reference.md 第 2/4 節列出的參數;物流單號問不到暢流任何東西,只能整批拉回本地自己比對 |
條件的值是 falsy(例如 status=0) |
條件被當成沒帶:inbound/query.php 帶 status=0 回 total=18=該廠商全部,而同樣不存在的 status=5/9 正常回 0 筆 |
查詢前把空值濾掉並擋下,別讓 None/""/0 進 query string |
⚠ 以上數字為 2026-08 實測,每條各自只驗過一家廠商的帳號;空字串("")與 order_query.php
上的 falsy 行為屬同機制推論、未實測。
白話後果:把「只拿到這麼多」當成「只有這麼多」,再去做「本地有、暢流沒有就刪掉」這種對帳, 沒拉到的單會被整批當成過期資料刪掉,而畫面一路綠燈、上次同步時間照更新。 所以分頁結果一定要帶「完不完整」的旗標,判準見下。
「查不到」不等於「出錯」(最容易寫錯的一段)
暢流的「這個條件下沒有資料」有兩種長相,而且看端點而定,只認得其中一種就會誤判:
result.ok=true+rows是[]或null,多半伴隨total=0/maxpage=0(✅ 實測:inbound/query.php、order/order_query.php;rows直接是null實測見於inventory/stock_query_page.php,其他端點未逐一驗證)result.ok=false+ message 含「查無/無符合/沒有符合」(HTTP 仍 200;✅ 實測:product/bom.php)
- 兩種都要收斂成同一個結果「這次沒查到」——只認一種,同一種現場狀況會一次做得成、一次做不成。
- 🔴 取
rows的寫法有兩個陷阱:data.get("rows", [])在「key 在、值是null」時 default 不生效,會拿到None,list(None)當場 TypeError;data.get("rows") or []不會爆, 但會把null/{}/0/""全吃成空陣列,「回應壞掉」與「真的查無」就分不出來。 保守寫法是先判型別:拿到的不是 list 就當「回應結構異常」處理,不要一律當成空。 - 「真的一張都沒有」的判準要同時滿足「第 1 頁」+「空 rows」+「
total=0」; 第 2 頁以後的空白是「暢流剛說共 N 頁、現在又說查無」的前後不一致,絕不可當成翻完了。 換句話說:第 1 頁查無=真的沒有(完整,該刪就刪);第 2 頁以後查無=不完整,停手別刪。 - 🔴
product/bom.php要再分一層:「沒問到」(連線失敗、HTTP 錯誤頁、回應結構不對、 其他ok=false)絕不可以當成「暢流說它不是組合品」——後者會讓組合品不展開元件, 撿貨單看起來完全正常、貨卻少出。權威答案要三條件同時成立(HTTP 200 +ok=false+ 訊息含「非組合品」),其餘一律重試,耗盡才擋下。 ⚠ 暢流那句「查無符合的資料,或提供的SKU商品非組合品」同時含「查無」與「非組合品」, 只比對「查無」必然誤判。細節見references/api-reference.md§0.5 與 §4.5。 - ⚠ 白名單被拒、權限不足的訊息不含那三個關鍵詞,那仍然是失敗——別把「連不上」讀成「沒有資料」。
- 建議把查無類訊息獨立成一個例外(本 skill 客戶端用
WMSEmpty,WMSError的子類)並分開接: 接到WMSEmpty是「沒資料」,接到其他WMSError才是真的失敗。
認證流程
POST {base}/token/authorize.php
Header: Authorization: Basic {base64(API_ID:API_KEY)}, User-Agent: {瀏覽器UA}
→ data.access_token(效期 1 小時)
之後每支 API: Authorization: Bearer {access_token}
回應統一格式:result.ok / result.message / data。詳見 references/api-reference.md 第 1 節。
token 快取的三個坑(長駐服務才會遇到)
Token 效期 1 小時,一定要快取(提前 5 分鐘換新即可)。但只看效期是不夠的:
- 快取鍵要含「憑證版本」。 管理者在後台重設過 API 憑證之後,只看效期的長駐服務
最長會有 55 分鐘還在用舊 token——後台按「連線測試」顯示通過、員工那邊卻刷不出單,
兩邊講的話不一樣。做法:快取值存成
(token, 取得時刻, 憑證版本),三者任一不符就重新取; 「憑證版本」拿資料庫裡那筆憑證的更新時間字串當比對值就夠(資料驅動,多程序部署下 每個程序各自比對,不必互相通知去清誰的快取)。 - 多家廠商(多租戶)要逐家各自快取。 一張 token 只對換它的那組憑證有效, 快取鍵請用廠商 ID,不要用單一全域變數。
- 做「連線測試」要用一個快取必定為空的客戶端。 沿用共用的那個單例等於測到舊憑證 換來的舊 token,管理者會拿到一個「通過」的假答案。
對應的客戶端介面:WMSClient(creds_version=...)、set_credentials()(換憑證會自動作廢 token)、
invalidate_token()(測試前先清)。
個資解密配方(實測成功)
- 演算法 AES-128-ECB;金鑰 = 後台「個資解密金鑰」字串的前 16 字元(UTF-8 bytes)
- 密文 Base64;Padding PKCS7
import base64
from Crypto.Cipher import AES
def decrypt_pii(b64_text, full_key):
ct = base64.b64decode(b64_text)
pt = AES.new(full_key.encode("utf-8")[:16], AES.MODE_ECB).decrypt(ct)
p = pt[-1]
if 1 <= p <= 16 and pt[-p:] == bytes([p]) * p:
pt = pt[:-p]
return pt.decode("utf-8")
(scripts/wms_client.py 的 decrypt() 已內建此邏輯。)
時間一律台灣時間(✅ 2026-08 實測)
API 回的時間字串不帶時區標示,值就是台灣時間(UTC+8)。若系統存世界標準時間(UTC),
讀進來要先補 +08:00 再轉——這種錯不會報錯,只會讓某些數字安靜地差 8 小時。
已逐一驗過的欄位:order_date、finish_time、order_logistics.php 時間軸、入庫紀錄 inbound_date;
其餘時間欄同族推定、未逐一驗。
⚠️ 不要拿暢流後台畫面去校正 API:畫面「核貨時間」與 API finish_time 一秒不差,
但畫面「上傳SHOPEE」那一格少 8:00:00 整(=該格用世界標準時間顯示,是暢流的畫面問題)。
對不上時以 API 為準,別在程式裡偏移時間。細節與範圍但書(實拉樣本僅一家廠商三張單、
「撿貨時間」那格待補驗)見 references/api-reference.md 第 0 節。
常用端點速查
| 任務 | 端點 | 重點 |
|---|---|---|
| 查訂單 | GET order/order_query.php |
可用 order_no / date_min,date_max / status / source_key;分頁一律 nowpage+pagesize(上限 50/頁)。⚠ date_* 篩的是訂單日期不是出貨日——只拉「今天」會漏掉前幾天下單、今天才要出的積壓單,務必往回拉數天。⚠ 參數表以外的參數會被靜默忽略、照樣回整批 |
| 查庫存+儲位 | GET inventory/stock_query.php |
用 sku 或 item_no 查,一次可帶 50 個(逐個打實測慢 10 倍:141 個商品 21 秒 → 批次 2~3 秒);儲位名在 data.rows[].spaces[].name,倉別=名字用 - 切開的第一段(warehouse 欄實測是 null,不能用)。⚠ occupied_stock(佔用)批次查會歸戶錯誤——同一張單押的多個商品,佔用會加總全掛在第一個商品上、其餘顯 0;看佔用/可用庫存必須單查。佔用有兩層語意:商品層=所有未出貨單(F/W/P)加總、儲位層=僅「轉揀貨(P)」已指定撿貨格的量(F/W 階段儲位層恆 0);可用庫存 API 不給、自己算 stock−佔用(✅2026-09 實測,見 api-reference §3)。核貨時就從記了儲位層佔用的那一格扣、扣的量=佔用量,同商品其他格不動(✅2026-09-11 三個 SKU 核貨前後對照) |
| 全量列舉庫存 | GET inventory/stock_query_page.php |
分頁版(nowpage+pagesize,上限 50);不帶 sku 就是整倉一頁頁列舉;⚠ 組合品在這支查不到(total=0/rows=null)。詳見 api-reference §3.1 |
| 查入庫單 | GET inbound/query.php |
用 order_no / build_date / supplier;rows[].products[] 是入庫商品。⚠️ 登記是逐品項的(實測一張單可跨 12.5~97.5 分鐘),判「收完了沒」看每個品項有沒有 history,別看 status(入庫狀態碼官方無碼表,附錄 A 那張是訂單的) |
| 查商品目錄 | GET product/pro_query.php |
🔴 沒有批量,一次只能一個 sku;要一批就整本拉(不帶 sku、pagesize=100,上限就是 100)。每列帶 combination/processed 官方旗標 |
| 查商品明細 | GET product/pro_detail.php |
sku= 或 item_no= 皆可;真正的規格層在 specifications[](各規格的 item_no 與 stock) |
| 取面單(寄件單) | POST logistics/label.php |
JSON body {"order_no":"…"};回 label_url/limit_time/send_num。黑貓/新竹/MOMO甲配/店到家的面單只有這條路(它們的 print_url 實測恆空)。⚠ 已不是唯一的 POST——取號那兩支也是 POST 且會寫入,見下一列 |
| 幫自家單取黑貓托運號 | POST logistics/confirm.php → POST logistics/request.php |
🔴 request 真取號(寫入、不可逆);confirm 只問「走哪家+可選尺寸」(✅ 實測無副作用)。request body {"order_no","size":[{"code","quantity"}]}。多箱=一箱一號+一箱一張面單,貨到付款的代收金額由暢流拆到各箱(✅2026-09-14 重驗,⚠ 只驗過轉單中的人工開單、轉揀貨的單還沒驗;8/25「一單恆一號」、9/11「N 號但面單只有最後一號」都已過時)。⚠ 回 ok=true 但 send_num 是空的也遇過——打完一定回查訂單。平台單不必也不該打(平台自取號)。全文 references/api-reference.md §6.6 |
完整參數與回傳欄位見 references/api-reference.md。其他端點(商品、物流、POS、新增/取消類)清單也在該檔第 6 節。
🔴 第 6 節那張表把查詢與會改暢流資料的端點(新增/取消/揀貨/庫存更新/入庫單新增取消/ 物流確認、請求、完成)混排在一起,表內已逐支標記讀/寫。預設只打查詢端點, 並且別拿 GET/POST 當判準——
logistics/label.php是 POST 但實測零副作用, 掛 GET 的inventory/stock_allocation.php反而看不出是查詢還是執行。
🔴 訂單的「物流」有三個欄位,別選錯
(✅ 2026-08 實測;完整值域與證據見 references/api-reference.md 第 2 節〈物流三欄〉)
freight_name(運送方式名稱)=顯示與分組就用這欄,人看得懂、值域=該租戶後台「運送方式」清單。logistics(取號渠道)=物流單號走哪條線取。官方文件漏記,但實測每一列都會回; 實拉值域cat/shopee/moplus/momo_pelican/pelican/none。 ⚠ 渠道不等於誰家車來載——新竹物流的單logistics是shopee(=蝦皮代辦取號)。logistics_code(物流代碼)=髒欄位,只存參考:值域混形(多數其實是中文運送方式名原樣填入), 且與freight_name兩個方向都不是一對一(mo店+ 三種運法代碼全是moplus)——拿它分組會靜默併錯。
三欄都是下單匯入就有值,不用等取號。⚠ 值域只實拉過一家廠商(自然風),換廠商請重驗;
另外 logistics 這個名字在入庫單查詢也有一個,是同名不同義的另一個欄位。
⚠ 用狀態碼(status)篩單前先讀這三句
(✅ 2026-08 實測,範圍=單一廠商帳號自然風)
- 狀態不是每張單都走完 P→C→S 的階梯:蝦皮來源的黑貓單近 21 天 12 張 12/12 直接停在 S、
0 張停在 P 或 C(同期非蝦皮的黑貓單則停得住 C)。只拉
W,P,C會整批漏單。 - S(已出貨)不代表貨已離倉:那個 S 是回報電商平台那一秒設的(時間=
finish_time+2 秒), 查order_logistics.php看不到物流的集貨/轉運紀錄。 - 物流單號(
send_num)要到轉揀貨(P)才有(W/F 實測 0%);廠商自送/自取類永遠不會有。
細節、樣本數與尚未證實的部分,見 references/api-reference.md 附錄 A 下方〈狀態碼的實務眉角〉。
🔴 商品與條碼:三個最容易踩的坑(✅ 2026-08 實測)
- 條碼登記在
item_no,不是sku。暢流商品主檔沒有獨立的條碼欄位;sku是賣場/內部代號(例A020151-02-CH),實體條碼(EAN-13、ITF-14)登記在item_no(例4710088832351)。拿掃到的條碼去比sku會全部比不中。 而且item_no非空 ≠ 架上掃得到(同一欄也被拿來填自家料號、0、C-0000008這種值), 要用「長度 8~14 碼+GS1 檢查碼實算」判;整箱商品可能登的是 14 碼外箱碼, 判準不可收窄成 12/13 碼。同一條碼可能掛多個 SKU → 禁止拿條碼反查品項, 只能拿來驗「這串碼屬不屬於指定品項」。 product/pro_query.php沒有批量。sku=A,B回 0 筆不報錯、sku[]=A&sku[]=B只回第一個、重複鍵只回最後一個——全部靜默,看起來像成功。 要一批商品就整本拉(不帶 sku、pagesize=100),不要自己拼批次。 ⚠ 整本拉回來的每一列,sku是陣列(例["MP-0032"])、spec是 list([{sku, item_no, name}],真正的倉端品項在這裡); 直接str(row["sku"])會變成"['MP-0032']",拿去查庫存回 0 筆也不報錯(✅2026-09-10 踩過)——從spec[]逐筆取。pagesize上限逐端點不同:訂單/入庫單 50、商品目錄 100; 超過上限不報錯,只是靜默截到上限(商品目錄實測開 200/500/1000/2000 都照樣每頁回 100)。
判「是不是組合品」用官方旗標 combination(pro_query/pro_detail 每一列都有),
不要看貨號字頭。細節、欄位表與實測範圍但書(多數只驗過一家租戶)見
references/api-reference.md §4.7~§4.9。
使用客戶端的範例
scripts/wms_client.py 以環境變數或建構參數讀取憑證(不含任何硬編碼金鑰):
from wms_client import WMSClient
# 憑證來自環境變數 CHANGLIU_BASE_URL / CHANGLIU_API_ID / CHANGLIU_API_KEY / CHANGLIU_DECRYPT_KEY
c = WMSClient()
orders = c.query_orders(date_min="2026/06/01", date_max="2026/06/30", source_key="shopee")
name = c.decrypt(orders[0]["receiver_name"]) # 解出收件人姓名(若有權限)
stock = c.query_stock(sku="C010014-THL") # 查庫存與儲位
inbound = c.query_inbound(order_no="2606180002") # 查入庫單
逾時要設多少(有實測數據,別憑感覺)
暢流正常回應 1~2 秒。三筆不同時間、不同情境的實測:三家廠商並行查 1.01.2 秒;
換 token 1.4 秒、單筆查詢 0.640.75 秒;訂單拉 10 頁共 12.8 秒(每頁約 1.28 秒)。
⚠️ 這些樣本來自同一台暢流主機的少數幾家廠商,是「正常時多快」的參考值,不是保證上限。
分層設,別一個值走天下:
| 情境 | 單次逾時 | 為什麼 |
|---|---|---|
| 背景批次拉單 | 15 秒 | 留餘裕給偶發的慢回應;翻頁是多次請求,每次各自計時 |
| 有人在畫面前等(連線測試、現場刷單) | 8 秒 | 沿用 15 秒的話「取 token+查詢」兩段最壞 30 秒,使用者只能乾等 |
| 整支互動流程 | 再包一個 20 秒總上限 | 保證畫面一定回得來(例:asyncio.wait_for(...)) |
scripts/wms_client.py 建議預設 15 秒(舊版寫死 40 秒),要調就 WMSClient(timeout=8)。
注意它是每次請求各算,不是整批的總時間(自動翻頁會打很多次)。
失敗要怎麼判讀
例外訊息是自由文字,呼叫端拿字串去比對很脆。建議把任何一次失敗翻成一個封閉的分類碼 再落庫,由上往下第一個命中者勝:
| 順序 | 訊號 | 分類碼 | 白話 |
|---|---|---|---|
| 1 | 訊息或回應內容含「白名單」 | ip_not_whitelisted |
這台機器的對外 IP 沒加進這家廠商的後台 |
| 2 | HTTP 403 且內容含 1010 |
blocked_1010 |
被 Cloudflare 擋(多半是沒帶瀏覽器 UA) |
| 3 | 連線層例外(逾時、連不上、TLS 失敗) | timeout |
網路或對方慢,稍後再試 |
| 4 | 其餘,且失敗在取 token 那一步 | auth_failed |
憑證錯(API_ID/API_KEY 不對) |
| 5 | 其餘,且token 拿到了、查詢那步才失敗 | query_denied |
登得進去,但這支/這筆查不了(權限不足) |
第 4、5 條「用失敗在哪一步分憑證錯與權限不足」是文件不會寫、但每次串接都用得到的判讀技巧。
⚠️ 1010 要兩步都檢查:scripts/wms_client.py 只在取 token 那步認 1010,
查詢那步被 Cloudflare 擋會變成「回應非 JSON(HTTP 403)」這種看不懂的訊息,自己包一層時要補上。
🔴 分類碼可以落庫,但回應原文只寫伺服器 log、不要落庫也不要顯示——
暢流的自由文字備註欄可能被人填進收件人姓名電話。
安全須知
- 金鑰絕不寫進程式或 skill。一律用環境變數或各專案的設定檔(並加入 .gitignore)。
- API_ID、API_KEY、個資解密金鑰外洩等同帳號被接管,請妥善保管。
- 解密後的姓名/電話/地址屬個資,依個資法妥善處理,勿外流或寫入版控。
- ⚠️ 「個資」不等於「那兩個密文欄」:入庫單的
sender_name/sender_phone不在加密範圍內,demo/products[].notice(訂單則是buyer_msg/remark/notice)是自由文字欄可能藏姓名電話。 回應原文整包存檔/寫進資料庫(快照、除錯用 dump)之前先剔除這些欄位(見眉角 4)。 這些欄位會不會有值取決於各租戶怎麼建單,不能假設它們永遠是空的。 - 寫 log 也算落地:錯誤處理時只寫例外類別與訊息,不要把回應原文整段寫進 log。 要落庫的請落上面那組錯誤分類碼,不要落原文。
- 用不到收件人個資就不要申請「個資存取權限」、不要保管解密金鑰:沒權限拿到的本來就是
密文或
****,這是成本最低的防線。 - 🔴 暢流有一批端點會真的改倉庫資料(新增訂單/取消訂單/訂單揀貨/新增商品/庫存更新/
新增與取消入庫單/物流確認、請求、完成),它們和查詢端點混在同一張表裡
(
references/api-reference.md第 6 節,表內已逐支標記讀/寫)。 除非任務明確要求寫入,預設只打查詢端點。 - ✅ 寫入端點的白名單要寫進客戶端,不要靠自律:把允許打的路徑列成清單,不在清單的直接丟例外。 這樣呼叫端寫錯、或日後有人「順手加一支」,門仍然關著;靠「我記得別打那幾支」遲早會破。
- ⚠️ 不要拿 GET/POST 當「會不會改資料」的判準,兩個方向都會判錯:
logistics/label.php(取寄件單網址)是 POST 但沒有副作用(✅ 2026-08 實測:16 張不同的 已取號單、40 餘次呼叫,每次呼叫前後各回查一次訂單,物流單號與狀態碼全程零異動; ⚠ 未取號的單完全沒測);而掛 GET 的inventory/stock_allocation.php(庫存調撥) 反而看不出是查調撥紀錄還是執行調撥(📄 未實測)。
面單(寄件單)怎麼拿(最容易照文件想像卡死的一題)
- 訂單查詢的
print_url不是每張單都有:實測黑貓/新竹/MOMO甲配/店到家恆空 (近 30 天逐狀態切片,出完貨也不會長出來,不是時間差),只有 mo店+ 走 momo 第三方物流的 宅配通那條 100% 有值。決定有沒有的是物流商/運送方式,不是來源代碼——同一個source_key=moplus走黑貓時一樣全空。另外 F 待處理/W 轉單中狀態下,print_url/send_num/AllPayLogisticsID三欄都必空。 - 這幾家的面單只有
POST logistics/label.php拿得到:必須送 JSON body{"order_no":"…"}(form-data 會被回「請輸入JSON格式的資料」),回label_url/limit_time/send_num。 - 🔴
label_url回的是網頁不是 PDF、效期以回傳的limit_time為準、別寫死(早期實測約 30 分鐘;2026-08-25 兩批同租戶實測皆為+4 小時)——無論多長,取到就儘快抓/渲染, 不可先收集一整批網址再慢慢處理。多箱單(✅2026-09-14)一箱一張面單、渲染成 N 頁,各頁文字層有自己的物流單號,但沒有箱序; ⚠ 2026-09-11 還遇過「3 個號、頁面只有最後一號那一張」(暢流其後已改)——批次印單要驗「頁數=號碼數」。 - 🔴 它是 POST:實測對已取號的單零副作用,但未取號的單從沒測過,最壞情況是觸發 取號=真寫入、不可逆。保守做法是在客戶端寫死「沒有有效物流單號就不打」。
- 完整規格(多箱行為、面單轉成可印檔案的三條路、黑貓渲染的三個必要條件)見
references/api-reference.md§6.5。⚠ 上述數據取自單一廠商 API 帳號、2026-08, 不是暢流全域鐵律。
物流單號 send_num 的三個坑(很常踩)
實測整理(樣本=單一廠商,細節與數字見 references/api-reference.md 附錄 A-1):
- 「沒有號碼」有兩種長相。空值=還沒取號(取號在 W 轉單中 → P 轉揀貨 之間,
W/F 階段必定沒有);一串全 0(實測 8 碼)=手開單/Excel 匯入這類暢流本來就不給號的單。
判「有沒有號」要看「去掉所有 0 之後還有沒有字元」,不要比對字串
"00000000"; 而且絕不可拿全 0 那批當索引鍵,它們會全部撞在一起。 另外,走廠商自送/自取的單(取號渠道logistics=none)永遠不會有暢流物流單號。 - 一單多箱是逗號串。黑貓多箱時,暢流把每箱的號碼用半形逗號串成一格塞在
send_num, 拿整串比對單一箱的條碼永遠對不上,要先拆開。號碼格式也跨平台不統一(8~15 碼、有純數字有英數混合), 不要用長度或字集去驗。 - 不能用物流單號反查暢流。
order_query.php沒有這類參數,帶了會被靜默忽略、照樣回整批 (不是查無,見上面〈拉單完整性〉);order_logistics.php只認order_no。 要反查只能自己拉日期區間、在本地建索引。
折扣資料的限制(常被問)
discounts(賣家折扣/平台補助)只有這些來源提供(下面寫的是 source_key 代碼,括號是平台名):
shopline(SHOPLINE)、cyberbiz(Cyberbiz)、shopee(蝦皮)、91appapi(91APP(API))/_91app(91APP(Excel))、
qdm(QDM)、shopify(Shopify)。
⚠ 不是 91app、也不是大寫 QDM——這兩個在 source_keys.py 裡查無此代碼,拿去送 source_key 會篩不到東西。
momo、moplus、coupang 等沒有 discounts(一律空陣列),只能拿到 total_price 總額。
蝦皮折扣 key:voucher_from_seller(賣場券)、voucher_from_shopee(蝦皮券)、coins(蝦幣)。