从下单到签收 AI 能不能说明白:shippingDeliveryTime 与 TransitTimeTable 的配置数据

2026-09-23 01:21:41 0 次浏览
GEOschema.orgJSON-LDPython结构化数据跨境电商

北卡一位采购在 AI 搜索里问「Northbay 的铝合金躺椅发到萨凡纳港要几天」,屏幕上的回答是「通常 7 到 15 个工作日」。业务同事把截图甩进群里:7 天是空运,15 天是海运拼柜,两条线的报价差四倍,AI 把两个数字揉成了一句没人敢拿去签合同的话。 第 3 周复盘时我们把页面拆了一遍,发现问题不在模型。站上关于时效的内容全写在配送政策页的四段散文里,运输方式、截单时间、目的地混在一起,而 Offer 节点上除了 price 和 availability 什么都没有。 这篇记的就是怎么把「下单到签收」这件事拆成可被机器定位的数据:生成式引擎优化(Generative Engine Optimization, GEO)里最容易被跳过的一块,恰恰是买家问得最多的一块。

适用读者:外贸独立站与跨境电商的前端、SEO 工程师,以及需要把物流时效落成结构化数据的运营同学。读之前知道 JSON-LD 长什么样就够,不要求写过 schema.org 标记。

一句「到美国东岸多久」,为什么答成了区间

Northbay 的情况不算特殊。1,400 个 SKU,配送政策页有三份(US / EU / AU),每份由不同的人在不同时间写的。空运那句写「2-4 business days after dispatch」,海运那句写「4-6 weeks」,中间还夹了一句「orders placed before 2pm EST ship same day」。人读得懂,机器拿到的是三段各说各话的文本,而且没有一个数字挂在具体港口上。

货车沿世界地图路线与时间轴行驶

AI 搜索处理这类提问时走的路子比较固定:先定位实体,再找实体上挂的数值,把数值拼进答案。页面上找不到可定位的数值节点时,它只能把整段文字当上下文引用。于是出现「7 到 15 个工作日」——这个区间是模型自己从两段文本里取的最小值与最大值拼出来的,站上从来没有过这个承诺。

更麻烦的是后续。那位采购拿着这句话来问销售,销售报价走的是海运,客户回了一句「你们官网说 15 天,为什么报价单写 4 周」。这单最后没丢,但来回解释了四封邮件。第 6 周我们把同样的问题在三个 AI 助手里各问了 4 遍,12 次里有 9 次给的是含糊区间,2 次直接说「该站点未提供配送时效」,1 次把空运时效安在了海运 SKU 上。

schema.org 里跟物流时效有关的是哪几个类型

先说清楚命名,这块的叫法在社区里一直是乱的。很多人嘴里的 shippingDeliveryTime,在 schema.org 里其实不是一个属性名,而是一整块结构的简称:Offer 上的 shippingDetails 指向 OfferShippingDetails,OfferShippingDetails 的 deliveryTime 指向 ShippingDeliveryTime。真正要写的是后两个类型。

OfferShippingDetails 负责「这条航线送到哪、多少钱、不发货的地方有哪些」,ShippingDeliveryTime 负责「多久到」。它俩是嵌套关系,不是并列关系,写反了校验器不一定报错,但语义会变成运费里夹了一段文本。

ShippingDeliveryTime 只有四个自有属性,但每个都有坑:

  • handlingTime:下单到出库。按惯例算工作日,写成 QuantitativeValue。
  • transitTime:出库到签收。同样是 QuantitativeValue,单位用 UN/CEFACT 推荐代码。
  • cutoffTime:当天截单时间,ISO 8601 时间且必须带时区偏移。过了这个点,起算日往后推一天。
  • businessDays:哪几天算工作日,用 OpeningHoursSpecification 表达。
位置 字段 该填什么 填错了会怎样
Offer shippingDetails OfferShippingDetails 数组,一条航线一个对象 时效只剩散文,AI 只能整段引用
OfferShippingDetails deliveryTime ShippingDeliveryTime 对象 时效被读成运费的附属文本
ShippingDeliveryTime handlingTime 下单到出库,QuantitativeValue 缺失时窗口从 0 起算,承诺偏乐观
ShippingDeliveryTime transitTime 出库到签收,QuantitativeValue 缺失时按当天到达处理
ShippingDeliveryTime cutoffTime 如 14:00-05:00,带 UTC 偏移 无偏移时跨时区解释会漂
ShippingDeliveryTime businessDays OpeningHoursSpecification 周末被算进时效窗口
OfferShippingDetails shippingRate MonetaryAmount 运费与时效混在一个实体里
OfferShippingDetails shippingDestination DefinedRegion,可到州与邮编段 目的地只能按国家粒度猜
OfferShippingDetails transitTimeLabel 与设置页对齐的标签文本 分层时效找不到归属
OfferShippingDetails doesNotShip true 表示该地区不发货 与 deliveryTime 同写会自相矛盾

