简介:这是一套面向Java初学者与毕业设计学生的仓库管理系统完整项目源码,围绕库存跟踪、商品流转与资源分配等企业日常运营场景展开,适合作为课程设计、毕设选题或Java Web入门练手项目。压缩包共104个文件,约5.51MB,以73个class编译文件与14个java源码为主,另含3个jar依赖、1个sql建库脚本及少量jpg、gif、png界面截图,便于直接运行与二次开发。项目覆盖Java SE基础、MVC设计模式、Spring依赖注入与事务管理、MyBatis持久层操作、Servlet与JSP交互、Session与Cookie用户认证授权,以及商品增删改查、价格调整、供应商管理等核心模块,并涉及MySQL数据库设计与IDEA开发环境。目前已有1067人学习下载,读者可借此理解Java后端分层架构与业务系统构建流程,掌握从数据库设计到前后端联调的完整实践思路。
1. 从一张入库单说起:基于 Java 的仓库管理系统到底在管什么
很多团队做仓库管理系统,第一反应是“先建张库存表,加减数量不就行了”。真上线两周就会被打脸:采购收货、质检、上架、拣货、复核、出库、退货、盘点,每一步都在改同一批库存,谁先谁后、改多改少,全靠一张表根本兜不住。基于 Java 的仓库管理系统,本质是把“货、位、单、账”四件事拆开建模,再用事务和状态机把它们串成一条可追溯的链路。它解决的不是“记个数”,而是让每一次库存变动都有单据来源、有操作人、有前后快照,出了问题能倒查。
这套东西适合谁?一是中小电商、制造业、三方仓储里被 Excel 和多套系统折磨的开发和运维;二是正在做 Java 课程设计、想拿一个真实业务练 Spring Boot + MyBatis 的工程师。热搜里“java 八股文”“java 面试题”常年霸榜,但面试官真正会追问的是:库存扣减怎么防超卖、并发下怎么保证数据一致性、单据状态怎么流转。这些问题的答案,恰好都藏在一个能跑起来的仓库管理系统里。下面我按自己落过地的方案,把选型、建表、核心链路和踩过的坑讲清楚。
2. 技术选型与领域建模:为什么是 Spring Boot + MyBatis-Plus 这套组合
2.1 分层架构与依赖选型
仓库管理系统的业务复杂度集中在“单据流转 + 库存计算”,不在高并发网关层,所以选型的第一原则是开发效率和可维护性,而不是堆中间件。我一般用 Spring Boot 做单体起步,等出库峰值真的压不住了再拆。核心依赖大致是这几块:
| 层次 | 选型 | 理由 |
|---|---|---|
| Web 层 | Spring Boot + Spring MVC | 生态成熟,拦截器、参数校验开箱即用 |
| 持久层 | MyBatis-Plus | 单表 CRUD 免写 SQL,复杂库存查询仍可手写 XML |
| 数据库 | MySQL 8 + InnoDB | 行锁 + 事务是库存扣减的底线保障 |
| 缓存 | Redis(可选) | 热点库存预扣,降低数据库压力 |
| 鉴权 | Spring Security + JWT | 仓库有角色区分,理货员和主管权限不同 |
这里重点说 MyBatis-Plus。热搜里有人问“mybatisplus 根据 java 实体类生成创建表的 sql 语句”,这其实是个常见误区:MyBatis-Plus 本身不负责建表,它做的是实体与表的映射。真正能根据实体生成 DDL 的是 MyBatis-Plus 的代码生成器配合模板,或者干脆用 Flyway/Liquibase 管理建表脚本。我倾向于后者,因为仓库系统的表结构变更频繁,脚本化迁移比“实体反向生成”可控得多。
// 库存实体:注意 version 字段用于乐观锁,deleted 用于逻辑删除 @Data @TableName("wms_stock") public class Stock { @TableId(type = IdType.AUTO) private Long id; private Long skuId; // 商品 SKU private Long warehouseId; // 仓库 private Long locationId; // 库位 private Integer qty; // 可用数量 private Integer lockedQty; // 锁定数量(已分配未出库) @Version private Integer version; // 乐观锁版本号 @TableLogic private Integer deleted; }这段代码的关键在@Version和@TableLogic。@Version让 MyBatis-Plus 在 update 时自动带上version = ?条件,防止并发覆盖;@TableLogic让删除变成更新deleted字段,仓库数据不能物理删除,否则对账时历史单据会指向空记录。参数上,qty和lockedQty必须分开,可用库存 = qty - lockedQty,这是防超卖的基础。
2.2 领域模型:货、位、单、账四张核心表
仓库系统的表可以很多,但骨架就四组。第一组是主数据:商品 SKU、仓库、库位。第二组是单据:入库单、出库单、盘点单、调拨单,每张单据有主表和明细表。第三组是库存:按 SKU + 仓库 + 库位维度存数量。第四组是流水:每一次库存变动都写一条 inventory_transaction,记录变动前、变动后、来源单据。
-- 库存流水表:对账和追溯全靠它 CREATE TABLE wms_inventory_transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, location_id BIGINT NOT NULL, biz_type VARCHAR(32) NOT NULL, -- INBOUND/OUTBOUND/CHECK/TRANSFER biz_no VARCHAR(64) NOT NULL, -- 来源单据号 change_qty INT NOT NULL, -- 正数入库,负数出库 before_qty INT NOT NULL, after_qty INT NOT NULL, operator VARCHAR(32), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_sku_wh (sku_id, warehouse_id), KEY idx_biz_no (biz_no) );biz_type和biz_no是追溯的钥匙:任何一笔库存对不上,先按biz_no捞出这条单据的所有流水,看before_qty + change_qty是否等于after_qty,链条断在哪一步一目了然。before_qty和after_qty冗余存储是故意的,不要为了省空间只存 change_qty,否则并发场景下根本还原不出当时的真实值。索引建在sku_id + warehouse_id上,因为最高频的查询就是“某个 SKU 在某仓库还有多少”。
3. 核心链路落地:入库、出库与库存扣减的代码实现
3.1 入库上架:从收货单到库存增加
入库的完整链路是:采购单 → 收货单 → 质检 → 上架单 → 库存增加。很多简化版系统把收货和上架合并,结果就是“货还在月台,系统里已经显示在货架”,盘点时全是差异。我坚持拆开,收货只改“在途/待检”状态,上架才真正加库存。
@Transactional(rollbackFor = Exception.class) public void putAway(Long putAwayOrderId, String operator) { PutAwayOrder order = putAwayMapper.selectById(putAwayOrderId); if (order.getStatus() != PutAwayStatus.WAIT_PUTAWAY) { throw new BizException("单据状态不允许上架"); } for (PutAwayItem item : order.getItems()) { // 1. 加库存,带乐观锁重试 int retry = 0; while (retry < 3) { Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>() .eq(Stock::getSkuId, item.getSkuId()) .eq(Stock::getWarehouseId, order.getWarehouseId()) .eq(Stock::getLocationId, item.getLocationId())); if (stock == null) { stock = initStock(item, order); stockMapper.insert(stock); break; } int before = stock.getQty(); stock.setQty(before + item.getQty()); if (stockMapper.updateById(stock) > 0) { // 2. 写流水 saveTransaction(stock, item.getQty(), "INBOUND", order.getOrderNo(), operator); break; } retry++; } if (retry >= 3) { throw new BizException("库存更新冲突,请重试"); } } order.setStatus(PutAwayStatus.FINISHED); putAwayMapper.updateById(order); }逻辑说明:先校验单据状态,防止重复上架;库存不存在时初始化一条记录;存在时用乐观锁更新,updateById返回影响行数为 0 说明版本号被别的线程改了,重试最多 3 次。参数上,@Transactional保证库存和流水要么都成功要么都回滚,rollbackFor = Exception.class是因为默认只回滚运行时异常,业务异常如果不显式声明会漏掉。这里没用悲观锁select ... for update,是因为上架并发度通常不高,乐观锁重试成本更低;但出库场景就不一样了,见下一节。
3.2 出库拣货:防超卖的三道防线
出库是仓库系统最容易翻车的地方。热搜里“java 怎么保证数据一致性”问的就是这个。我的方案是三道防线:Redis 预扣、数据库乐观锁、库存流水核对。
public void allocate(Long outboundOrderId) { OutboundOrder order = outboundMapper.selectById(outboundOrderId); for (OutboundItem item : order.getItems()) { // 第一道:Redis 预扣,key 为 stock:sku:warehouse String key = "stock:" + item.getSkuId() + ":" + order.getWarehouseId(); Long remain = redisTemplate.opsForValue().decrement(key, item.getQty()); if (remain == null || remain < 0) { redisTemplate.opsForValue().increment(key, item.getQty()); // 回补 throw new BizException("库存不足:" + item.getSkuId()); } // 第二道:数据库锁定库存,locked_qty 增加 int updated = stockMapper.lockStock(item.getSkuId(), order.getWarehouseId(), item.getQty()); if (updated == 0) { redisTemplate.opsForValue().increment(key, item.getQty()); throw new BizException("数据库库存不足,预扣回滚"); } } }对应的 Mapper SQL 必须带条件,这是防超卖的核心:
UPDATE wms_stock SET locked_qty = locked_qty + #{qty}, version = version + 1 WHERE sku_id = #{skuId} AND warehouse_id = #{warehouseId} AND (qty - locked_qty) >= #{qty} AND deleted = 0;(qty - locked_qty) >= #{qty}这个条件写在 SQL 里,而不是先查再判断,是因为“查”和“改”之间有窗口,并发下必然超卖。把判断和更新合成一条原子语句,影响行数为 0 就说明可用库存不够。Redis 预扣只是挡在前面的减压阀,真正的底线是这条 SQL。第三道防线是出库完成后,把locked_qty减掉、qty减掉,并写流水,三者必须在同一事务里。
3.3 盘点与调拨:状态机怎么设计才不乱
盘点和调拨的难点不在计算,在状态流转。一张盘点单要经历“创建 → 盘点中 → 待审核 → 已审核 → 已调整”,每一步谁能操作、能改什么字段都不同。我一般用枚举 + 状态机表来管,而不是在 Service 里堆 if-else。
public enum CheckStatus { CREATED, CHECKING, WAIT_AUDIT, AUDITED, ADJUSTED; // 允许的流转:CREATED->CHECKING->WAIT_AUDIT->AUDITED->ADJUSTED public boolean canTransferTo(CheckStatus target) { return switch (this) { case CREATED -> target == CHECKING; case CHECKING -> target == WAIT_AUDIT; case WAIT_AUDIT -> target == AUDITED; case AUDITED -> target == ADJUSTED; default -> false; }; } }调拨则是“出库 + 入库”的组合,但绝不能简单调两次单。正确做法是调拨单审核后,源库位减库存、目标库位加库存,写两条流水,用同一个biz_no关联。如果分两次独立操作,中间失败就会出现“货已出、未入”的黑洞。参数上,调拨的biz_type用TRANSFER_OUT和TRANSFER_IN区分,对账时按biz_no聚合,两条流水的change_qty之和必须为 0。
4. 避坑与排查:仓库系统上线后最常翻车的 5 个点
4.1 库存对不上,流水却“看起来”没问题
现象:盘点时发现实物比系统少,但查流水每条before + change = after都成立。原因:并发下两条流水交叉写入,各自算出的before_qty都是旧值,导致后写的覆盖了先写的。解决:库存更新必须用数据库行锁或乐观锁串行化,流水写入放在更新成功之后,且before_qty从更新后的返回值反推,不要自己查了再算。
4.2 单据重复提交,库存加了两遍
现象:用户网络卡顿点了两次“确认上架”,库存翻倍。原因:接口没有幂等控制。解决:前端按钮置灰只是体验层,后端必须用单据号 + 状态做幂等,update ... where status = 'WAIT'影响行数为 0 就直接返回,别抛异常也别再执行。
4.3 逻辑删除后唯一索引冲突
现象:删掉一个库位再新建同名库位,报唯一键冲突。原因:@TableLogic只是把deleted置 1,数据库唯一索引还认这条记录。解决:唯一索引把deleted字段加进去,或者用时间戳做删除标记,别用 0/1。
4.4 大事务拖垮数据库
现象:一次上架 500 个 SKU,事务跑了 30 秒,锁等待超时。原因:整个上架单一个大事务。解决:按 SKU 拆小事务,或者用批量更新 + 异步写流水,但要注意拆事务后的一致性边界,宁可慢也别丢数据。
4.5 Redis 预扣和数据库不一致
现象:Redis 显示有库存,数据库扣减失败。原因:Redis 扣了但数据库事务回滚,回补逻辑没执行。解决:预扣和回补必须成对,用 try-finally 包住,或者干脆把 Redis 当纯缓存,库存判断只信数据库。我一般后者,Redis 只做限流和热点读,不做扣减依据。
5. 进阶技巧:用流水对账脚本验证系统一致性
系统跑起来只是开始,能不能长期可信,取决于你有没有一套自动对账手段。我习惯写一个定时任务,每天凌晨跑一遍库存流水,验证三件事:每条流水的before + change = after;每个 SKU 的流水累加值等于当前库存;锁定库存不超过总库存。
@Scheduled(cron = "0 0 2 * * ?") public void reconcile() { List<Stock> stocks = stockMapper.selectList(null); for (Stock stock : stocks) { // 1. 流水累加校验 Integer sum = transactionMapper.sumChangeQty(stock.getSkuId(), stock.getWarehouseId()); if (sum == null) sum = 0; if (!sum.equals(stock.getQty())) { log.error("库存不一致 sku={} wh={} 流水和={} 库存={}", stock.getSkuId(), stock.getWarehouseId(), sum, stock.getQty()); } // 2. 锁定库存不能超过总库存 if (stock.getLockedQty() > stock.getQty()) { log.error("锁定超限 sku={} locked={} qty={}", stock.getSkuId(), stock.getLockedQty(), stock.getQty()); } } }这个脚本的价值在于:它不依赖业务代码的正确性,而是用最原始的流水累加去反推库存。一旦报警,先别急着改数据,按biz_no把相关单据的流水全捞出来,看是哪一步的after_qty和下一步的before_qty对不上,断点就是 bug 所在。参数上,cron放在凌晨业务低峰,sumChangeQty走覆盖索引避免全表扫。
还有一个容易被忽略的技巧:给库存表加一个last_transaction_id字段,每次更新库存时把最新流水 ID 写进去。对账时如果发现某条库存的last_transaction_id对应的流水时间早于该 SKU 的最后一条流水,说明有更新漏写了流水,直接定位到具体操作。这个字段我踩过坑才加上,属于典型的“后悔药”设计。
最后说个习惯:任何涉及库存变动的接口,我都会在测试环境用 JMeter 开 50 个线程并发打,专门看有没有超卖和流水断链。上线前跑一遍,比事后对账省心得多。仓库系统不怕功能少,怕的是账对不上,一旦信任崩了,后面推什么功能都难。希望帮到你。
本文还有配套的精品资源,点击获取