简介:这份JAVA版WMS物流仓储管理系统源码面向第三方物流仓储企业与自营仓储场景,适合需要搭建或二次开发仓储信息化平台的开发者与实施团队。系统基于SpringMVC+Hibernate+Minidao+Easyui+Redis+Ehcache等技术栈构建,包含Web后台与Android PDA端,功能覆盖订单管理(OMS)、仓储管理(WMS)、计费管理(BMS)、现场作业(RF)及第三方接口模块,并已对接SAP ECC、SAP HANA、用友U8、百胜E3、UAS等系统,同时扩展了进销存与BOM模块。压缩包共约2000个文件,以js、png、java、css、jsp、gif等为主,涵盖前端页面、后端逻辑、样式资源与数据库脚本,整体约65.55MB。目前已有563人学习下载,适合研究仓储系统架构、接口对接与PDA作业流程的开发者参考借鉴。
1. 从一份 JAVA 版 WMS 源码说起:PDA 端和 Web 端到底怎么分工
很多做 Java 的同行第一次接触 WMS 物流仓储管理系统,都是被一份「JAVA 版 WMS 源码,包含 PDA 端和 Web 端」的压缩包勾起的兴趣。下载下来解压,看到一堆 Spring Boot 工程、几个前端目录、还有一套手持终端页面,第一反应往往是:这不就是个增删改查吗?真跑起来才发现,入库、上架、拣货、复核、发运这条链路里,Web 端和 PDA 端的分工完全不是「一个管后台一个管前台」这么简单。Web 端负责的是单据、策略、库存台账、报表这些「慢思考」的活;PDA 端负责的是扫码、确认、移库、盘点这些「快执行」的活,两者共享同一套库存模型,但交互节奏、并发压力、离线容忍度完全不同。这篇文章就顺着这份源码的技术骨架,把 WMS 的领域模型、PDA 与 Web 的接口边界、库存扣减的并发处理、以及部署踩坑讲清楚,适合想拿它做二次开发、或者想照着搭一套自己仓储系统的 Java 工程师。
2. WMS 的领域模型与两端职责拆解
2.1 先想清楚库存到底挂在哪一层
WMS 最容易翻车的地方,不是代码写得多烂,而是库存模型一开始就设计歪了。常见做法是把库存挂在「仓库 + 库位 + SKU + 批次」这个四元组上,而不是简单挂在 SKU 上。原因很直接:同一个 SKU 在不同库位、不同批次、不同效期下的可售状态是不一样的,PDA 扫码上架时如果只更新 SKU 总量,后面拣货就会找不到货。
在这份源码里,通常能看到几张核心表:wms_warehouse(仓库)、wms_location(库位)、wms_sku(商品)、wms_stock(库存快照)、wms_stock_log(库存流水)。库存快照表存的是当前余量,流水表存的是每一次变动的原因和方向。两者必须同时写,且要在同一个事务里,否则对账时就是一笔糊涂账。
-- 库存快照:一个库位一个 SKU 一条记录 CREATE TABLE wms_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_id BIGINT NOT NULL, location_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, batch_no VARCHAR(64) DEFAULT NULL, qty_available INT NOT NULL DEFAULT 0, -- 可用量 qty_locked INT NOT NULL DEFAULT 0, -- 锁定量(已被订单占用) version INT NOT NULL DEFAULT 0, -- 乐观锁版本 UNIQUE KEY uk_loc_sku_batch (location_id, sku_id, batch_no) ); -- 库存流水:只追加,不修改 CREATE TABLE wms_stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stock_id BIGINT NOT NULL, biz_type VARCHAR(32) NOT NULL, -- INBOUND/OUTBOUND/MOVE/CHECK biz_no VARCHAR(64) NOT NULL, -- 关联单据号 change_qty INT NOT NULL, -- 正数入库,负数出库 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );qty_available和qty_locked分开是关键。下单时先锁定量,实际出库时再扣减可用量并释放锁定量。如果只用一个字段,超卖和重复拣货几乎必然出现。version字段是给乐观锁用的,PDA 端多人同时操作同一个库位时,靠它兜底。
2.2 Web 端管策略,PDA 端管执行
Web 端和 PDA 端的职责边界,决定了接口怎么切。Web 端面向的是仓管员和计划员,操作频率低但逻辑复杂:入库单创建、上架策略配置、波次生成、库存调拨审批、报表导出。PDA 端面向的是一线操作工,操作频率高但逻辑简单:扫库位、扫商品、输入数量、确认提交。
所以接口设计上,Web 端可以走常规的 RESTful 分页查询和表单提交,PDA 端则要尽量做「一次请求完成一个动作」的粗粒度接口。比如上架这个动作,PDA 端不应该先查库位再查商品再提交,而是一个/pda/putaway/confirm接口,把库位码、商品码、数量一起传上去,服务端一次性完成校验、库存更新、流水记录。
@PostMapping("/pda/putaway/confirm") public Result<PutawayResp> confirmPutaway(@RequestBody @Valid PutawayReq req) { // 1. 校验库位是否存在且可用 Location loc = locationService.getByCode(req.getLocationCode()); if (loc == null || !loc.getEnabled()) { return Result.fail("库位不可用"); } // 2. 校验商品是否存在 Sku sku = skuService.getByCode(req.getSkuCode()); if (sku == null) { return Result.fail("商品不存在"); } // 3. 执行上架:更新库存 + 写流水,同一事务 stockService.putaway(loc.getId(), sku.getId(), req.getQty(), req.getBizNo()); return Result.ok(new PutawayResp(loc.getCode(), sku.getCode(), req.getQty())); }这个接口的入参只有三个业务字段加一个单据号,服务端把校验和写入包在一起。PDA 端拿到成功响应就播一声提示音,失败就显示原因,不需要理解库存模型。这种粗粒度设计的好处是,PDA 端的网络请求次数少,弱网环境下成功率更高。
2.3 单据状态机是两端同步的锚点
Web 端和 PDA 端要协同,靠的不是实时推送,而是单据状态机。一张入库单从「已创建」到「已收货」到「已上架」到「已完成」,每个状态变更都由某一端触发,另一端通过查询看到最新状态。这样即使 PDA 端离线几分钟,重新联网后拉一次单据状态就能对齐。
常见做法是在单据表上放一个status字段,配合一张wms_order_status_log记录每次流转。状态流转必须做前置校验,比如「已上架」不能直接跳到「已完成」,必须先经过「已复核」。这份源码里如果状态机写得随意,二次开发时最容易出的问题就是状态回退和数据不一致。
3. 把源码跑起来:环境、依赖与最小启动路径
3.1 环境准备与依赖版本对齐
拿到一份 Java WMS 源码,第一步不是急着改代码,而是先把环境对齐。这类项目通常依赖 JDK 8 或 JDK 11、MySQL 5.7/8.0、Redis、Maven。版本不对齐,启动时报的错往往和业务无关,全是依赖冲突。
# 检查 JDK 版本,WMS 源码多数基于 JDK 8 或 11 java -version # 检查 Maven 版本 mvn -v # 初始化数据库,先建库再导入脚本 mysql -uroot -p -e "CREATE DATABASE wms DEFAULT CHARSET utf8mb4;" mysql -uroot -p wms < doc/sql/wms_schema.sql mysql -uroot -p wms < doc/sql/wms_init_data.sql数据库脚本一般放在doc/sql或src/main/resources/sql下。导入顺序不能反,先建表再插初始数据。初始数据里通常包含默认仓库、默认管理员账号、基础库位,这些是登录和跑通流程的前提。如果导入时报外键约束错误,多半是字符集或引擎不一致,把建表语句里的ENGINE=InnoDB DEFAULT CHARSET=utf8mb4统一一遍。
3.2 配置文件里必须改的几项
源码里的application.yml或application-dev.yml通常带着作者本地的配置,直接启动大概率连不上数据库。需要改的核心项就几个:数据库连接、Redis 连接、文件上传路径、PDA 接口的鉴权开关。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/wms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 wms: pda: token-expire: 7200 # PDA 登录令牌有效期,单位秒 offline-allow: true # 是否允许离线补传 upload: path: /data/wms/upload # 附件和导入文件存放目录serverTimezone必须显式指定,否则 MySQL 8 下时间字段会差 8 小时,库存流水的时间就对不上。pda.token-expire别设太短,一线操作工登录一次要管一整个班次,频繁掉线会直接影响作业效率。offline-allow如果打开,PDA 端要配合本地缓存,服务端要能处理重复提交,这个后面避坑章节会细说。
3.3 启动顺序与最小验证
依赖服务要先起,再起应用。顺序是 MySQL → Redis → 后端应用 → Web 前端 → PDA 端页面。后端启动成功后,先用接口工具验证登录和基础查询,再打开前端。
# 后端打包启动 mvn clean package -DskipTests java -jar target/wms-admin.jar --spring.profiles.active=dev # 前端(如果是 Vue 或 React 工程) cd wms-web npm install npm run dev验证的最小路径是:用初始管理员账号登录 Web 端 → 创建一个入库单 → 用 PDA 端页面扫码上架 → 回 Web 端查库存。这条链路走通,说明数据库、缓存、两端接口都是通的。如果卡在某一步,先看后端日志里的 SQL 和异常栈,再看前端请求的响应体,基本能定位到是配置问题还是代码问题。
4. PDA 端与 Web 端的接口联调与数据一致性
4.1 PDA 端的鉴权与请求封装
PDA 端跑在手持终端上,屏幕小、网络不稳,鉴权不能照搬 Web 端的 Session 方案。常见做法是登录后发一个 token,PDA 端存在本地,每次请求带在 Header 里。token 过期时间要够长,同时服务端要支持 token 续期。
// PDA 端登录后签发 token public String login(String username, String password) { User user = userService.checkLogin(username, password); if (user == null) { throw new BizException("账号或密码错误"); } String token = UUID.randomUUID().toString().replace("-", ""); // 存 Redis,key 带前缀,value 存用户 ID 和仓库权限 redisTemplate.opsForValue().set( "wms:pda:token:" + token, user.getId() + ":" + user.getWarehouseId(), pdaTokenExpire, TimeUnit.SECONDS ); return token; }token 里绑定了用户 ID 和仓库 ID,后续每个 PDA 接口都从 token 里取仓库权限,避免越权操作别的仓库。pdaTokenExpire从配置读,别硬编码。如果 PDA 端支持「记住密码」,本地存的应该是加密后的凭证,不是明文密码。
4.2 库存扣减的并发处理
PDA 端最大的技术风险是并发。多个操作工同时扫同一个库位、同一批货,如果库存扣减没做好,就会出现负库存或者重复扣减。常见做法是乐观锁加重试,或者用 Redis 分布式锁。
@Transactional(rollbackFor = Exception.class) public void outbound(Long stockId, int qty, String bizNo) { // 乐观锁更新:只有版本号匹配才更新成功 int affected = stockMapper.reduceAvailable(stockId, qty); if (affected == 0) { // 更新失败,说明库存被并发修改,重试或抛异常 throw new BizException("库存不足或并发冲突,请重试"); } // 写流水 stockLogMapper.insert(new StockLog(stockId, "OUTBOUND", bizNo, -qty)); }对应的 SQL 要带版本号条件:
UPDATE wms_stock SET qty_available = qty_available - #{qty}, version = version + 1 WHERE id = #{stockId} AND qty_available >= #{qty} AND version = #{version};qty_available >= #{qty}这个条件保证不会扣成负数,version条件保证不会覆盖别人的更新。如果affected == 0,要么是库存不够,要么是并发冲突,业务上可以提示重试。高并发场景下,乐观锁重试次数要设上限,否则会拖垮数据库。
4.3 两端数据一致性的兜底
Web 端和 PDA 端看到的数据要一致,靠的是同一套库存表和同一个事务边界。但 PDA 端可能离线,补传的数据可能重复,所以接口要做幂等。常见做法是用业务单号加操作类型做唯一键,重复提交直接返回成功。
-- 幂等表:防止 PDA 重复提交 CREATE TABLE wms_idempotent ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_no VARCHAR(64) NOT NULL, biz_type VARCHAR(32) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_biz (biz_no, biz_type) );每次 PDA 提交前先插幂等表,插入成功才执行业务,插入冲突说明已经处理过,直接返回上次结果。这样即使网络抖动导致重复提交,库存也不会被扣两次。
5. 部署与二次开发中的避坑清单
5.1 时区不一致导致流水时间错乱
现象:库存流水表里的create_time比实际时间差 8 小时,对账时按时间段查询查不到数据。
原因:MySQL 8 的serverTimezone没配,或者 JVM 时区和数据库时区不一致。JDBC 连接串里没写serverTimezone=Asia/Shanghai时,驱动会用默认时区解析。
解决:连接串显式加serverTimezone=Asia/Shanghai,同时确认服务器系统时区和 JVM 启动参数-Duser.timezone=Asia/Shanghai一致。改完重启应用,历史数据如果已经错了,只能按业务时间手动修正。
5.2 PDA 端重复提交造成库存多扣
现象:操作工反映只扫了一次,但库存扣了两次,流水里出现两条相同单号的记录。
原因:PDA 端网络慢,用户等不及点了两次提交,或者离线补传时把同一条记录传了两遍。服务端没有做幂等校验。
解决:按 4.3 的方案加幂等表,biz_no + biz_type做唯一键。同时 PDA 端提交后要禁用按钮,直到收到响应。离线补传的数据要带原始时间戳,服务端按时间戳排序处理。
5.3 库位编码重复导致上架串位
现象:上架时扫的库位码明明是对的,但库存记到了另一个库位,拣货时找不到货。
原因:库位编码没有做唯一约束,或者导入初始数据时重复插入了相同编码的库位。PDA 端按编码查询时返回了多条,取了第一条。
解决:wms_location表的code字段加唯一索引,导入数据前先去重。PDA 端查询库位时如果返回多条,要报错而不是取第一条。Web 端新建库位时也要做编码重复校验。
5.4 事务范围过大导致 PDA 接口超时
现象:PDA 端提交上架请求,转圈很久最后超时,但后台其实已经处理成功,用户重试又造成重复。
原因:上架接口的事务里包含了日志记录、消息推送、报表更新等非核心操作,事务持有时间过长,PDA 端等不及。
解决:把非核心操作移出主事务,用异步或事务提交后的事件机制处理。主事务只保留库存更新和流水写入,保证快速返回。PDA 端的超时时间也要合理设置,别设太短。
5.5 前端静态资源路径写死导致部署后 404
现象:本地跑得好好的,部署到服务器后 Web 端页面能打开但接口 404,或者静态资源加载失败。
原因:前端代码里接口地址写死了localhost:8080,或者 Nginx 配置的代理路径和后端实际路径不一致。
解决:前端接口地址走环境变量或配置文件,部署时改成实际域名或相对路径。Nginx 代理要确认location /api/的转发规则和后端context-path匹配。PDA 端页面如果是独立部署,跨域配置也要检查。
6. 从跑通到能用:库存对账与压力验证的实操技巧
跑通一份 WMS 源码只是起点,真正决定它能不能投入使用的,是库存对账和并发压力这两关。我一般会先做一个小工具,把库存快照和流水做全量比对,公式很简单:期初 + 入库 - 出库 = 期末。如果对不上,就按库位和 SKU 逐条查流水,看是哪一笔的change_qty和快照变化不一致。
-- 库存对账:快照余量 vs 流水汇总 SELECT s.location_id, s.sku_id, s.qty_available, IFNULL(SUM(l.change_qty), 0) AS log_sum FROM wms_stock s LEFT JOIN wms_stock_log l ON l.stock_id = s.id GROUP BY s.location_id, s.sku_id, s.qty_available HAVING s.qty_available <> IFNULL(SUM(l.change_qty), 0);这条 SQL 查出来的就是账实不符的记录。常见原因是流水漏写、事务回滚不完整、或者并发更新时版本号没对上。发现之后不要急着改数据,先定位是哪次操作导致的,把根因修掉再修数据。
压力验证不用上专业工具,用 JMeter 或者简单的并发脚本就行。重点是模拟多个 PDA 同时扫同一个库位出库,看库存会不会扣成负数、流水会不会重复。我一般会开 20 个线程,每个线程提交 50 次出库请求,跑完检查库存和流水条数。如果库存出现负数,说明乐观锁或条件更新没写对;如果流水条数多于请求次数,说明幂等没做好。
还有一个容易被忽略的点是 PDA 端的离线补传。测试时要模拟断网,让 PDA 端缓存一批操作,恢复网络后一次性补传,看服务端能不能正确处理重复和乱序。补传的数据要带客户端时间戳,服务端按时间戳排序,遇到重复单号直接跳过。这个场景在真实仓库里很常见,尤其是地下库或信号死角。
最后说个习惯:每次改完库存相关的代码,我都会把对账 SQL 跑一遍,确认账实一致再提交。这个动作花不了几分钟,但能省掉后面大量的排查时间。WMS 这行,库存准不准是底线,功能再花哨,账对不上就是白搭。希望帮到你。
本文还有配套的精品资源,点击获取