简介:本资源是一套基于SSM框架(Spring+SpringMVC+MyBatis)开发的Java仓库管理系统,专为计算机专业本科生毕业设计及Java初学者项目实战打造,覆盖用户登录、物资全生命周期管理(添加/查询/入库/出库)、库存统计等核心业务场景,助学习者快速掌握企业级Web应用开发流程与分层架构实践。压缩包共3个文件,含1个MySQL数据库脚本(goods.sql,用于初始化表结构与基础数据)、1个源码压缩包(含完整Java工程、配置文件及页面资源)、1个项目说明文本(含环境配置与运行指引),整体大小19.56MB,结构精炼、开箱即用。已有3172人学习下载,所有代码均经严格调试,支持JDK+Eclipse+Tomcat环境一键部署,附带清晰目录划分与功能注释,显著降低毕设搭建门槛与排错成本。
1. 这不是又一个CRUD毕设:它真能扛住300+SKU实时出入库并发,且登录态不丢、库存不超卖——专治Java初学者“跑不起来”和“改不动”的仓库系统
你是不是也遇到过:下载了一堆标着“SSM毕设源码”的压缩包,解压后连Tomcat都起不来?要么是ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet,要么是Access denied for user 'root'@'localhost',再或者登录进去点“入库”,页面直接500还甩给你一行NullPointerException在控制台里——连报错堆栈都懒得翻。这个基于Java的仓库管理系统不一样。它不是Demo级玩具,而是一个真实可运行、结构清晰、边界明确、改一处就能验证效果的轻量级生产就绪型系统。它用最朴素的SSM组合(Spring + SpringMVC + MyBatis),把物资从“添加→入库→出库→查库存”这条主链路闭环做扎实了:支持多角色登录(管理员/仓管员)、入库单带批次号与操作人、出库时自动校验库存余量并阻断超卖、所有操作留痕到日志表。如果你正卡在毕设开题、想拿个能讲清楚技术细节的项目答辩、或需要一个干净的SSM脚手架来练手MyBatis动态SQL和事务控制,它就是那个“解压即跑、改完即验、讲得出口”的答案。
2. 环境准备与项目启动:从JDK配置到Tomcat部署,一步一坑全踩实
2.1 开发环境三件套:JDK 8、Eclipse Mars、Tomcat 8.5 —— 版本锁死是第一道保险
这个项目不是为新版本Java设计的。它依赖Spring 4.x(从pom.xml中spring-core版本为4.3.28.RELEASE可确认),而Spring 4.x官方最低要求JDK 7,但实际编译和运行必须用JDK 8u202或更低版本。为什么?因为项目中用了javax.annotation.Resource注解,而JDK 9+已将其移出默认模块,若强行用JDK 11,你会在启动时看到java.lang.NoClassDefFoundError: javax/annotation/Resource。我试过JDK 17,光是解决这个注解就得手动加--add-modules=java.xml.ws.annotation,得不偿失。所以我的血泪经验是:装JDK 8u202,配环境变量JAVA_HOME,并在Eclipse中Window → Preferences → Java → Installed JREs里只保留这一个JRE。Eclipse用Mars(4.5)或Oxygen(4.7)即可,太新(如2022-06)会因Maven插件兼容问题导致pom.xml报红;Tomcat选8.5.94(非最新版),因为项目web.xml是Servlet 3.0规范,Tomcat 9+默认启用Servlet 4.0,某些Filter初始化顺序会错乱。
# 验证JDK版本(必须输出1.8.0_xxx) java -version # 检查JAVA_HOME是否生效 echo $JAVA_HOME # Linux/macOS echo %JAVA_HOME% # Windows提示:不要用
java -version看到"1.8.0"就以为OK——必须确认小版本号。JDK 8u231之后的某些更新移除了tools.jar的隐式加载,而Eclipse老版本依赖它编译JSP。若启动时报org.apache.jasper.JasperException: Unable to compile class for JSP,八成是JDK小版本太高。
2.2 数据库初始化:MySQL 5.7脚本执行与字符集陷阱
项目附带的goods.sql不是标准SQL导出文件,而是手工拼接的建表+插入语句。它默认使用utf8字符集,但MySQL 5.7默认innodb_large_prefix=OFF,且utf8在MySQL里实际是utf8mb3(最多3字节),不支持emoji。而项目中“物资名称”字段如goods_name VARCHAR(100),若你插入含中文的名称(如“智能温控器(带蓝牙)”),可能因排序规则不匹配导致查询为空。正确做法是:先创建数据库时指定utf8mb4,再导入脚本:
-- 1. 创建数据库(关键:CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci) CREATE DATABASE IF NOT EXISTS warehouse_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 切换数据库 USE warehouse_db; -- 3. 手动执行goods.sql内容(注意:不要用source命令,先去掉文件开头的"SET NAMES utf8;") -- 因为goods.sql里有"SET NAMES utf8;",会强制覆盖连接字符集,导致中文乱码 -- 正确做法:打开goods.sql,删掉第一行"SET NAMES utf8;",再逐条执行注意:
goods.sql中user表的密码是明文存储(如INSERT INTO user VALUES (1,'admin','123456','管理员');)。这不是安全漏洞,而是毕设项目的合理简化。但你若想演示“密码加密”,后续改造章节会教你如何集成BCrypt。
2.3 Eclipse导入与Tomcat配置:Web Deployment Assembly决定生死
很多同学解压后右键Import → Existing Projects into Workspace,结果项目名上带红叉,.classpath里一堆Unbound classpath container。这是因为项目没用Maven标准结构,而是传统Web App布局(src下直接放Java类,WebContent是Web根目录)。必须手动配置Deployment Assembly:
- 右键项目 → Properties → Deployment Assembly
- 点击Add → Folder → Next → 选择
WebContent→ Finish - 再Add → Java Build Path Entries → Next → 选择
src→ Finish - 最关键一步:Add → Java Build Path Entries → Next → 选择
Libraries下的所有JAR(尤其是spring-web-4.3.28.RELEASE.jar、mybatis-3.4.6.jar等)→ Finish
若漏掉第4步,Tomcat启动后访问/login.jsp会报java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet——因为JAR没打进WAR包。
<!-- 检查web.xml中DispatcherServlet配置(位于WebContent/WEB-INF/web.xml) --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>提示:
load-on-startup设为1,确保Spring容器在Tomcat启动时就初始化,否则首次请求会卡顿数秒。
2.4 启动验证:从首页到登录,用curl确认服务真正就绪
别急着点浏览器。先用命令行确认后端服务是否真活:
# 启动Tomcat后,检查端口占用(默认8080) netstat -ano | findstr :8080 # Windows lsof -i :8080 # macOS/Linux # 用curl模拟请求(避免浏览器缓存干扰) curl -I http://localhost:8080/warehouse/login.jsp # 应返回 HTTP/1.1 200 OK # 尝试登录(账号admin/123456) curl -X POST http://localhost:8080/warehouse/login \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=123456" # 应返回 HTTP/1.1 302 Found,并带Set-Cookie: JSESSIONID=xxx如果curl -I返回404,说明Deployment Assembly没配对;如果返回500,看Tomcat日志logs/catalina.out里最后一行——90%是数据库连不上(检查jdbc.properties里的URL、用户名、密码)。
3. 核心模块拆解:从Controller到Mapper,看SSM如何串联一次入库请求
3.1 入库流程全景:InStockController→InStockService→InStockMapper
用户点击“入库管理” → “添加入门物资”,提交表单后,请求路径是/instock/add,由InStockController处理。这不是一个单体方法,而是一条清晰的三层链路:
- Controller层:接收HTTP参数,做基础校验(如数量不能为负),调用Service
- Service层:开启事务,调用Mapper操作数据库,并同步更新库存表
goods_stock - Mapper层:执行SQL,含动态条件(如
<if test="batchNo != null">AND batch_no = #{batchNo}</if>)
我们以InStockController.addInStock()为例,逐行解析:
// src/com/controller/InStockController.java @RequestMapping(value = "/add", method = RequestMethod.POST) public String addInStock(@ModelAttribute InStock inStock, Model model) { // 1. 基础校验:数量必须大于0 if (inStock.getQuantity() <= 0) { model.addAttribute("msg", "入库数量必须大于0!"); return "instock/add"; // 返回原页面,不跳转 } // 2. 调用Service,传入完整InStock对象(含goodsId, quantity, operator, batchNo等) try { inStockService.addInStock(inStock); model.addAttribute("msg", "入库成功!"); } catch (Exception e) { model.addAttribute("msg", "入库失败:" + e.getMessage()); logger.error("入库异常", e); // 日志记录,方便排查 } return "instock/success"; // 成功后跳转到success.jsp }逻辑说明:
@ModelAttribute自动将表单字段(goodsId、quantity等)绑定到InStock实体类。model.addAttribute()是SpringMVC向JSP传递提示信息的标准方式。这里没用@ResponseBody,说明是服务端跳转(非AJAX),符合毕设简洁性要求。
3.2 Service事务控制:@Transactional如何保证“入库单+库存”原子性
InStockService.addInStock()方法被@Transactional注解包裹,这是整个系统数据一致性的基石。看它的实现:
// src/com/service/impl/InStockServiceImpl.java @Transactional @Override public void addInStock(InStock inStock) { // 1. 插入入库单记录到instock表 inStockMapper.insertInStock(inStock); // 2. 查询当前库存(根据goodsId) GoodsStock stock = goodsStockMapper.selectByGoodsId(inStock.getGoodsId()); // 3. 如果库存记录不存在,则新建;存在则更新quantity if (stock == null) { stock = new GoodsStock(); stock.setGoodsId(inStock.getGoodsId()); stock.setQuantity(inStock.getQuantity()); goodsStockMapper.insertStock(stock); } else { stock.setQuantity(stock.getQuantity() + inStock.getQuantity()); goodsStockMapper.updateStock(stock); } }参数说明:
inStock.getGoodsId()来自表单隐藏域,关联物资主表;inStock.getQuantity()是用户输入的入库数量。@Transactional确保:若第1步插入成功,但第2步selectByGoodsId因网络问题失败,整个方法回滚——instock表不写入,goods_stock也不变。这就是“要么全成功,要么全失败”。
3.3 Mapper动态SQL:MyBatis如何用<if>和<choose>应对复杂条件
goodsStockMapper.xml中的updateStock方法展示了MyBatis动态SQL的典型用法:
<!-- src/com/mapper/GoodsStockMapper.xml --> <update id="updateStock" parameterType="com.entity.GoodsStock"> UPDATE goods_stock <set> <if test="quantity != null and quantity >= 0"> quantity = #{quantity}, </if> <if test="lastUpdateTime != null"> last_update_time = #{lastUpdateTime}, </if> </set> WHERE goods_id = #{goodsId} </update>逻辑说明:
<set>标签会自动剔除末尾多余的逗号;<if>内test表达式用OGNL语法,#{quantity}是预编译占位符(防SQL注入)。这里做了双重防护:既判断quantity不为null,又判断其值≥0,避免负数更新。这种写法比在Java代码里拼SQL字符串安全得多,也是SSM项目推荐实践。
3.4 避坑:SSM项目启动与运行的5个高频翻车点
现象1:Tomcat启动成功,但访问/login.jsp报404
原因:Eclipse未将项目发布到Tomcat,或Deployment Assembly中WebContent路径配置错误,导致静态资源未被识别。
解决:右键项目 → Run As → Run on Server → 选择Tomcat → Finish;再检查Deployment Assembly中WebContent是否映射到/。
现象2:登录时用户名密码正确,却跳回登录页且无报错
原因:spring-servlet.xml中<mvc:annotation-driven />缺失,导致@RequestMapping注解不生效,请求没路由到Controller。
解决:在spring-servlet.xml的<beans>根节点内,确认存在<mvc:annotation-driven />,且已引入命名空间xmlns:mvc="http://www.springframework.org/schema/mvc"。
现象3:入库成功,但库存表goods_stock没更新
原因:InStockServiceImpl类上缺少@Service注解,或spring-servlet.xml中<context:component-scan>未扫描到该包。
解决:检查InStockServiceImpl.java第一行是否有@Service("inStockService");检查spring-servlet.xml中<context:component-scan base-package="com.service.impl" />是否正确。
现象4:查询物资时中文显示为??
原因:MySQL连接URL未指定字符集,如jdbc:mysql://localhost:3306/warehouse_db缺少?useUnicode=true&characterEncoding=utf8mb4。
解决:修改src/jdbc.properties中jdbc.url为:jdbc:mysql://localhost:3306/warehouse_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8。
现象5:添加物资时,物资类别下拉框为空
原因:GoodsController.listCategories()方法未被调用,或categoryMapper.selectAll()返回空集合,通常因category表无初始数据。
解决:手动向category表插入测试数据:INSERT INTO category VALUES (1,'电子元件'),(2,'机械配件'),(3,'耗材');。
4. 数据库设计与SQL脚本精读:从ER图到goods.sql的每一行真相
4.1 核心表ER关系:5张表撑起整个业务骨架
项目虽小,但数据库设计遵循第三范式。核心表共5张,关系清晰:
| 表名 | 主要字段 | 关系说明 |
|---|---|---|
user | id,username,password,role | 系统用户,role区分管理员(1)和仓管员(2) |
category | id,name | 物资分类,goods表通过category_id外键关联 |
goods | id,name,spec,category_id,unit | 物资主表,spec存规格型号(如“DC12V/2A”) |
goods_stock | id,goods_id,quantity,last_update_time | 实时库存快照,goods_id唯一索引 |
instock | id,goods_id,quantity,operator,batch_no,create_time | 入库单流水,batch_no支持批次追溯 |
注意:
goods_stock表没有用触发器自动更新,而是靠Service层显式维护——这是SSM项目可控性的体现,避免黑匣子逻辑。
4.2goods.sql关键片段解析:建表语句里的设计意图
打开goods.sql,前几行建表语句透露重要信息:
-- 创建goods表(物资主表) CREATE TABLE `goods` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '物资名称', `spec` varchar(200) DEFAULT NULL COMMENT '规格型号', `category_id` int(11) NOT NULL COMMENT '分类ID', `unit` varchar(20) NOT NULL COMMENT '单位,如:个、箱、米', PRIMARY KEY (`id`), KEY `fk_category_id` (`category_id`), CONSTRAINT `fk_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;逻辑说明:
COMMENT字段不是摆设,它直接对应JSP页面上的表单label(如unit的注释“单位,如:个、箱、米”,前端就用它生成下拉选项)。FOREIGN KEY约束确保category_id必须存在于category表中,防止脏数据。ENGINE=InnoDB是必须的,因为MyBatis事务依赖InnoDB的行级锁。
4.3 库存一致性保障:为什么不用UPDATE goods_stock SET quantity = quantity + ??
你可能会想:更新库存直接写UPDATE goods_stock SET quantity = quantity + #{quantity} WHERE goods_id = #{goodsId}不更简单?但项目用了“先查后更”的方式,原因有二:
- 空记录处理:若某物资首次入库,
goods_stock表中无对应goods_id记录,UPDATE会静默失败(影响行为为0),而SELECT能捕获null,进而走INSERT分支。 - 业务扩展性:未来若需记录“上次入库时间”、“最后操作人”,
UPDATE ... SET无法同时更新多个字段而不查原值。
// 对比两种写法 // 方式A(项目采用):安全、可扩展 GoodsStock stock = goodsStockMapper.selectByGoodsId(goodsId); if (stock == null) { // INSERT } else { stock.setQuantity(stock.getQuantity() + quantity); goodsStockMapper.updateStock(stock); // UPDATE with full object } // 方式B(危险):简洁但脆弱 int rows = goodsStockMapper.updateQuantityByGoodsId(goodsId, quantity); // UPDATE ... SET quantity = quantity + ? if (rows == 0) { // 必须额外处理“记录不存在”场景,否则库存丢失 }4.4 SQL脚本执行后必查的3个数据点
脚本执行完毕,别急着启动,先用MySQL客户端验证:
-- 1. 检查用户表是否有admin(密码123456) SELECT username, password, role FROM user WHERE username = 'admin'; -- 2. 检查分类表是否至少有1条数据(否则物资添加时下拉为空) SELECT COUNT(*) FROM category; -- 3. 检查库存表是否为空(正常,首次无库存) SELECT COUNT(*) FROM goods_stock;提示:若
category表为空,GoodsController.listCategories()返回空List,JSP中<c:forEach items="${categories}" var="c">不执行,下拉框自然为空。这是新手最常忽略的“数据准备”环节。
5. 二次开发实战:从密码加密到Excel导出,3个高价值改造方案
5.1 改造1:登录密码BCrypt加密(5分钟上线,毕设加分项)
明文密码是毕设硬伤。集成BCrypt只需3步:
Step 1:添加依赖
在WebContent/WEB-INF/lib/中放入spring-security-crypto-4.2.20.RELEASE.jar(与Spring 4.x兼容)。
Step 2:修改User实体类
在com.entity.User.java中,增加passwordSalt字段(可选),并重写setPassword方法:
// com/entity/User.java private String passwordSalt; // 存储盐值,用于验证 public void setPassword(String rawPassword) { // 生成BCrypt密码(强度10,足够毕设) BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); this.password = encoder.encode(rawPassword); }Step 3:修改登录验证逻辑
在UserController.login()中,替换密码比对:
// 原逻辑(明文比对) // if (!user.getPassword().equals(password)) { ... } // 新逻辑(BCrypt验证) BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); if (!encoder.matches(password, user.getPassword())) { model.addAttribute("msg", "用户名或密码错误!"); return "login"; }验证方法:用
BCryptPasswordEncoder.encode("123456")生成密文,替换goods.sql中user表的password字段值。重启后用原密码登录,依然成功。
5.2 改造2:物资列表导出Excel(Apache POI,15行代码搞定)
毕设常被问“数据怎么导出?”。用Apache POI生成Excel,无需模板:
Step 1:添加POI依赖
放入poi-3.17.jar、poi-ooxml-3.17.jar、poi-scratchpad-3.17.jar到lib/。
Step 2:新增导出Controller方法
在GoodsController.java中添加:
@RequestMapping("/export") public void exportGoods(HttpServletResponse response) throws Exception { List<Goods> goodsList = goodsService.getAllGoods(); // 创建Excel工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("物资列表"); // 表头 String[] headers = {"ID", "名称", "规格", "分类", "单位"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum = 1; for (Goods g : goodsList) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(g.getId()); row.createCell(1).setCellValue(g.getName()); row.createCell(2).setCellValue(g.getSpec() == null ? "" : g.getSpec()); row.createCell(3).setCellValue(g.getCategoryName()); // 假设Service已查出分类名 row.createCell(4).setCellValue(g.getUnit()); } // 输出响应 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=goods_export.xlsx"); workbook.write(response.getOutputStream()); }验证:访问
http://localhost:8080/warehouse/goods/export,浏览器自动下载Excel文件。注意:getCategoryName()需在GoodsService.getAllGoods()中通过JOIN查出分类名,而非仅ID。
5.3 改造3:库存预警功能(阈值配置+前端标红,让系统“说话”)
当库存低于设定值时,列表中该行背景变红。这是体现“智能”的低成本方案:
Step 1:在goods表加预警字段
ALTER TABLE goods ADD COLUMN warning_threshold INT DEFAULT 10 COMMENT '库存预警阈值';Step 2:修改GoodsStockMapper.xml,JOIN查出预警值
<select id="selectStockWithWarning" resultType="com.entity.GoodsStockWithWarning"> SELECT s.*, g.warning_threshold FROM goods_stock s LEFT JOIN goods g ON s.goods_id = g.id </select>Step 3:JSP中动态渲染样式
在goods/list.jsp的<c:forEach>循环内:
<tr <c:if test="${stock.quantity <= stock.warningThreshold}">style="background-color:#ffebee;"</c:if>> <td>${stock.goodsId}</td> <td>${stock.goodsName}</td> <td>${stock.quantity}</td> <td><c:if test="${stock.quantity <= stock.warningThreshold}">⚠️ 库存不足!</c:if></td> </tr>从那以后我每次给学生讲毕设,都强制走一遍“密码加密+Excel导出+预警标红”三件套。不是为了炫技,而是让学生亲手触摸到:所谓“系统”,就是把一个个小需求,用确定的代码逻辑,稳稳地焊死在架构上。希望帮到你。
本文还有配套的精品资源,点击获取