做危废物联网对接,最难的部分通常落在现场那台称重终端上,而不是省平台接口文档本身。终端在磅房里,网络时有时无,操作员一天要过几十车,中间还要换人换班。接口文档写的是理想路径,现场跑的是另一条路。
这篇记三个真实踩过的坑,都是对接后一两个月才暴出来的东西:断网期间的数据怎么补、字段映射在哪一层做、事件回调被谁吃掉。
绝大多数终端对接的第一版都是这样写的:HTTP 请求失败,重试 3 次,还失败就记日志丢弃。这个写法在办公室测试没问题,到了磅房一周就会丢数据,因为现场断网经常不是几秒,而是几个小时——工地临时断电、光缆被挖、运营商基站维护。
正确的结构是本地先落盘、再异步上传,上传成功才置位,不成功就留着等下一轮。
数据表的关键不是「有没有上传」,而是三个状态字段:
CREATE TABLE weigh_record (
record_id TEXT PRIMARY KEY, -- 端侧生成,全局唯一,见下节
plate_no TEXT, -- 车牌
gross_weight REAL, -- 毛重 kg
tare_weight REAL, -- 皮重 kg
net_weight REAL, -- 净重 kg
weigh_time TEXT, -- 称重时刻 ISO8601 带时区
sync_state INTEGER DEFAULT 0, -- 0待传 1已传 2平台已确认 3人工处理
retry_count INTEGER DEFAULT 0,
last_error TEXT, -- 最后一次失败原因,别只存错误码
created_at TEXT
);sync_state 这里要分成「已传」和「平台已确认」两级,这是补传逻辑能不能收敛的分水岭。只记「已传」的话,请求超时但你不知道平台收没收到,重传就可能重复入库;记到「平台已确认」,才敢把这条从待传队列里拿掉。
补传的调度也不用太聪明,一个简单规则就够:
sync_state = 0 且 retry_count < 5 的记录,按 weigh_time 升序批量提交,单批不超过 50 条;retry_count 到 5 之后不要继续死磕,转为 sync_state = 3 并在终端界面给一条显式提示,让人看得见。闷着不报的队列,最后一定变成「数据怎么少了一车」的扯皮。补传还要注意一件事:不要让补传把实时数据挤掉。现场常见做法是一条队列走所有请求,断网两小时后恢复,几百条补传排在前面,新称的一车要等十几分钟才上传。按 weigh_time 判断新鲜度、实时记录优先插队,能避免这个问题。
对接第二个坑是字段映射。第一版往往是这样:拿平台的接口文档,照着字段一个个 if 过去,代码里到处是 if plate is None: plate = driver_name 这类兜底。
问题在于,同一个概念在不同平台、不同版本里叫法不一样:
业务概念 | 省平台常见字段 | 企业自有系统常见字段 | 现场读卡器常见字段 |
|---|---|---|---|
车牌号 |
|
|
|
毛重 |
|
|
|
皮重 |
|
|
|
净重 |
|
|
|
称重时间 |
|
|
|
废物代码 |
|
| 不出现,靠人工选 |
联单号 |
|
| 不出现 |
把这些差异写在业务逻辑里,改一次平台就要动一次代码,而且改不干净——总有一处漏掉,线上表现为某个字段偶尔为空,查起来要翻日志。
更稳的做法是单独一层映射配置,业务代码只认内部标准模型:
FIELD_MAP = {
"plate_no": ["plateNo", "carNumber", "vehicle_no", "plate"],
"gross_weight": ["grossWeight", "gross_wt", "w1"],
"tare_weight": ["tareWeight", "tare_wt", "w2"],
"weigh_time": ["weighTime", "weigh_time", "timestamp"],
}
def pick(raw: dict, keys: list[str]):
for k in keys:
v = raw.get(k)
if v not in (None, "", 0):
return v
return None这个写法有两个必须注意的点,都是踩过才记住的:
一是 0 和空要分开判。 上面 v not in (None, "", 0) 里把 0 当空值,对重量字段是错的——皮重为 0 是合法值(空车过磅),不能因为它是 0 就去取下一个候选字段。重量类字段只判 None 和 "",数字类字段单独处理。
二是时间戳单位不统一。 毫秒和秒混用是最常见的低级错误,13 位当 10 位解析会得到一个 1970 年附近的时间,或者一个几万年后的时间。落到库里之前统一转成本地时间字符串并带上时区,不要在不同层反复转。
映射层还要有一个「命中率统计」。上线后跑一周,按字段统计 pick() 返回 None 的比例,哪个字段的空值率高,说明那个平台的字段名还没穷举完。这比等业务方来报「数据不对」快得多。
第三个坑是回调。称重终端通常要接收两类事件:平台下发的指令(比如要求补录、要求重新上传凭证),以及平台对已上传数据的确认。
回调翻车主要出在两个地方。
第一是幂等。 平台的确认回调可能重复推送——网络抖动、平台侧重试、你这边响应慢导致对方认为失败。如果确认逻辑写的是「收到确认就把本地记录状态改掉」,重复推送通常无害;但如果写的是「收到确认就往统计表加一条」,重复推送就会把数量做多。
幂等的键不能用本地自增 id,必须用端侧生成的 record_id。生成方式建议是「设备编号 + 称重时刻 + 序号」,保证断网重连、数据库重建之后还能算出同一个值,而不是依赖 UUID 之外的状态:
def make_record_id(device_sn: str, weigh_time: datetime, seq: int) -> str:
return f"{device_sn}-{weigh_time:%Y%m%d%H%M%S}-{seq:04d}"第二是回调的验签与超时。 回调接口收到请求后应该先做两件事:验签、落一条原始报文。不要在校验通过之后才存原始报文,现场排查九成情况要看「对方到底发的是什么」,报文丢了就只能靠猜。
另外,回调处理必须快速返回。常见做法是接住请求、写入消息队列、立即返回成功,真正的处理异步做。如果在回调里同步查库、算净重、调另一个接口,一旦某个环节慢,平台侧超时重试就会带来重复推送——然后你又回到幂等那个问题上。
一个容易忽略的细节:回调地址如果是内网穿透或反向代理,务必确认原始请求头没有被改写。 验签通常依赖原始 body 和特定 header,中间件做了一次 JSON 重排(比如把 "a":1 变成 "a": 1),验签就会一直失败,而日志上看起来一切正常——请求到了、返回 200、就是验不过。
不用等出问题再补。
断网补传的本质是把「一次请求」改成「一个有状态、可重放的队列」;字段映射的本质是把平台差异挡在一层配置里,不让它渗进业务代码;事件回调的本质是把「收到就算完成」改成「幂等 + 可追溯」。
这三件事都不复杂,但都需要在架构上留出位置。留了位置,后面接第三个、第四个平台时会轻松很多;没留位置,每接一个平台就是一次重写。
本文由河南白泽环保科技有限公司技术团队整理,内容为危废物联网称重终端对接的工程实践记录,不含具体项目数据与客户信息。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。