时长不要用 ISO 8601 的 P3D 这种写法。schema.org 文档给的例子是 QuantitativeValue 配 minValue / maxValue / unitCode,unitCode 取 d(天)或 HUR(小时)。写成 P3D 的页面在校验器里照样过,但多数引擎不会去做时长解析,那个值就退化成一段字符串。

原理剖析:AI 引擎从 JSON-LD 到一句话答案的中间几步

这一节讲清机器侧到底发生了什么,因为后面所有配置取舍都跟它有关。

第一步是结构化优先的抽取。 爬虫拿到 HTML 后,JSON-LD 里的节点会先被解析成一张实体小图,散文是兜底来源。原因是 JSON-LD 里的类型与属性名是显式的,抽错的代价低;从段落里识别「4-6 weeks」属于哪条航线、算不算工作日,则要做指代消解,成本高且不稳定。同一份信息两边都有时,结构化那份的权重更高。

第二步是数值归一。 QuantitativeValue 的 minValue、maxValue 加 unitCode,构成一个可计算的区间。d 这个单位代码让引擎知道要按天累加,HUR 则按小时。散文里的「4-6 weeks」虽然人类一眼就懂,但它是一段文本,引擎拿到的只是 token,能做的最多是原样引用。

第三步是窗口计算。 到达窗口大致是这样合出来的:最早 = handlingTime.minValue + transitTime.minValue,最晚 = 两个 max 相加;下单时间晚于 cutoffTime 时,起算日顺延到下一个 businessDays 里的日子。这就是为什么 handlingTime 和 transitTime 都要给两端——只写一个值,窗口会塌成「第 N 天到」,现实中极少成立。

第四步是航线选择与答案合成。 同一个 Offer 上挂了多条 OfferShippingDetails 时,引擎要决定引用哪一条。这时候 transitTimeLabelshippingDestination 就成了选择器:问句里出现「萨凡纳」或「佐治亚」,匹配到 addressRegion 含 GA 的那条;问句里出现「快一点」「加急」,才轮到标签里带 express 的那条。匹配不上时,模型通常把所有候选的极值并成一个区间,再补一句「通常」「一般」这类软词——这正是我们最初看到的那句回答的来历。

flowchart LR
  A[HTML 页面] --> B{能否取到 JSON-LD}
  B -->|有| C[ShippingDeliveryTime 节点]
  B -->|无| D[从散文里猜数字]
  C --> E[数值归一 min/max + unitCode]
  D --> F[字符串,只能整段引用]
  E --> G[算窗口 handling + transit]
  G --> H[按目的地与标签选航线]
  H --> I[合成一句话答案]
  F --> I

图里那条虚线路径(散文兜底)不是不能用,只是它产出的答案你控制不了。把数据补上,本质是给引擎一条它更愿意走的路。

值得单独说一句的是:结构化数据只提高「被正确引用」的概率,不保证被引用。引擎还会看来源权威性、页面是否被其他站点印证。所以别指望加了标记第二天回答就变,我们实际观察到变化是在第 3 周之后。

分层时效表:TransitTimeTable 这个词到底指什么

TransitTimeTable 不是 schema.org 的类型名,社区里用它指代「一个站点所有航线时效的清单」。落地方式靠 shippingSettingsLink:OfferShippingDetails 上给一个 URL 和 transitTimeLabel,URL 指向一个专门发 DeliveryTimeSettings 列表的页面,页面里每一条用相同的 transitTimeLabel 命名,两边靠标签对上。

这么绕是有原因的。一个 SKU 可能对应十几条航线,把每条的时效都内联进 Offer,页面体积会失控,而且改一次时效要刷几千个产品页。抽成设置页之后,改一次全站生效。

flowchart TD
  P[Product] --> O[Offer]
  O --> S1[OfferShippingDetails 标签 us-east-express]
  O --> S2[OfferShippingDetails 标签 us-east-economy]
  O --> S3[OfferShippingDetails doesNotShip true]
  S1 -.shippingSettingsLink.-> T[设置页 transit-table]
  S2 -.shippingSettingsLink.-> T
  T --> D1[DeliveryTimeSettings 空运]
  T --> D2[DeliveryTimeSettings 海运]
  D1 --> W1[handling 0-1 + transit 2-4]
  D2 --> W2[handling 1-2 + transit 24-32]

