
简介一份基于SSMSpringSpringMVCMybatis的仓库管理系统完整源码内含Java后端工程、MySQL数据库脚本与演示视频重点面向Java Web初学者、课程设计及毕业设计学生可帮助理解仓库管理中入库、出库、库存查询等典型场景的实现方式。资源共564个文件压缩包大小约60.79MB主要包含185个class文件、92个java源码、36个JSP页面、65个XML配置以及jar依赖、JS/CSS、SQL脚本、mp4演示视频等目录结构清晰覆盖代码、配置、静态资源与数据库脚本基本达到下载后即可导入IDE进行构建运行的程度。项目实现基于Spring的依赖注入与事务管理SpringMVC负责请求分发Mybatis封装数据持久化Maven管理依赖Tomcat作为运行容器适合读者对照代码逐步拆解SSM整合思路。已有1731人学习浏览通过源码与演示视频结合可作为仓库管理系统开发初期的起步模板也便于扩展商品管理、供应商管理等后续功能。1. 仓库管理系统用 ssmjsp 源码跑起来之前先想清楚这套组合的定位仓库管理系统WMS被封装成 ssmjsp 项目源码这种形态在 JavaWeb 的课程设计、毕业设计和中小型企业内部系统里出现频率一直很高。组合里的 Spring 负责业务对象装配SpringMVC 管请求路由MyBatis 管数据库操作JSP 负责结果页渲染。标题里同时出现“源码”和“数据库”说明交付物是可运行的完整工程而不是零散代码片段演示视频则意味着作者已经默认你要照着操作步骤复现整个环境。对 IT 从业者来说这个题目真正值得关注的不是登录注册页面而是入库、出库、库存汇总这一套事务与分组查询逻辑以及“数据库和 JSP 之间数据怎么运输”的完整链路。下文按数据库建模、SSM 配置、业务事务编码、部署验证四条线展开看完之后你既能手动重建这套系统也能在现有源码基础上快速改出自己要的功能遇到“jsp 改了不生效”这类部署问题也有明确的排查路径。2. ssm 与 jsp 各管什么仓库管理系统里的分层、数据流转与版本搭配2.1 仓库管理系统的 ssm 分层把容器、路由和 SQL 划分清楚SSM 三个字母对应 Spring、SpringMVC、MyBatis。Spring 负责管理 Service 和 Mapper 这类对象的生命周期入库、出库、盘点服务在 Java 里都是无状态业务对象声明好构造函数后容器会自动注入依赖业务代码里不会出现一行new xxxService()。SpringMVC 覆盖 HTTP 层请求参数绑定、URL 映射、返回结果都归它管。MyBatis 负责持久化映射它的特点是 SQL 写在 XML 里通过动态 SQL 拼接 where 条件恰好适配仓库管理系统中“按商品编码 仓库 日期范围”这种组合查询。JSP 在整套结构里的定位经常被低估。有人认为 JSP 是过时技术但对仓库管理系统这种表格、表单、搜索框密集型的页面来说JSP 配合 JSTL 和 EL 表达式可以直接把后端的ListStock渲染成页面不需要为了一个内部系统维护两套前端工程。JSP 本身会编译成 Servlet能直接访问 request 和 session 作用域这意味着 Controller 里model.addAttribute(当前用户, user)之后页面可以直接取到排错时看 JSP 的报错栈也比看前后端接口日志更直观。2.2 一张入库单从 JSP 到 MySQL 的数据流转路径实际操作里一条入库数据是这样流动的用户在inbound/add.jsp填写数量和备注form 表单的action指向/inbound/savemethodpost。DispatcherServlet 拦截请求后根据PostMapping(/save)找到 InboundController 方法Spring 自动把 form 表单里namewarehouseId的参数映射到 DTO 的同名字段。业务方法执行结束后返回redirect:/inbound/list视图解析器看到redirect:前缀就返回 302浏览器重新请求列表页最终数据以新的 JSP 页面输出。这一段流转中最容易出错的地方是把参数名写错比如前端传warehouseId后端 DTO 里写widSpring 绑定会静默失败得到 null。列表页的 JSP 渲染核心是 JSTL 的循环标签这里给出一段可直接复用的片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered thead trth仓库名称/thth商品编码/thth商品名称/thth库存数量/th/tr /thead tbody c:forEach items${stockPage.list} varstock tr td${stock.warehouseName}/td td${stock.skuCode}/td td${stock.skuName}/td td${stock.quantity}/td /tr /c:forEach /tbody /table逻辑说明${stockPage.list}来自 Controller 中model.addAttribute(stockPage, page)传入的分页对象c:forEach每次迭代把列表中的元素绑定到stock变量上后续用${stock.quantity}读取。页面里不能直接写后端实体类需要保证实体 getter 命名规范MyBatis 返回值才撑得起 EL 表达式取值。2.3 maven 坐标与依赖版本怎么选源码编译不过的常见根源拿到源码后第一步是检查 Maven 依赖。仓库管理系统这类项目经常出现在不同人机器上抛出ClassNotFoundException或NoSuchMethodError多数是版本混搭导致。以下是一组相对稳定的搭配适合 JDK 1.8、Tomcat 8.5/9 环境properties spring.version5.3.27/spring.version mybatis.version3.5.10/mybatis.version mybatis-spring.version2.0.7/mybatis-spring.version mysql.version8.0.33/mysql.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency /dependencies参数说明spring-webmvc和spring-context必须同版本否则组件扫描会在运行时出现 conflicting bean 定义。mybatis-spring负责把 MyBatis 的 SqlSessionFactory 交给 Spring 管理版本 2.x 匹配 MyBatis 3.5.x。MySQL 驱动 8.x 对应com.mysql.cj.jdbc.Driver旧源码里写com.mysql.jdbc.Driver的在 MySQL 8 下也能跑但会打印过时警告建议顺手换掉。JSP 的 servlet-api 由 Tomcat 提供不要把 servlet-api.jar 打进 WAR 包否则启动时容易产生java.lang.LinkageError。2.4 功能模块与页面映射仓库管理系统的最小划分把整个仓库管理系统拆成下面这些模块每一块对应一组 Controller 和 JSP这也是大多数源码在WEB-INF/views目录下的实际组织方式功能模块请求入口Service 核心类视图位置登录与用户管理/user/loginUserService/WEB-INF/views/user/login.jsp仓库与货位维护/warehouse/listWarehouseService/WEB-INF/views/warehouse/list.jsp商品基础资料/sku/listSkuService/WEB-INF/views/sku/list.jsp入库单管理/inbound/saveInboundService/WEB-INF/views/inbound/add.jsp出库单管理/outbound/saveOutboundService/WEB-INF/views/outbound/add.jsp库存查询与盘点/stock/listStockService/WEB-INF/views/stock/list.jsp设计这套映射的意图非常直接库存表不单独提供修改入口所有数量变化必须经过入库单或出库单的 Service 方法盘点才能作为第三种修正途径。不少人在改造源码时会绕过 Service 直接改 Mapper短时间看不出问题一旦做报表会对不上账。3. 数据库设计仓库管理系统的核心表、索引与 ssm 配置文件3.1 库存表与流水表如何建模仓库管理系统的数据库设计不能把“库存数量”只放在商品表里。一个 SKU 可能在多个仓库有库存一个仓库也存放多种商品所以需要单独的库存表以warehouseId skuId作为联合唯一键。每一次变动都要落到流水表这样即使库存数据被误更新也能根据流水恢复。下面是 MySQL 8 下的建表语句CREATE DATABASE IF NOT EXISTS wms_ssm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE wms_ssm; CREATE TABLE wms_warehouse ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_code VARCHAR(32) NOT NULL UNIQUE, warehouse_name VARCHAR(128) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE wms_sku ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL UNIQUE COMMENT 商品编码外部系统同步, sku_name VARCHAR(128) NOT NULL, category_code VARCHAR(32) DEFAULT NULL, unit VARCHAR(16) DEFAULT 件, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE wms_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_warehouse_sku (warehouse_id, sku_id), CONSTRAINT fk_stock_warehouse FOREIGN KEY (warehouse_id) REFERENCES wms_warehouse(id), CONSTRAINT fk_stock_sku FOREIGN KEY (sku_id) REFERENCES wms_sku(id) ) ENGINEInnoDB; CREATE TABLE wms_stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, warehouse_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, change_qty INT NOT NULL COMMENT 正数入库负数出库, before_qty INT NOT NULL, after_qty INT NOT NULL, biz_type VARCHAR(32) NOT NULL COMMENT INBOUND/OUTBOUND/CHECK, biz_no VARCHAR(64) NOT NULL COMMENT 业务单号防止重复提交, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_biz_no (biz_no), KEY idx_sku_time (sku_id, create_time) ) ENGINEInnoDB;逻辑说明wms_stock_flow里的uk_biz_no唯一约束是在拦截重复提交前端按钮没做防重点时用户连点两次提交会产生两个相同 biz_no数据库直接拒绝第二条插入。before_qty和after_qty存的是变更前后的快照这样报表模块查询流水时不需要再去关联库存表。库存表的联合唯一键保证同一个仓库里同一商品只有一行库存记录。3.2 初始化基础数据仓库、商品、初始库存一次插入源码里配套的 database 目录一般会有初始化脚本如果没有你自己也能补。下面这一段插入基础数据覆盖一个仓库和两个商品方便启动后立刻验证功能INSERT INTO wms_warehouse (warehouse_code, warehouse_name) VALUES (WH-BJ-01, 北京一号库); INSERT INTO wms_sku (sku_code, sku_name, category_code, unit) VALUES (SKU-10086, 无线扫码枪, HW, 台), (SKU-10010, 工业标签纸, HW, 卷); INSERT INTO wms_stock (warehouse_id, sku_id, quantity) VALUES (1, 1, 120), (1, 2, 500);参数说明插入库存时quantity可以等于 0 或一个盘点后的初始值。很多项目会在系统上线时先跑“库存初始化”功能本质就是批量往wms_stock写入数据。注意不要在这个阶段插入流水否则盘点报表会把初始库存误认为入库记录。3.3 jdbc 配置与 MyBatis 整合参数ssm框架连接数据库的两种易错点SSM 里配置数据库的方式沿用 Spring 管理数据源的做法。jdbc.properties里最重要的参数如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/wms_ssm?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 jdbc.usernameroot jdbc.passwordyour_password jdbc.initialSize5 jdbc.maxActive20 jdbc.maxWait3000参数说明serverTimezoneAsia/Shanghai不写MySQL 8 驱动会报CST时区错误。maxWait3000指连接池在拿不到连接时最多等待 3 秒超过直接抛异常如果设置为负数系统会无限等待生产环境会让请求线程全部挂住。数据库密码里有特殊字符时properties 文件要转义例如写成\。整合 MyBatis 的 Spring 配置集中在spring-mybatis.xmlcontext:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value${jdbc.initialSize}/ property namemaxActive value${jdbc.maxActive}/ property namemaxWait value${jdbc.maxWait}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.wms.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.wms.mapper/ /bean逻辑说明mapperLocations配置可以简写为classpath:mapper/*.xml它让 MyBatis 在启动阶段加载所有 Mapper XML缺少这行配置时经常出现接口方法在运行时报Invalid bound statement (not found)。MapperScannerConfigurer的basePackage负责扫描 Mapper 接口并生成代理对象这样才能在 Service 里直接用Autowired注解注入。更常见的坑是把typeAliasesPackage配成空指针或用mybatis-config.xml重复配置别名导致 entity 包里的类无法被识别SQL 返回结果映射失败。3.4 数据源常用的关键参数速查下面这个表格对应 Druid 连接池里需要知道的几个参数阅读源码时如果遇到不理解的值可以对照参数名作用建议值initialSize启动时创建的物理连接数5maxActive最大活跃连接数20maxWait获取连接最大等待毫秒数3000minIdle最小空闲连接数5timeBetweenEvictionRunsMillis空闲连接检测周期60000validationQuery连接有效性检测语句SELECT 1production 环境建议把testWhileIdle保持默认 true这样连接池会定期检测连接是否被数据库主动断开否则前一夜被回收的连接会影响第二天的第一次业务请求。4. 用 ssm 写仓库管理系统的核心业务入库、出库与事务控制4.1 Service 层事务边界怎么设仓库管理系统里最容易出现的数据不一致场景是流水插入了库存没更新或者库存减了流水没记录。解决这个问题必须把“更新库存”和“插入流水”放在同一个事务内。下面是入库 Service 的推荐写法Service public class InboundServiceImpl implements InboundService { private final StockMapper stockMapper; private final StockFlowMapper stockFlowMapper; public InboundServiceImpl(StockMapper stockMapper, StockFlowMapper stockFlowMapper) { this.stockMapper stockMapper; this.stockFlowMapper stockFlowMapper; } Override Transactional(rollbackFor Exception.class) public void createInbound(InboundDTO dto) { Stock stock stockMapper.lockByWarehouseAndSkuForUpdate( dto.getWarehouseId(), dto.getSkuId()); int beforeQty stock null ? 0 : stock.getQuantity(); int afterQty beforeQty dto.getQuantity(); if (stock null) { stock new Stock(); stock.setWarehouseId(dto.getWarehouseId()); stock.setSkuId(dto.getSkuId()); stock.setQuantity(afterQty); stockMapper.insert(stock); } else { stockMapper.updateQuantity( dto.getWarehouseId(), dto.getSkuId(), afterQty); } StockFlow flow new StockFlow(); flow.setWarehouseId(dto.getWarehouseId()); flow.setSkuId(dto.getSkuId()); flow.setChangeQty(dto.getQuantity()); flow.setBeforeQty(beforeQty); flow.setAfterQty(afterQty); flow.setBizType(INBOUND); flow.setBizNo(buildBizNo(dto)); stockFlowMapper.insert(flow); } }逻辑说明Transactional(rollbackFor Exception.class)是关键默认情况下 Spring 只对 RuntimeException 回滚改成Exception.class后检查异常也能触发回滚。lockByWarehouseAndSkuForUpdate会在数据库层面锁住库存行防止两个并发请求同时读到相同的旧库存。如果库存表中没有对应记录说明是首次入库直接 insert 库存行如果已有记录用更新后的库存量覆盖旧值流水里保留 before 和 after 两个快照。4.2 Mapper XML 里的 FOR UPDATE 与动态 SQL上面 Service 依赖的 Mapper XML 写法如下select idlockByWarehouseAndSkuForUpdate resultTypecom.wms.entity.Stock SELECT id, warehouse_id, sku_id, quantity, updated_time FROM wms_stock WHERE warehouse_id #{warehouseId} AND sku_id #{skuId} FOR UPDATE /select update idupdateQuantity UPDATE wms_stock SET quantity #{quantity} WHERE warehouse_id #{warehouseId} AND sku_id #{skuId} /update insert idinsert parameterTypecom.wms.entity.Stock useGeneratedKeystrue keyPropertyid INSERT INTO wms_stock (warehouse_id, sku_id, quantity) VALUES (#{warehouseId}, #{skuId}, #{quantity}) /insert参数说明FOR UPDATE在 InnoDB 引擎下走的是行锁前提是查询条件能命中唯一索引。前面建表时给wms_stock加了uk_warehouse_sku (warehouse_id, sku_id)所以FOR UPDATE能精确锁住唯一一行而不是锁全表。useGeneratedKeystrue配合keyPropertyidinsert 完成后把自增主键返回到实体对象的 id 字段后续如果要拿主键关联其他表就用得上。组合查询库存时Mapper 里会用到动态 SQL。下面这段是按条件模糊查询的常见写法select idsearchStock resultTypecom.wms.dto.StockVO SELECT s.id, w.warehouse_name, sk.sku_code, sk.sku_name, s.quantity FROM wms_stock s LEFT JOIN wms_warehouse w ON s.warehouse_id w.id LEFT JOIN wms_sku sk ON s.sku_id sk.id where if testwarehouseId ! null AND s.warehouse_id #{warehouseId} /if if testskuName ! null and skuName ! AND sk.sku_name LIKE CONCAT(%, #{skuName}, %) /if /where ORDER BY s.updated_time DESC /select逻辑说明where标签会自动去掉第一个多余的AND比手动写where 11干净索引使用上也更友好。LIKE CONCAT(%, #{skuName}, %)不用${skuName}直接拼接是为了防止 SQL 注入同时避免中文乱码问题。这种查询在仓库管理系统的库存页面里是绝对核心稍微复杂的报表也会在这里扩展 group by。4.3 出库与 Controller 层参数绑定库存不足的异常怎么处理出库比入库多一步校验库存数量不能为负。最常见的做法是在 Service 中判断库存不足直接抛业务异常而不是返回一个带有状态码的普通对象。下面是出库 Service 中校验库存的重要片段Stock lock stockMapper.lockByWarehouseAndSkuForUpdate( dto.getWarehouseId(), dto.getSkuId()); if (lock null || lock.getQuantity() dto.getQuantity()) { throw new InsufficientStockException( dto.getSkuId(), dto.getQuantity()); } stockMapper.updateQuantity( dto.getWarehouseId(), dto.getSkuId(), lock.getQuantity() - dto.getQuantity());Controller 统一捕获这个异常并定位到错误页面Controller RequestMapping(/outbound) public class OutboundController { private final OutboundService outboundService; public OutboundController(OutboundService outboundService) { this.outboundService outboundService; } PostMapping(/save) public String save(ModelAttribute OutboundDTO dto, Model model) { try { outboundService.createOutbound(dto); return redirect:/outbound/list; } catch (InsufficientStockException e) { model.addAttribute(errorMsg, 库存不足当前剩余 e.getCurrentQty()); return outbound/error; } } }逻辑说明ModelAttribute负责把表单参数绑定到 OutboundDTO前提是 JSP 里 input 的 name 和 DTO 字段名一致。异常的捕获放在 Controller 层页面层只需要读取errorMsg展示。不要把 try-catch 吞进 Service 里否则事务边界会被破坏库存和流水的原子性就没了。5. 源码部署、运行验证与“jsp 改了不生效”的三个处理位置5.1 从 mvn clean 到拉起数据库的启动检查拿到源码后建议先用命令行的方式验证环境而不是直接在 IDE 里点运行。确认本机 Java 和 Maven 版本后按下面的顺序操作mysql -uroot -p wms_ssm.sql mvn clean package -DskipTests mvn tomcat7:run第一条命令把源码包里的数据库脚本导入 MySQL。第二条命令跳过单元测试只打包。第三条命令是借用 tomcat7 插件启动实际代码跑在 Tomcat 9 里也没有问题。启动日志出现INFO: Deploying web application directory之后访问http://localhost:8080/wms/login.jsp上下文路径以 web.xml 和工程名为准。验证登录是否成功可以直接观察页面跳转。登录成功后列表页能显示之前插入的四个商品库存行说明数据库连接、MyBatis 映射、JSP 渲染都已经打通。如果页面报 500先看日志里Caused by段绝大部分是数据库账号密码错误或 mapper XML 没加载。5.2 浏览器、Tomcat、IDE 三层缓存怎么逐层排除“JSP 改了不生效”在 ssmjsp 项目里属于最高频问题但问题通常不在修改动作本身而是三层缓存叠加。浏览器层好判断按 F12 打开 Network看当前请求是否返回 304如果返回 304 就强制刷新或给 URL 参数加时间戳。第二层是 Tomcat 对 JSP 编译结果的缓存。开发模式下检查 Tomcat 的conf/web.xmlJspServlet 的development参数应为 true否则 JSP 只在首次启动时编译之后不再重新检查文件变动。第三层是 IDE 部署路径。IntelliJ IDEA 里 Artifacts 选择 exploded 模式时修改 JSP 后必须重新 build否则 Tomcat 加载的是 target 目录里的旧文件。多数“改了没反应”的案例都出在这一层可以直接看 Tomcat 实际加载的路径对比 JSP 文件修改时间比盲目重启容器有效。5.3 用一条 SQL 验证入库是否真实落账源码改造完成后不要只依赖页面数据直接查数据库更能确认事务是否生效SELECT f.biz_no, f.biz_type, f.change_qty, f.before_qty, f.after_qty, s.quantity AS current_stock FROM wms_stock_flow f JOIN wms_stock s ON f.warehouse_id s.warehouse_id AND f.sku_id s.sku_id ORDER BY f.create_time DESC LIMIT 10;把页面新增的入库数和这条 SQL 的结果做对比如果after_qty与current_stock一致说明事务回滚逻辑没有破坏数据一致性如果不一致优先排查 Service 方法上是否少加了Transactional。这条验证思路同样适用于出库单和盘点调整单仓库管理系统里最需要盯的数据就是流水表最后一行的 after 值和库存表当前值。本文还有配套的精品资源点击获取