《1688物流API接口边界:logistics.trace.get 与 freight.template.list 的隐藏约束》(附Python源码)
先拍结论:
1688 物流域有两个"看起来简单、实际上暗坑极多"的接口:
alibaba.logistics.trace.get不是按运单号查世界的,而是"按订单/物流编号看平台已录入的轨迹";
freight.template.list(/delivery.template.list)不是用来算运费的,而是"读商家运费模板结构"的,真算钱要靠重量/件数/地区/子模板自己复刻平台规则。把"轨迹查询"当实时快递网关、把"模板列表"当运费计算器,是 ERP 接入 1688 物流最常见的两类幻觉。
一、alibaba.logistics.trace.get:轨迹接口的真实边界
1. 它到底吃什么参数
官方文档里关键是这三个:
order_id:必须,交易订单号trade_source_type:必须 ,例如cbu-tradelogistics_id:可选,订单下的物流编号(如AL8234243)
返回的是:
trace_list[]
├─ logistics_id
├─ order_id
├─ logistics_bill_no # 真正运单号
└─ logistics_steps[]
├─ accept_time
├─ remark
└─ trace_node_list[]
├─ accept_time
├─ action # TRANSPORT / SIGN / UNSIGN
├─ facility_name
├─ facility_type # 网点 / 分拨中心
├─ area_code
└─ remark
⚠️ 重点:不是
company_code + waybill_no查全网快递;它是"1688 订单关联的那张物流单,平台现在知道哪些节点"。
2. 隐藏约束(文档不写但会咬人)
- 未发货查不到
- 订单没发货 → 返回成功但
trace_list空,或别处接口直接报500_2 订单尚未发货 - 正确动作:监听"已发货消息"后再查,不要定时盲扫所有单
- 订单没发货 → 返回成功但
- 轨迹有录入延迟
- 快递公司已经派送了,1688 侧可能晚几分钟到几小时
- 不能拿
trace.get的"无 SIGN 节点"直接判"未签收"
- 一个订单可能多物流单
- 拆单发货:A 仓发 2 件、B 仓发 3 件
- 只看第一个
trace_list[0]会丢货
- action 不是淘宝那套
- 1688:
TRANSPORT / SIGN / UNSIGN - 淘宝:
ARRIVE / SIGN / SENT_SCAN - 直接复用淘宝状态机 = 签收逻辑错乱
- 1688:
- bill_no 可能后期才补
- 供应商"点了发货"但电子面单没回写 →
logistics_bill_no空 - 这时候要:等 → 重推 → 再查,不是立刻报异常
- 供应商"点了发货"但电子面单没回写 →
- QPS 不是无限
- 物流类也吃开放平台流控,盲轮询 500 单/30s 必被限流
二、freight.template.list / delivery.template.list:模板接口的真实边界
1. 它干什么
查"某个商家/应用下"的运费模板列表:
- 模板名
- 计费维度
- 是否包邮
- 子模板(快递 / 货运 / 系统模板)
- 地区费率
2. 计费维度藏在子模板里
第三方/商品域文档里能看到这种结构:
DeliverySubTemplate
chargeType:
0 = 按重量
1 = 按件数
2 = 按体积
serviceType:
0 = 快递
1 = 货运
2 = 货到付款
serviceChargeType:
0 = 卖家承担
1 = 买家承担
firstUnit # 首重(克) / 首件(件) / 首体积
firstUnitFee # 单位:分
nextUnit
nextUnitFee
leastExpenses # 最低一票
toAreaCodeText # 上海、福建、广东
重点:接口给你"规则",不给你"这笔订单运费 = 12.5"。
平台自己算,因为还要叠:订单总重、是否跨模板、是否满包邮、买家地区、是否货到付款。
3. 隐藏约束(成本算错就亏)
- 多商品多模板 ≠ 简单累加
- 老逻辑:各商品按自己模板算,再加总
- 1688 新逻辑(2024-10 后):同订单多模板时,用总重/总件数分别套每个模板,取对买家更低的那个
- 首重单位可能是克不是千克
firstUnit=1000是 1000 克- 你用 1.5 kg 直接塞进
firstUnit=1的模板 → 运费翻 1000 倍
- 包邮门槛是"实付货值"还是"含运费"要分清
- 满 99 包邮:通常是货值满 99,不是货值+运费满 99
- 系统模板(isSysTemplate)不能当普通模板改
- 平台默认偏远地区/默认快递,覆盖逻辑要单独处理
- 模板列表 ≠ 商品当前生效模板
- 商品可能绑了模板 A,但商家刚改成模板 B
- 下单前要看商品详情里的
freightTemplateId,不是"商家最后一个模板"
- 跨境/货到付款/大件货运是不同 serviceType
- 用快递模板算大件 = 血亏
三、生产向:轨迹归一化 + 运费预估(不是精算)
1. 轨迹客户端
python
# ali1688/logistics_trace.py
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class TraceNode:
accept_time: str
action: str # TRANSPORT / SIGN / UNSIGN
facility_name: str
area_code: str
remark: str
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
@dataclass
class LogisticsTrack:
logistics_id: str
bill_no: Optional[str]
nodes: list[TraceNode]
raw: dict = field(default_factory=dict)
@property
def signed(self) -> bool:
return any(n.action == "SIGN" for n in self.nodes)
@property
def latest_remark(self) -> str:
return self.nodes[-1].remark if self.nodes else ""
class Ali1688TraceClient:
def __init__(self, top_client, qps_limiter):
self.top = top_client
self.limiter = qps_limiter
def get_track(self, order_id: int, logistics_id: str = None) -> Optional[LogisticsTrack]:
self.limiter.acquire("logistics.trace.get")
params = {
"order_id": order_id,
"trade_source_type": "cbu-trade",
}
if logistics_id:
params["logistics_id"] = logistics_id
# resp = self.top.execute("alibaba.logistics.trace.get", params)
resp = {"trace_list": []} # 伪响应
traces = []
for t in resp.get("trace_list", []):
nodes = [
TraceNode(
accept_time=n.get("accept_time", ""),
action=n.get("action", ""),
facility_name=n.get("facility_name", ""),
area_code=n.get("area_code", ""),
remark=n.get("remark", ""),
)
for n in t.get("trace_node_list", [])
]
traces.append(LogisticsTrack(
logistics_id=t.get("logistics_id", ""),
bill_no=t.get("logistics_bill_no") or None,
nodes=nodes,
raw=t,
))
# 多物流单:返回列表,不让上层只看第一单
return traces
消费侧铁律:
tracks = client.get_track(order_id)
if not tracks:
# 可能是未发货 / 延迟录入 → 进"待补查队列",不报失败
return {"state": "no_trace_yet"}
all_signed = all(t.signed for t in tracks)
any_bill = [t.bill_no for t in tracks if t.bill_no]
2. 运费模板"预估器"(只做采购成本下限)
python
# ali1688/freight_estimator.py
from dataclasses import dataclass
from enum import IntEnum
class ChargeType(IntEnum):
WEIGHT = 0 # 克
COUNT = 1 # 件
VOLUME = 2 # 立方厘米/方
@dataclass
class FreightRule:
charge_type: int
first_unit: float
first_fee_fen: int
next_unit: float
next_fee_fen: int
least_fee_fen: int
area_text: str
buyer_pays: bool
def estimate_freight(rule: FreightRule, qty: float) -> int:
"""
qty:
- 重量场景 = 克
- 件数场景 = 件
- 体积场景 = 体积单位
"""
if qty <= 0:
return 0
if qty <= rule.first_unit:
fee = rule.first_fee_fen
else:
extra = qty - rule.first_unit
steps = (extra + rule.next_unit - 1) // rule.next_unit
fee = rule.first_fee_fen + int(steps) * rule.next_fee_fen
return max(fee, rule.least_fee_fen)
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# 多模板订单:平台新逻辑是"取低"
def pick_1688_new_logic(rules: list[FreightRule], total_qty: float) -> int:
fees = [estimate_freight(r, total_qty) for r in rules]
return min(fees) if fees else 0
注意:这只是采购侧成本预估/异常拦截。
真要对买家收银:还得叠地区匹配、包邮门槛、订单级多模板、货到付款加价------这些平台不给你算好,也不保证你复刻得和 1688 完全一样。
四、和前几篇的拼接
- 《1688 订单 API》 :
trace.get是订单状态机的"物流子状态源" - 《1688 商品 API》 :
freightTemplateId在商品里,模板规则在freight.template.list里 - 《统一采购适配层》:运费要进"采购成本",不是直接等于"买家付的运费"
- 《两套接口边界》:物流轨迹/运费模板是"供货侧元数据",不能当成"闲鱼/Mercari 买家物流页的直接数据源"
五、上线前检查表
logistics.trace.get
- 只在"已发货"后查,不盲轮询
- 处理多物流单(拆单)
-
bill_no为空进补偿队列 - action 用 1688 枚举,不抄淘宝
- 无 SIGN 不代表未签收(有延迟)
- 物流类接口走令牌桶
freight.template.list
- 不把"模板列表"当"订单运费"
- 重量用克、件数用件、体积用体积极
- 多模板订单用"平台新逻辑取低"
- 包邮门槛/地区/最低一票都生效
- 商品绑哪个模板以商品详情为准
- 运费只做成本预估,不对买家强制精算
六、一句话收口
logistics.trace.get是**"平台知道到哪了"**,不是"快递公司官网";
freight.template.list是**"商家定了个规则"**,不是"这笔单运费多少钱"。1688 物流对接的成熟度 =
轨迹当"事件流"消费,运费当"成本模型"估算,绝不假装自己是 1688 计费内核。