设置页里还有个容易忽略的开关:isUnlabelledFallback。标成 true 的那条不给标签,表示「没有匹配到具体标签时用这条兜底」。它和 transitTimeLabel 不能同时有意义地出现,文档里也写了这一点。我们给兜底那条放的是全站最保守的时效,宁可回答慢一点也不想再出现凭空拼出来的区间。

有个版本差异要提醒:schema.org 已经把 DeliveryTimeSettings 标记为被 ShippingConditions 取代的类型,页面上有 superseded 提示。现有文档和大量已发布页面还在用这套标签引用模式,短期内不会失效,但新项目做技术选型时可以留意一下后续类型。

代码:从航线配置表生成 shippingDetails

环境是 Python 3.10+,只用标准库,没有第三方依赖。下面这段把业务侧维护的航线表渲染成 JSON-LD 片段,接在现有的模板渲染流程里即可。

# 环境:Python 3.10+,仅标准库。作用:航线表 -> shippingDetails
import json

# 一条航线 = 目的地 + 运输方式
# handling 是下单到出库,transit 是出库到签收,单位都是天
LANES = [
    # 标签, 国家, 州列表, 处理区间, 在途区间, 运费, 币种
    ("us-east-express", "US", ["GA", "SC", "NC"], (0, 1), (2, 4), 28.0, "USD"),
    ("us-east-economy", "US", ["GA", "SC", "NC"], (1, 2), (24, 32), 9.5, "USD"),
]

# 单位代码取 UN/CEFACT 的 d
# 写中文「天」基本就废了,写 DAY 多数引擎也认
UNIT = "d"

def qv(pair):
    # 转成 QuantitativeValue,两端都给区间才立得住
    lo, hi = pair
    return {"@type": "QuantitativeValue", "minValue": lo, "maxValue": hi, "unitCode": UNIT}

def shipping_details(lane):
    # 拆字段:标签/国家/州/处理/在途/运费/币种
    label, country, region, handling, transit, rate, cur = lane
    return {
        "@type": "OfferShippingDetails",
        # 标签要和设置页 DeliveryTimeSettings 的 transitTimeLabel 一致
        "transitTimeLabel": label,
        # 设置页是分层时效的单一事实来源
        "shippingSettingsLink": "https://example.com/shipping/transit.jsonld",
        # 目的地给到州,问到具体港口时才匹配得上
        "shippingDestination": {"@type": "DefinedRegion", "addressCountry": country, "addressRegion": region},
        "deliveryTime": {
            "@type": "ShippingDeliveryTime",
            # 下单到出库这一段
            "handlingTime": qv(handling),
            # 交给承运商之后这一段
            "transitTime": qv(transit),
            # 截单时间必须带时区偏移
            "cutoffTime": "14:00-05:00",
            # 工作日决定窗口从哪天开始算
            "businessDays": {"@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]},
        },
        # 运费单独挂,别和时效塞进同一个对象
        "shippingRate": {"@type": "MonetaryAmount", "value": rate, "currency": cur},
    }

# 输出:一个 Offer 上挂两条航线
print(json.dumps({"@type": "Offer", "shippingDetails": [shipping_details(x) for x in LANES]}, indent=2))

跑出来的单条航线长这样,transitTimeLabel 是连接两边的关键字段:

{
  "@type": "OfferShippingDetails",
  "transitTimeLabel": "us-east-express",
  "shippingSettingsLink": "https://example.com/shipping/transit.jsonld",
  "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "US", "addressRegion": ["GA", "SC", "NC"] },
  "deliveryTime": {
    "@type": "ShippingDeliveryTime",
    "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "d" },
    "transitTime": { "@type": "QuantitativeValue", "minValue": 2, "maxValue": 4, "unitCode": "d" },
    "cutoffTime": "14:00-05:00",
    "businessDays": { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] }
  },
  "shippingRate": { "@type": "MonetaryAmount", "value": 28.0, "currency": "USD" }
}

校验器能查语法,查不出「写得合法但会误导」。下面这段是我们加在 CI 里的自检,挡掉了几类真出过的问题:区间写反、单位写成中文、不发货地区还挂着时效。环境同样是 Python 3.10+ 标准库。

# 环境:Python 3.10+,仅标准库。作用:上线前拦掉误导性写法
import json
import re
import sys

# 时长单位白名单:d 是天,HUR 是小时
UNITS = {"d", "DAY", "HUR"}
# 截单时间必须带 UTC 偏移,否则跨时区解释会漂
CUTOFF = re.compile(r"^\d{2}:\d{2}(:\d{2})?([+-]\d{2}:\d{2}|Z)$")

