今年四月,合作了三年的一家承运商把面单接口换到新版本。旧接口用一个字段装完整地址,新接口拆成省、市、区、街道、详细地址五段。我们没当回事,直接把旧字符串塞进新字段的详细地址里,结果打印出来的面单上,门牌号被截掉了。
一天四百多单,六十多单被网点退回,理由统一写着地址不完整。客服打了两天电话,重新打印的费用按张算,数额不算大,客户的信任掉得厉害。
这篇写这次换代的处理过程,以及后来固化下来的一套适配配置。结论先说:接口换代出问题,九成不是新接口难用,是旧数据里那些"拼在一起才完整"的字段,拆不开。
旧接口的地址是一个自由文本,长度上限一百二十个字,仓库的人怎么写都行。有人写"某某路十八号三号楼二层",有人写"某某路18号3号楼2层",还有人把收货人电话也写进去。
新接口要求省、市、区、街道四段分开,每段都有长度限制,详细地址只留六十个字。原来那一整串塞进详细地址,前面的省市区没了,后面的门牌被截断。
问题出在数据源头。旧系统从来没存过结构化地址,只是在打印的时候把几段拼起来。换代的时候想把拼好的拆回去,等于让系统做一次没有标准答案的还原。
旧接口只有一个必填项:运单号。新接口必填的有七项,收货人手机号必须十一位纯数字,重量必须按克传整数,件数不给默认值。
重量这一项坑了我们两天。旧系统存的是千克,带两位小数,一点二五公斤。新接口要一千二百五十克。直接传一点二五,被判定为超轻件,整批单子挂在待处理。
长度限制也不是小事。收货人姓名旧接口允许三十个字,新接口只给二十个。有几个企业客户的联系人写的是"某某公司仓储部某某某",二十个字装不下,接口直接拒单。
接口通了,面单打出来还是错的。打印模板里字段位置是按旧格式排的,新接口返回的字段名换了,模板取不到值,打出来一片空白。
那两天现场的状态是:接口返回成功,面单打不出来,仓管拿着手机拍屏幕上的地址手写。一天四百单,手写到晚上九点。
模板要跟着接口一起改,这件事在计划里被漏掉了。后来我们把"接口加模板"当成一个整体来验收,不再分开测。
换代的正确姿势不是改内部表结构,是在内部模型和外部接口之间加一层映射。内部还是那套订单、收货人、地址,出口的地方按接口版本翻译一次。
这样做的直接好处是:下一家承运商的接口再变,只需要再写一份映射,内部代码不动。我们后来接第二家承运商,只花了半天。
映射层要能处理三件事:字段改名、格式转换、缺失兜底。字段改名靠配置,格式转换靠函数,缺失兜底靠默认值和拦截规则。
接口版本要写在配置里。代码里硬编码一个版本号,下次升级又要发版。配置里写,改一行就生效,回滚也只改一行。
配置里还要有灰度开关。新版本先跑一条线路,跑三天没问题再全量。出问题把开关关掉,回到旧版本,业务不中断。
这套映射放在搭贝的表单规则里,换承运商只改配置不动代码。
下面是我们现在用的适配配置,YAML 格式。左边是承运商要的字段名,右边是内部字段名,中间带必填、长度、格式校验。缺字段直接拒单,超长截断并记日志,超时进重试队列。
# 面单出口适配:内部字段映射到承运商新版面单字段
version: waybill-v3
carrier: express-a
path: /open/waybill/create # 只写相对路径,域名在环境变量里
timeout_ms: 4000
retry:
times: 2
backoff_ms: 800
mapping:
orderNo:
from: order_no
required: true
max_len: 32
receiver:
name:
from: consignee_name
required: true
max_len: 20
mobile:
from: consignee_mobile
required: true
pattern: '^[0-9]{11}$'
province: {from: addr_province, required: true, max_len: 20}
city: {from: addr_city, required: true, max_len: 20}
district: {from: addr_district, required: true, max_len: 20}
street: {from: addr_street, required: false, max_len: 30}
detail: {from: addr_detail, required: true, max_len: 60}
sender:
name: {from: shipper_name, required: true, max_len: 20}
mobile: {from: shipper_mobile, required: true, pattern: '^[0-9]{11}$'}
package:
weightGram:
from: gross_weight_g
required: true
min: 1
transform: round_int
count:
from: pkg_count
required: true
default: 1
on_error:
missing_field: reject
over_length: truncate_and_log
timeout: queue_retry配置写完之后,映射的正确性可以离线验。拿一万条历史订单跑一遍,看有多少条会被拒、多少条会被截断。这个数字在上线前就该知道,不要等网点退单才发现。
新旧接口的字段列成一张表,一行一个字段,四列:旧字段名、新字段名、是否必填、转换规则。这张表由业务和开发一起过,业务确认含义,开发确认格式。
比对的时候会发现一些同名不同义的情况。旧接口的"件数"指包裹数,新接口的"件数"指内件数,一个包裹里装三件货,两个数不一样。这种差异不逐条比对是发现不了的。
接口返回成功不代表面单能打。要用真实打印机打五十张,拿扫码枪扫一遍,看能不能扫出运单号,看地址有没有缺段,看条形码有没有糊。
我们那次打了三十张,扫出来七张识别失败,原因是条码区域留白不够。这个在接口返回里完全看不出来,只有实打才知道。
实打还要分两种纸。热敏纸和铜版纸的打印浓度不一样,同一张面单在两种纸上扫出来的结果可能不同。仓库用哪种纸,就用哪种纸测,不要拿办公室的打印机凑数。
面单打出来只是开始。承运商揽收之后会回传轨迹,轨迹里的单号要能和我们的订单号对上。换代的时候如果单号规则变了,回传就对不上,客服查不到单。
对账方法是抽一天的单,把回传的轨迹条数和发出的面单条数比一遍,差异逐条查。差一条都要查清楚,否则积到月底就是几百条。
每次调用的请求报文和响应报文都要存,至少存三个月。出问题的时候,头一件事是看当时发了什么、回了什么,没有报文就只能猜。
存报文要注意脱敏。手机号、详细地址要打码,但不能全打,保留后四位,查问题的时候还能对上人。
字段字典写清楚每个字段的含义、格式、取值范围、谁维护。它不只是给开发看的,客服查单、仓库改地址都要用。
字典要和配置放在一起,改配置的时候顺手改字典。两处不一致,过半年就没人说得清哪个是对的。
字典里还要记下每个字段是哪个环节填的。地址四段是下单人填还是客服补,手机号是客户自己填还是坐席录,写清楚之后,数据出错时知道去找谁,不用在三个部门之间来回问。
新旧接口并行至少要留两周。这两周里旧接口保留可用,开关一拨就能回去。回退路径要真的演练一次,不能只写在文档里。
我们演练了一次回退,花了十一分钟。这十一分钟里下单暂停,比想象中久,因为旧接口的证书快到期了。演练之后把证书续了,也把回退步骤从七步压到三步。
换代之前先问对方要测试环境。有的承运商提供沙箱,可以在里面随便打单,不产生实际运单。有的只给文档,那就只能自己搭一个假接口,按文档返回固定报文。
沙箱能省掉大量联调时间。我们这次没有沙箱,靠真实下单试,试废了二十多张面单,费用自己承担。
问:地址拆不开怎么办,历史数据全是长字符串。
答:两条路。一条是做一次地址解析,按行政区划词典从字符串里匹配省市区,匹配不上的进人工队列;另一条是让下单端从源头改成四段下拉,历史数据不回溯,只保证新单结构化。两条路可以并行,新单先规范,老单慢慢补。不要指望用一个正则把所有地址都拆对,地址的写法比想象中乱。
问:重量单位换算放在映射层还是放在下单时?
答:放在映射层。内部统一用克存,展示的时候按业务习惯换成千克。存克的原因是整数不丢精度,也不会有浮点尾差。映射层只做格式转换,不做业务判断,业务判断放在下单校验里,职责分清之后好排查。
问:接口超时重试会不会重复下单?
答:会,如果没有幂等键。每次请求带一个由订单号加时间片生成的请求号,承运商侧按请求号去重,重复请求返回同一张面单。我们自己也要存一份请求号和运单号的对应关系,重试之前先查这张表,已经出单的直接取号,不再调用。
问:新接口返回成功,但面单内容是错的,怎么提前发现?
答:做内容校验,不只看返回码。把返回报文里的收件人、地址、重量取出来,和请求报文逐字段比对,不一致就告警。再往前一步,把面单图片的识别结果和订单比对,这一层成本高,适合单量大的场景。两层都做,基本不会出现打错单发出去的情况。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。