这个报错最容易把人带偏,因为结账页上姓名、街道、邮编全都有,看起来不可能“缺地址”。我第一次处理时一直盯着表单校验,后来才发现:页面展示的是浏览器里保存的地址对象,提交订单时后端检查的却是当前 Quote 里的 shipping address。两边并不是同一份状态。

所以我现在遇到这句话,第一件事不是让用户重填,而是确认最后一次 place order 使用的 cart_id,是否就是保存地址时的 cart_id。PWA、移动端和做过 checkout 定制的站点,登录、合并购物车或刷新 token 后很容易换了 cart,页面却还留着旧地址。

先看请求顺序,不要先猜字段

在浏览器 Network 里按 cart_id 过滤,把一次完整结账串起来。实体商品通常应该看到这样的顺序:

  1. setShippingAddressesOnCart 成功,并返回 shipping_addresses;
  2. 重新读取可用配送方式;
  3. setShippingMethodsOnCart 成功;
  4. 设置账单地址、支付方式;
  5. placeOrder 使用同一个 cart_id。

如果地址请求在前端显示“成功”,但响应的 shipping_addresses 是空数组,就不要继续看支付模块。先保存该请求的 variables 和响应,再到后端日志里用时间与请求 ID 对齐。常见情况是前端吞掉了 GraphQL 的 errors,只判断 HTTP 200。

数据库只用来确认事实,不要直接补地址

拿到 Quote 的内部 ID 后,可以只读检查地址是否真正落库:

SELECT entity_id, is_active, customer_id, store_id, items_count
FROM quote
WHERE entity_id = :quote_id;
SELECT address_id, address_type, country_id, region_id, region,       postcode, city, shipping_method, collect_shipping_rates
FROM quote_address
WHERE quote_id = :quote_id;

实体商品应该至少有一条 address_type = 'shipping' 的记录。记录存在也不等于可用,还要看国家、region、邮编和配送方式。某些国家要求 region,前端传了文字 region 却没传正确的 region_id,地址能显示,后续校验仍可能失败。

不要为了赶紧恢复下单,直接往 quote_address 插数据。地址保存不仅是几列字段,还会触发运费收集、税费和扩展属性处理;手工补行往往把“缺地址”变成更难追的金额错误。

我会特别检查这四个分支

  • 登录后 cart 发生合并:游客 cart_id 已失效,页面组件仍把地址写给旧 cart;
  • 自定义地址组件只改了 UI 状态:Knockout/React store 更新了,但没有真正调用保存地址的 mutation;
  • 配送方式重算清掉了地址或 method:切换国家、优惠码或门店自提后,异步响应乱序覆盖了新状态;
  • 购物车全是虚拟商品:本来就不需要 shipping address,此时若自定义 place-order 校验仍强制要求它,判断位置写错了。

修复后怎样证明不是“碰巧好了”

我会开慢速网络复测一次,再分别测试游客、登录客户、登录时合并购物车,以及返回上一步修改国家。每次都记录 cart_id,并确认保存地址的响应先回来,配送方式才允许选择。订单创建后,再核对订单 shipping address 与用户最后一次提交一致。

如果加一条“为空就再调一次保存地址”的重试便不报错,这通常只是掩盖竞态。真正的修复是让结账状态只认当前 cart,并让地址、运费、支付按可观察的顺序推进。