def check(offer):
    # 一次跑完再统一报,方便批量改
    errors = []
    # 一个 Offer 上通常有多条航线,逐条查
    for sd in offer.get("shippingDetails", []):
        # 标签用于定位是哪条航线出的问题
        name = sd.get("transitTimeLabel") or "(无标签)"
        # 不发货的地区不该同时给时效,两者互斥
        if sd.get("doesNotShip") and sd.get("deliveryTime"):
            errors.append(f"{name}: doesNotShip 与 deliveryTime 冲突")
        # 时效块可能整块缺失
        dt = sd.get("deliveryTime") or {}
        # 处理与在途两段都要有
        for key in ("handlingTime", "transitTime"):
            qv = dt.get(key)
            # 缺一段会让另一端被当成 0,窗口直接塌掉
            if not qv:
                errors.append(f"{name}: 缺 {key}")
                continue
            # 单位代码不认识就退化成字符串
            if qv.get("unitCode") not in UNITS:
                errors.append(f"{name}: {key} 的 unitCode 非法")
            # 区间写反是低级错误,但真会写反
            if qv.get("minValue", 0) > qv.get("maxValue", 0):
                errors.append(f"{name}: {key} 区间写反")
        # 截单时间格式
        if "cutoffTime" in dt and not CUTOFF.match(dt["cutoffTime"]):
            errors.append(f"{name}: cutoffTime 缺时区偏移")
    return errors

# 用法:python check_shipping.py offer.json
if __name__ == "__main__":
    # 读入即将上线的 JSON-LD 文件
    data = json.load(open(sys.argv[1], encoding="utf-8"))
    bad = check(data)
    # 有问题就退 1,让流水线拦下来
    print("\n".join(bad) or "全部通过")
    sys.exit(1 if bad else 0)

这个脚本拦不住语义层面的一致性,比如设置页里改了时效但产品页的标签没同步。那部分我们靠构建时比对标签集合来兜:产品页出现的所有 transitTimeLabel,必须能在设置页里找到同名条目,找不到就构建失败。

45 天对照:改了什么,变了什么

改完之后我们在三个 AI 助手里固定问同一组 12 个问题,每两周记录一次。下面是第 45 天的对照,前 6 个是客户真实问过的高频问法。

客户问题 改造前 改造后
发到萨凡纳港要几天 通常 7-15 个工作日 空运 3-5 天,海运 25-34 天
海运到美东贵还是空运贵 未给出价格 空运 28 美元起,海运 9.5 美元起
下午 3 点下单当天能发吗 未提及截单 工作日 14:00(美东)前下单当天出库
能不能发到夏威夷 未说明 不发货(doesNotShip)
到加州和到佐治亚一样快吗 一样 加州 4-6 天,佐治亚 3-5 天
周末算工作日吗 未说明 周一至周五出库

12 个问题里,能给出分层答案的从 1 个变成 9 个;剩下 3 个仍答得含糊,都是跨了设置页和产品页两边信息才能回答的组合问法(比如「含清关一共多少天」)。这类问题我们还没解决,因为清关时长没落在结构化数据里,属于后续要补的字段。

顺带一个副作用:销售那边用「官网说 15 天」来扯皮的邮件没有了。这算不上什么指标,但对团队来说比命中率更有体感。

几个容易踩的坑,和一句澄清

handlingTimetransitTime 只写一段,是最常见的一类。页面能过校验,AI 读到的是半截窗口,回答会比实际承诺乐观。反过来把 handlingTime 写成总时长、transitTime 空着,语义就整个歪了。

截单时间写 14:00 不带偏移也很多。跨时区站点上这个值是没法解释的,美东的下午两点和采购所在地的下午两点差着十几个小时,AI 只能跳过不提。

还有一个误区值得单独说:结构化数据不是给 AI 看的独家通道。同一份 shippingDetails,Google 的商家列表富媒体结果也在读,站内的配送说明页、购物车结算页也该复用同一份配置。我们最初只在产品页加标记,结算页还是老文案,结果客户在结算页看到的时效和 AI 说的不一致,又是一轮解释。数据同源这件事不做,加多少标记都会漏。

至于「加了标记回答就准了」这种期待,实测下来不成立。它提高的是被引用的正确率,不是被引用的次数,而且生效有滞后。第 1 周几乎没变化,第 3 周开始有分层答案出现,第 6 周才稳定下来。

你们站点上的配送时效是怎么组织的?如果有把清关时长、节假日停运这些也结构化进去的做法,评论区聊聊,我这边一直在找能落的结构。

参考与延伸

GEO|AI搜索优化|schema.org|OfferShippingDetails|ShippingDeliveryTime|JSON-LD|结构化数据|跨境电商

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。