智能货柜:柜格状态机与并发开柜控制
扫码开柜看起来简单,真正难的是并发、超时与硬件回执不一致。
货柜自主存取的主链路是:扫码 → 分配柜格 → 下发开柜 → 等待回执 → 关柜确认。任何一步丢消息,都会变成「钱扣了门没开」或「门开了状态没更新」。我在 .NET 后端里用显式状态机建模每个柜格,而不是在业务代码里散落 if-else——这是后续对账和运维介入的基础。
用状态机锁住真相
每个柜格维护明确状态:空闲 / 预占 / 开柜中 / 占用 / 故障。业务只允许合法迁移,禁止「跳步改状态」。状态变更必须写流水表,带上操作人、指令 ID、时间戳,方便和支付、取货记录对齐。
预占与过期
预占要带过期时间(常见 2–5 分钟):用户扫码后不去取,必须自动释放,否则库存柜格会被占死。过期任务用定时 Job 或延迟队列触发,释放前再查一次当前状态,避免和用户刚好开柜的动作打架。
并发开柜
同一柜格同一时刻只允许一个进行中的开柜指令。用分布式锁或数据库行锁保证:
- 下发前二次确认柜格仍可用
- 硬件超时未回执 → 进入「待确认」,而不是直接标失败或成功
- 运维可人工核验现场后闭环,补写回执或标记故障
典型坑
- 只锁 Redis 不锁 DB:进程崩溃时锁过期,两个指令同时下发
- 超时后直接标失败并退款:门其实开了,造成资损
- 用「最后更新时间」覆盖状态:回执乱序时会把「已占用」写回「空闲」
和 MQTT 的配合
指令走 MQTT,业务库记流水。以业务流水为准,以硬件回执纠偏。每条指令带全局唯一的 commandId,设备端和云端各维护去重集合。回执乱序时,靠指令 ID 对齐,而不是「最后一条覆盖一切」。
关柜确认同样要走状态机:只有收到「门已关」且传感器一致,才从「开柜中」迁到「占用」或「空闲」。
支付与取货对齐
扫码链路往往和支付回调并行到达。我的习惯是:支付成功只产生「待取货」订单,真正占用柜格发生在分配成功之后。若支付成功但分配失败,走自动退款或人工工单,而不是强行开柜。取货完成后,状态从「占用」回到「空闲」,并触发清洁或消杀流程(若业务需要),避免下一用户打开仍显示上一单信息。
后台运营需要的能力
运营后台要能按柜机、柜格、时间段筛选「待确认」与「故障」列表,支持远程禁用单格而不下线整机。批量补货场景下,运维开门应走独立指令类型,与普通用户开柜共用状态机但权限与审计字段不同,防止和线上订单抢锁。
运维与对账检查清单
- [ ] 任意柜格可查询完整状态迁移历史
- [ ] 「待确认」态有告警与人工处理入口
- [ ] 支付成功与开柜成功可逐笔勾对
- [ ] 故障柜格不参与自动分配
- [ ] 幂等接口:同一扫码请求重复提交不重复开柜
货柜系统稳不稳,取决于你是否承认:硬件世界永远比 API 更脏。状态机不是文档装饰,而是和 MQTT、支付、运维三条线之间的契约。
上线前我会用故障注入跑一轮:模拟回执延迟、重复回执、支付回调重复、MQTT 断连。能通过这类演练的系统,现场投诉率会明显下降。
小结
货柜业务必须承认硬件回执会脏、会慢、会丢。状态机、预占过期、指令幂等和人工核验入口,是把「偶发事故」变成「可闭环工单」的基础。压测时重点打并发开柜与超时未回执两条路径。