
做仓库管理系统这几年我一直有个体会WMS 本身不难难的是它周围的设备。以前很多仓储项目到货之后人工扫码、叉车搬运、纸质单据系统上线只是把 Excel 换成了网页现在不一样了客户一开口就是“我要上 RFID 通道机”“AGV 能不能直接接进 WMS 里”。需求越来越往设备层走但真正接触过 RFID 读写器和 AGV 调度系统的 Java 开发其实不多——这恰好是 JeeWMS 这类开源项目最有价值的地方它把一个完整的仓库业务骨架摆在面前你可以在上面把货位管理、出入库流程、库存账目先跑通再去接硬件逐步把设备层打通。这篇内容就是围绕 JeeWMS 开源仓库管理系统展开的 RFID 与 AGV 的 IoT 集成实战记录重点讲三件事WMS 的业务模块怎么理解RFID 和 AGV 两类核心设备怎么接入 Java 后端以及接入过程中那些文档里不会写的坑。适合正在做 WMS 选型或二次开发的人、做仓储自动化的实施工程师、以及想用真实项目练习 Java 技术栈的开发者参考。1. 内容整体设计与思路拆解1.1 JeeWMS 的模块结构与业务分层JeeWMS 是典型的 Java 技术栈开源 WMS后端基于 Spring Boot MyBatis数据库用 MySQL 和 Redis前端有独立的管理界面。它的核心价值在于业务模块足够完整入库管理、出库管理、库存管理、盘点管理、波次策略、容器管理、基础资料等全都覆盖了。很多制造业客户问“WMS 各个模块怎么分”直接拿 JeeWMS 的菜单结构讲就可以了基础资料层仓库、库区、货位、物料、供应商、客户、条码规则。作业执行层入库单、上架任务、出库单、拣货任务、复核打包、装车发运。库存管理层实时库存、冻结库存、库存流水、批次与序列号追溯。策略配置层上架策略、分配策略、波次策略、补货策略。这套分层方式不是 JeeWMS 独有的而是行业通用模型。理解它很关键因为后面接 RFID 和 AGV本质上都是在给“作业执行层”喂数据和收结果。比如 RFID 通道机读到的入库托盘最终要生成一张入库单并触发上架任务AGV 把货从收发货区运到货架区WMS 要能下发搬运指令并接收完成回传。1.2 为什么选 JeeWMS 做 IoT 集成而不是商业 WMS商业 WMS 比如富勒、SAP EWM 这些功能确实深但问题也很明显设备接口是黑盒想要自定义数据格式、想要在关键节点插入自己的逻辑往往要通过对方的实施团队走工单一条需求排期几周甚至几个月。开源系统在这方面的优势是“代码即文档”——你可以直接读源码确认它如何处理库存占用、如何生成任务再针对设备接入做改造。JeeWMS 的代码结构对二次开发比较友好一个典型的优点是它将“单据”和“任务”分开。入库单是一个头 明细的结构真正执行时由系统生成上架任务分配给操作员或设备这种设计跟 RFID、AGV 的任务模型天然匹配RFID 读到实物就是“单据完成”AGV 搬运就是“任务执行”。所以我在实际项目里更愿意在这种开源的 Java WMS 上做设备层集成而不是在闭源系统里绕来绕去。1.3 设备层接入的整体架构思路把 JeeWMS、RFID、AGV 三者放在一起看我习惯把它拆成三层业务层JeeWMS 负责库存账、单据流、策略决策。接入层一个独立的设备中间件服务负责跟 RFID 读写器、AGV 调度系统通信将硬件事件转成业务事件。设备层RFID 读写器、天线、AGV 小车、充电桩、呼叫按钮等物理设备。这里要特别强调接入层的独立性。我第一次做的时候图省事直接把 RFID 的 SDK 调到了 JeeWMS 的业务代码里结果换一个品牌的读写器就要改动业务代码AGV 调度接口调整也直接影响单据流程。后来重构把所有硬件通信收敛到一个中间件服务WMS 只通过 HTTP 接口或消息队列与中间件交互设备再怎么变业务层都是稳的。这也符合 IoT 集成的基本理念设备千差万别但业务模型是稳定的中间必须有一层做协议转换。1.4 技术选型的几个取舍RFID 这边高频 HF 和超高频 UHF 是两种常见选择。仓内托盘、料箱追踪一般用超高频读取距离远、批量识别能力强门禁、工位识别用高频更稳。硬件通信上主流读写器都会提供串口和网络两种接口网络接口TCP/UDP更适合接入后端服务。AGV 这边市面上的调度系统五花八门有商业的也有开源的 OpenTCS还有些厂家用自己的调度平台。做 WMS 集成时不用纠结调度算法本身WMS 只需要回答三个问题该搬什么、从哪里搬、搬到哪里。路径规划、避障、车辆分配这些交给 AGV 调度系统处理。WMS 与调度系统之间通常走 Rest API 或消息任务状态通过回调通知。这样设计下来各层职责清晰后面不管是扩展机械臂还是接入电子标签都不会伤筋动骨。2. 核心细节解析与实操要点2.1 RFID 集成的工作原理与标签数据设计RFID 的工作原理并不复杂读写器通过天线发射射频信号标签进入磁场后获得能量将芯片内存储的数据返回给读写器。但在 WMS 场景里真正要设计的是“标签里存什么”。我见过很多项目的失败不是硬件不行而是数据设计一塌糊涂。标签裸奔只存一个 EPC 编码数据库里没有关键词对应关系结果每次读到的标签都不知道对应什么物料。我的建议是至少规划三个数据区EPC 区全球唯一的标签 ID相当于是标签的身份证一般由厂商写入。TID 区芯片本身的永久标识不可修改用于防伪校验。用户数据区可以自己定义格式比如写入“物料编码 批次号”或者“库位编码 托盘号”。在 JeeWMS 场景里我的做法是托盘标签的用户区存托盘号货品通过数据库绑定。RFID 读到托盘号再去 WMS 查这个托盘上放了什么料、哪个批次、数量多少。这样标签简单业务灵活不会因为写错数据导致整个托盘报废。2.2 RFID 读写器的通信与数据上报格式设备联网是第二步。以某款超高频读写器为例它支持 TCP 客户端模式读写器主动连接我们的中间件服务的某个端口读到的标签数据以固定报文格式上报。典型的上报报文大致是这样的结构{ deviceCode: RFID-GATE-01, antenna: 1, epc: E2003411010101234567, tid: A1B2C3D4E5F6, rssi: -42, timestamp: 2024-11-20 14:30:00 }这里面天线编号很关键因为它能告诉我们标签是从哪个位置读到的比如 1 号天线在入库口2 号天线在出库口两个天线读到同一个 EPC 就代表货品穿过了通道机。RSSI 表示信号强度可以用来辅助判断标签位置和读取质量。中间件要做的就是把这样一条原始事件翻译成业务事件标签 E2003411... 出现在入库口。这里有一个实操要点批量读取时会出现大量重复上报同一个标签一秒内可能被读到十几次中间件必须做去重和滤波。我的处理方式是用一个内存窗口同一个 EPC 在 2~3 秒内只保留第一条并通过 Redis 设置分布式窗口这样多台读写器同时上报也不会导致业务重复触发。2.3 AGV 集成中的任务模型设计AGV 的对接比 RFID 稍复杂因为它涉及任务生命周期管理。WMS 不能只是把搬运指令丢给调度系统然后就不管了必须跟踪任务从“创建”到“完成”的整个状态变化。我在 JeeWMS 里的实现方式是在数据库里保留一张搬运任务表字段包括任务号、源库位、目标库位、优先级、状态、车辆编号、创建时间和完成时间。状态机的流转如下已创建WMS 生成搬运任务落库。已下发中间件将任务推送给 AGV 调度系统。执行中AGV 接到任务开始搬运。完成AGV 到达目标库位回传完成消息。异常取消AGV 无法执行或人为取消。这张表是 WMS 和 AGV 之间的“账本”两边系统状态不一致时以这张表为准做对账。很多 AGV 项目出错都是因为两边状态不同步WMS 认为任务完成了AGV 那边其实还在排队或者 AGV 已经搬完了WMS 还挂着“执行中”不释放库位。有了这张任务表和定时对账任务大部分问题都能被发现和修正。还有一个容易被忽略的点任务下发的接口必须保证幂等。调度系统可能因为网络超时而重试如果同一个任务被重复下发就可能导致两辆 AGV 去搬同一个托盘。解决办法很简单每次下发带上 WMS 的任务号调度系统按任务号去重重复请求直接返回原任务状态。2.4 AGV 调度算法与路径规划的基础认知虽然 WMS 不需要实现调度算法但作为集成方至少要理解 AGV 调度系统内部在做什么不然出了问题都不知道该找谁。AGV 调度最基础的是路径规划经典方法就是 A* 算法。把仓库地面划分成网格每一格是节点AGV 从起点到终点通过启发式搜索找到最短路径。三条 AGV 同时工作的时候需要考虑的是避碰策略时间窗法、交通管制法、优先级抢占法都常见。OpenTCS 这类开源调度系统内置了路径规划和车辆分配能力适合做原型验证和小规模项目但大型仓储里几十台车同时跑对调度算法和消息实时性的要求很高这时候通常需要选用商用调度平台或基于 OpenTCS 做深度二次开发。对 WMS 集成来说你只需要关注对外接口是否完善、任务回调是否及时、任务取消是否可靠这三个点。实测下来很多 AGV 厂家的调度系统对外接口做得并不标准尤其回调通知这块稍不注意就会丢消息所以 WMS 侧的轮询对账机制是必须保底的。2.5 接口安全与异常兜底机制设备接入层处于内网环境很多项目图省事接口不做认证直接裸奔。我的建议是至少加一层简单的 Token 校验即使在内网也要防止其他设备误调用接口产生脏数据。异常兜底机制更重要。比如 RFID 通道机偶尔会读到隔壁工位的标签AGV 任务下发后可能 10 分钟没有回执。这些问题如果不处理整个仓库账目就会越来越不准。我在中间件里加了三个兜底标签过滤根据天线位置和读取功率设置白名单过滤非本工位标签。任务超时扫描定时扫描搬运任务表超过设定时间未完成的任务转人工介入。数据对账每天凌晨对 WMS 库存和 AGV 任务记录做一致性校验自动生成差异清单。这三条看着不起眼但实际项目里八成以上的设备集成问题最后都靠兜底机制兜住了。3. 实操过程与核心环节实现3.1 环境准备与基础服务安装开始之前需要准备好基础环境JDK、MySQL、Redis、JeeWMS 源码包。我用的是 JDK 1.8 MySQL 5.7 Redis 5.x 的组合这是很多 Java 项目的稳定搭配。# 以 CentOS 7 为例安装基础服务 yum install -y java-1.8.0-openjdk systemctl start mysqld systemctl start redisJeeWMS 源码导入 IDE 后先改配置文件里的数据库连接和 Redis 配置spring: datasource: url: jdbc:mysql://localhost:3306/jeewms?useUnicodetruecharacterEncodingUTF-8 username: root password: yourpassword redis: host: 127.0.0.1 port: 6379启动后能正常登录系统、能维护基础资料和建单说明基础环境没问题接下来可以开始接设备。3.2 RFID 读写器对接的实操步骤RFID 读写器到 Java 中间件最稳妥的方式是走网络 Socket。读写器配置为 TCP 客户端主动连接中间件开放的 9001 端口。中间件用 Netty 做通信框架处理长连接、断线重连和数据拆包。服务端接收数据的关键代码如下public class RfidServerInitializer extends ChannelInitializerSocketChannel { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new RfidMessageHandler()); } }这里 LineBasedFrameDecoder 是按行拆分报文但实际项目中很多读写器的报文是定长或带帧头帧尾的需要根据设备协议自定义解码器。我第一次对接时贪快用了最简单的按行解码结果设备偶尔输出两段报文粘在一起解析直接乱掉后来改成按帧头帧尾截取问题消失。设备上报数据经过解码、解析、去重后写入 Redis 队列再由业务消费者转换成 JeeWMS 的入库事件。这里用 Redis 队列做缓冲非常有必要因为硬件上报频率往往高于业务处理速度直接同步调用会导致读写器缓冲区溢出。3.3 RFID 与 JeeWMS 入库流程的打通当托盘经过入库口通道机RFID 读到托盘标签后需要触发 JeeWMS 的收货入库流程。我采用调用 JeeWMS 内部 Service 的方式完成而不是直接操作数据库——这是关键点直接改表很容易绕过业务校验逻辑导致库存记录不一致。public String handleRfidInbound(String epc, String antennaNo) { // 1. 通过EPC获取托盘号 String palletNo rfidTagService.getPalletNoByEpc(epc); // 2. 获取托盘上绑定的物料与数量 PalletInfo palletInfo palletQueryService.getPalletInfo(palletNo); // 3. 调用JeeWMS的入库单接口 InboundOrderDTO dto new InboundOrderDTO(); dto.setWarehouseCode(WH01); dto.setPalletNo(palletNo); dto.setMaterialCode(palletInfo.getMaterialCode()); dto.setQty(palletInfo.getQty()); return inboundOrderService.createByRfid(dto); }这里我用的是自定义的 createByRfid 方法而不是标准的入库接口原因是标准入库流程往往要求扫码确认、数量校验等人工环节而 RFID 通道机自动入库应该走简化的流程读到即收货异常标签进异常队列。实际投入使用后要注意RFID 自动入库模式对基础数据的准确性要求极高如果物料编码没维护、库位没设置会导致大量异常单积压一定要配一个异常处理界面让仓管员能快速处理“读到了但不知道该放哪”的托盘。3.4 AGV 搬运任务下发的实现AGV 对接的实操从创建搬运任务开始。在 JeeWMS 的出库流程中当拣货完成生成待搬运的货物后系统自动创建 AGV 搬运任务并把任务推送出去。推送调用示例public boolean dispatchAgvTask(AgvTask task) { String url agvConfig.getDispatchUrl(); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(X-Auth-Token, agvConfig.getToken()); JSONObject body new JSONObject(); body.put(taskNo, task.getTaskNo()); body.put(sourceLocation, task.getSourceLocation()); body.put(targetLocation, task.getTargetLocation()); body.put(priority, task.getPriority()); try { ResponseEntityString resp restTemplate.postForEntity(url, new HttpEntity(body.toString(), headers), String.class); if (resp.getStatusCode().is2xxSuccessful()) { taskService.markDispatched(task.getTaskNo()); return true; } } catch (Exception e) { log.error(AGV下发失败, taskNo{}, task.getTaskNo(), e); taskService.markDispatchFailed(task.getTaskNo()); } return false; }注意这里 restTemplate 要配置连接超时和读取超时不能默认无限等待。AGV 调度系统如果响应慢很可能拖垮 WMS 的线程池。我用的是连接超时 3 秒、读取超时 10 秒失败后进入重试队列。这种重试一定要做成异步的不要在主线程里原地循环。我用 Spring 的 Async 或消息队列做延迟重试第一次 10 秒后重试第二次 1 分钟后重试超过三次标记失败转人工。3.5 AGV 任务状态回调与库存联动AGV 完成搬运后调度系统会回调 WMS 接口通知结果。我的接口设计如下PostMapping(/api/agv/callback) public Result agvCallback(RequestBody AgvCallbackDTO callback) { String taskNo callback.getTaskNo(); String status callback.getStatus(); if (COMPLETED.equals(status)) { // 1. 更新搬运任务状态 agvTaskService.markCompleted(taskNo); // 2. 释放源库位占用目标库位 inventoryService.relocateByTask(taskNo); // 3. 更新JeeWMS库存流水 stockRecordService.recordMove(taskNo); } else if (FAILED.equals(status)) { agvTaskService.markFailed(taskNo, callback.getReason()); } return Result.ok(); }这里最关键的是第 2 步释放源库位、占用目标库位。如果这里做漏了WMS 里的库存会显示货品还在原地但实际已经被 AGV 搬走盘点的时候差异会大到让人崩溃。关于回调接口的幂等性我需要再强调一遍AGV 调度系统因为网络原因可能会重复回调所以在标记完成之前必须判断任务当前状态。如果已经是完成态直接返回成功不再处理库存逻辑。不然同一次搬运可能生成两条库存流水账目就会差出两倍的数量。4. 常见问题与排查技巧实录4.1 RFID 数据连接错误与断连排查“RFID 数据连接错误”是出现频率最高的问题几乎每个新项目都会遇到。根据我手里的现场记录90% 的原因不是读写器坏了而是连接参数不对或者网络环境不稳。常见的原因和对策如下表现象可能原因排查方法读写器连不上服务端端口被防火墙拦截检查防火墙规则telnet 测试端口连通性连上后频繁断线网络不稳定或设备配置了休眠用网线直连测试调整设备保活参数能连接但收不到数据天线未开启或标签不在读取范围检查天线开关和标签位置观察设备日志数据乱码或解析失败报文格式不匹配对照设备协议文档检查帧头帧尾和编码格式偶尔读到相邻工位标签天线功率过大调低读写器功率增大天线屏蔽这里我特别想提天线功率的调整。UHF 读写器的功率不是越大越好功率过高会穿透隔板读到相邻区域的标签造成为数不准的脏数据。现场调试时拿一个测试标签从远到近慢慢移动找到临界读取距离再把功率留 20% 余量这样既能保证读取稳定又不会干扰隔壁工位。4.2 盘点差异大RFID 批量读取漏读误读RFID 盘点时发现账实不符是另一个高频问题。这里要分清漏读还是误读漏读的原因通常是大量标签密集堆叠天线信号被遮挡尤其金属货架对超高频 RFID 的反射吸收非常明显。这种情况下调整天线角度、增加天线数量、让托盘在通道机中停留更长时间都能明显改善读取率。误读则来自相邻区域标签串扰解决思路是缩小读取范围而不是单纯调小功率。可以在通道机四周加装屏蔽材料或者在读写器上启用“读取区域限定”功能。如果盘点仍然有少量差异我建议不要追求 100% 识别率而是在系统里设置一个“盘点复核”机制RFID 先扫出 95%剩下 5% 由人工手持终端复核。这个比例在实际项目中是可以接受的追求完美的识别导致调试成本成倍增加并不划算。4.3 AGV 任务状态不一致与回调丢失AGV 任务状态不一致的典型场景WMS 状态是执行中AGV 实际已经完成了搬运甚至已经搬完下一趟了。这种问题需要从机制上解决。我采取的方案是除了回调通知再增加一个定时对账任务每 2 分钟查询一次 AGV 调度系统的任务列表与本地搬运任务表比对发现状态不一致就自动修正。Scheduled(cron 0 */2 * * * ?) public void reconcileAgvTask() { ListAgvTask executingTasks agvTaskService.listExecuting(); for (AgvTask task : executingTasks) { AgvRemoteStatus remote agvClient.queryRemoteStatus(task.getTaskNo()); if (COMPLETED.equals(remote.getStatus()) !COMPLETED.equals(task.getStatus())) { agvTaskService.markCompleted(task.getTaskNo()); inventoryService.relocateByTask(task.getTaskNo()); } } }回调丢失是现实存在的尤其调度系统团队只保证了接口可用并没有做消息可靠性设计的时候。对账任务虽然做法笨但非常可靠我至今没有找到一个替代方案能在低成本的前提下保证两边状态绝对统一。4.4 AGV 路径冲突与死锁处理多台 AGV 同时运行时路径冲突很难完全避免。最常见的情况是两辆车在交叉口互相等待形成死锁。对于集成方来说能做的是把问题留给调度系统但在 WMS 侧做好任务下发策略一次不要下太多任务保证在途任务数在一个合理区间。同一条路径上的任务错峰下发避免多台车同时靠近同一个狭窄区域。为任务设置超时时间超时后主动取消并重新规划。从实际运行数据看把并发在途任务数控制在 5~8 个、同一个目的库位同时只分配一辆车死锁概率会大幅下降。此外AGV 调度参数的调整需要循序渐进一次只改一个参数并观察半天以上运行数据不要急着同时调多个参数出了问题根本定位不到原因。4.5 JeeWMS 性能瓶颈与并发优化设备接入后JeeWMS 的并发压力会比纯人工操作大很多。RFID 通道机一分钟可以连续读几十个托盘AGV 回调也可能几百毫秒内连续进来几个数据库连接池不够用、接口响应变慢是常见问题。优先检查三处配置数据库连接池Druid 的初始连接数和最大连接数开大一些比如初始 20、最大 200。Redis 连接池设备事件都走 Redis连接池够不够直接影响吞吐。业务线程池回调处理、任务下发如果是异步的线程池大小要单独配置不要用默认值。另外一个容易被忽视的点是 MyBatis 的慢 SQL。设备接入后库存查询接口会被高频调用如果没有给关键表加上合适的索引一次查询几百毫秒累计起来就会拖垮整个系统。上线前用慢 SQL 日志和 EXPLAIN 分析一遍主要查询该加索引的加上成本很低但效果立竿见影。最后再说一个我从项目里沉淀下来的体会设备集成的调试过程很磨人RFID 天线角度差一点、AGV 调度策略调一个参数都可能影响到整个仓库的运行效率。刚开始做的时候总想把所有问题都在代码层面解决后来发现设备的问题一部分靠代码兜底另一部分必须在现场一点点调。好的 WMS 设备集成不是靠某个炫酷算法一劳永逸而是靠扎实的对账机制、清晰的任务状态机和务实的异常处理流程撑起来的。这些基础工作做到位了RFID 和 AGV 才能真正变成仓库里稳定运行的得力干将而不是上线后天天让人提心吊胆的定时炸弹。