简介:这份资源是面向计算机专业应届生与Java初学者的一套毕业设计完整方案,主题为基于SSM框架(Spring + SpringMVC + MyBatis)与JSP技术实现的实验室耗材管理系统,可用于毕设选题参考、课程设计或SSM入门实战。系统围绕耗材入库、出库、库存查询与统计报表等核心业务展开,并支持多用户并发操作与基于角色的权限分配,兼顾数据安全与协作效率,前端通过JSP页面呈现,具备良好的交互体验与浏览器兼容性。压缩包为rar格式,整体约24.61MB,内含毕业论文与配套源代码,文件类型以源码、文档等为主,便于直接部署运行与二次开发。目前已有71人学习下载,读者可借此获得一套结构完整的赛题级项目方案,理解SSM三层架构的整合思路、数据库持久化实现与权限控制设计,并在此基础上按需扩展功能模块,适配不同实验室的个性化管理需求。
1. 从一份 .rar 说起:SSM 实验室耗材管理系统到底在管什么
实验室里最容易被忽略的成本,不是仪器,是耗材。一支移液枪枪头、一盒离心管、一瓶色谱纯乙腈,领用的时候没人记账,月底盘点永远对不上。我带过的一个本科毕设小组就卡在这个场景上:导师要求做一套能跑起来的实验室耗材管理系统,技术栈限定 SSM(Spring + SpringMVC + MyBatis)+ JSP,最后交一份论文加源码压缩包。这个标题里的「基于 SSM 框架开发的实验室耗材管理系统设计与实现 + jsp + 毕业论文(附源代码)」,本质就是一套典型的 JavaWeb 毕业设计交付物:后端用 SSM 三层架构,前端用 JSP 渲染页面,数据库存耗材台账、库存、领用记录和用户权限。
它解决的问题很具体——把耗材从入库、领用、退库到低库存预警的全流程搬到 Web 端,替代 Excel 手工台账。适合谁?正在做 Java 方向毕业设计、需要一套结构完整可答辩的系统、同时想借这个项目把 SSM 和 JSP 真正跑通一次的学生和初级开发者。别把它当成玩具,SSM 的配置链路和 JSP 的页面交互,恰恰是很多人「学过但没串起来」的那一段。下面我按实际落地顺序,把选型、建库、编码、避坑和进阶一条条拆开讲。
2. 为什么毕设还选 SSM + JSP:选型理由与最小可跑环境
2.1 SSM 三层在耗材管理里的职责划分
SSM 不是一个框架,是三个东西拼起来的组合。Spring 管 Bean 和事务,SpringMVC 管请求分发和视图解析,MyBatis 管 SQL 映射。放到耗材管理系统里,职责边界很清楚:Controller 层接收 JSP 表单提交的领用申请,Service 层做库存扣减和事务控制,Dao 层(Mapper 接口)执行具体的增删改查。耗材领用这个动作必须在一个事务里完成「插入领用记录 + 更新库存数量」两步,否则会出现记录写了但库存没扣的脏数据,这正是 Spring 声明式事务要兜住的地方。
为什么毕设场景下 SSM 比 Spring Boot 更合适?不是技术先进性问题,是答辩可控性问题。Spring Boot 自动装配把很多配置藏起来了,答辩老师问「事务是怎么配的」「Mapper 是怎么被扫描到的」,用 SSM 你能指着 XML 一行行讲清楚,用 Spring Boot 反而说不清自动配置背后发生了什么。JSP 同理,虽然现在企业里前后端分离是主流,但 JSP 的 EL 表达式和 JSTL 标签能让你在一个页面里同时看到数据绑定和循环渲染,对理解 MVC 请求响应链路更直观。
2.2 本地跑通的最小环境与依赖版本
环境这块翻车最多。我一般固定一套组合:JDK 8(不要上 11 以上,老版本 Tomcat 和 JSP 编译容易出问题)、Maven 3.6、Tomcat 8.5、MySQL 5.7。JDK 版本是血泪经验,用 JDK 17 跑老 SSM 项目,JSP 编译报Unable to compile class for JSP的概率极高。
pom.xml里核心依赖如下,版本号我按稳定组合给:
<!-- Spring 核心,四个模块版本必须一致 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.20</version> </dependency> <!-- MyBatis 与 Spring 整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.9</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- 数据库驱动与连接池 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <!-- JSP 相关,scope 必须是 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>逻辑说明:Spring 四个模块版本必须锁死一致,混版本会出现NoSuchMethodError。javax.servlet-api的 scope 设成 provided,因为 Tomcat 自带这个包,打进 war 会冲突。JSTL 是 JSP 页面用<c:forEach>循环渲染耗材列表的前提,漏了它页面直接报标签未识别。
参数说明:MySQL 驱动 5.1.49 对应 MySQL 5.7,如果你本地是 MySQL 8,驱动要换8.0.28且 URL 要加serverTimezone=Asia/Shanghai,否则连接报时区错误。Druid 连接池的初始连接数、最大连接数在applicationContext.xml里配,毕设场景 initialSize 设 5、maxActive 设 20 足够。
3. 数据库设计与 MyBatis 映射:耗材台账怎么建表
3.1 四张核心表与字段设计
耗材管理系统的数据模型不复杂,但字段设计有讲究。核心四张表:用户表sys_user、耗材信息表consumable、入库记录表stock_in、领用记录表stock_out。库存数量不单独建表,直接放在consumable表里用stock字段维护,领用和入库都去更新这个字段。
CREATE TABLE `consumable` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '耗材名称', `spec` VARCHAR(100) COMMENT '规格型号', `unit` VARCHAR(20) COMMENT '计量单位', `stock` INT DEFAULT 0 COMMENT '当前库存', `warn_line` INT DEFAULT 10 COMMENT '低库存预警线', `category` VARCHAR(50) COMMENT '分类', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `stock_out` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `consumable_id` INT NOT NULL, `user_id` INT NOT NULL, `quantity` INT NOT NULL COMMENT '领用数量', `out_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `remark` VARCHAR(255), INDEX idx_consumable (`consumable_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:consumable表的stock字段是库存的唯一真相来源,所有领用入库都围绕它做加减。warn_line是预警线,查询时用stock < warn_line就能筛出需要补货的耗材。stock_out表加consumable_id索引,因为按耗材查领用历史是高频操作。
参数说明:字符集用utf8mb4而不是utf8,因为耗材名称里可能出现特殊符号。stock字段用 INT 不用无符号,因为退库逻辑里可能出现临时负数需要排查,无符号会直接报错掩盖问题。预警线默认 10,实际按耗材周转速度调,枪头这种高频的可以设 50。
3.2 Mapper XML 里的动态 SQL 与库存扣减
MyBatis 的价值在动态 SQL。耗材列表查询要支持按名称模糊、按分类筛选、按库存状态过滤,用<if>拼条件最省事。
<select id="selectByCondition" resultType="com.lab.entity.Consumable"> SELECT * FROM consumable <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="category != null and category != ''"> AND category = #{category} </if> <if test="onlyWarn != null and onlyWarn == true"> AND stock < warn_line </if> </where> ORDER BY create_time DESC </select> <update id="reduceStock"> UPDATE consumable SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity} </update>逻辑说明:<where>标签自动处理第一个条件前的 AND,避免手写WHERE 1=1的丑写法。reduceStock的WHERE里带stock >= #{quantity}是关键,这是乐观锁思路——库存不足时更新影响行数为 0,Service 层据此判断领用失败并回滚事务,防止超领。
参数说明:#{name}是预编译占位符,防 SQL 注入,别用${}。onlyWarn是布尔参数,JSP 页面勾选「只看预警」时传 true。reduceStock返回 int,Service 里判断if (rows == 0) throw new RuntimeException("库存不足"),触发 Spring 事务回滚。
4. JSP 页面与 SSM 联调:从表单提交到列表渲染
4.1 领用表单的 JSP 写法与 EL 表达式
JSP 页面是这套系统的门面,也是答辩演示时老师看得最仔细的地方。领用页面要展示耗材下拉框、数量输入框、领用人信息,提交后走 Controller。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <form action="${pageContext.request.contextPath}/stock/out" method="post"> <select name="consumableId"> <c:forEach items="${consumableList}" var="item"> <option value="${item.id}"> ${item.name}(库存:${item.stock}${item.unit}) </option> </c:forEach> </select> <input type="number" name="quantity" min="1" required/> <input type="text" name="remark" placeholder="用途备注"/> <button type="submit">提交领用</button> </form>逻辑说明:${pageContext.request.contextPath}拿项目根路径,避免部署路径变化导致表单 404。<c:forEach>遍历 Controller 放进 Model 的consumableList,把每个耗材渲染成下拉选项,库存直接显示在选项文字里,领用人一眼能看到余量。
参数说明:name="consumableId"必须和 Controller 方法参数名或实体属性名对应,否则接收为 null。min="1"是前端兜底,后端仍要校验,前端校验永远不可信。required属性让浏览器拦截空提交,减少无效请求。
4.2 Controller 接收参数与事务边界
Controller 只做参数接收和视图跳转,业务逻辑全在 Service,这是 SSM 分层的底线。
@Controller @RequestMapping("/stock") public class StockController { @Autowired private StockService stockService; @PostMapping("/out") public String stockOut(@RequestParam Integer consumableId, @RequestParam Integer quantity, @RequestParam(required = false) String remark, HttpSession session) { User user = (User) session.getAttribute("loginUser"); if (user == null) { return "redirect:/login"; } try { stockService.stockOut(consumableId, user.getId(), quantity, remark); } catch (RuntimeException e) { return "redirect:/stock/list?error=stock"; } return "redirect:/stock/list"; } }逻辑说明:从 session 取登录用户,没登录直接踢回登录页,这是最朴素的权限拦截。Service 抛出的库存不足异常在这里捕获,重定向回列表页并带错误标记,JSP 页面根据error参数提示用户。
参数说明:@RequestParam(required = false)让备注可选。重定向用redirect:前缀,避免刷新页面重复提交表单。真实项目里权限会用拦截器统一处理,毕设阶段在 Controller 里判断也能接受,但论文里最好提一句拦截器方案。
Service 层的事务注解是核心:
@Service public class StockServiceImpl implements StockService { @Autowired private ConsumableMapper consumableMapper; @Autowired private StockOutMapper stockOutMapper; @Override @Transactional(rollbackFor = Exception.class) public void stockOut(Integer consumableId, Integer userId, Integer quantity, String remark) { int rows = consumableMapper.reduceStock(consumableId, quantity); if (rows == 0) { throw new RuntimeException("库存不足,领用失败"); } StockOut record = new StockOut(); record.setConsumableId(consumableId); record.setUserId(userId); record.setQuantity(quantity); record.setRemark(remark); stockOutMapper.insert(record); } }逻辑说明:@Transactional保证扣库存和插记录要么都成功要么都回滚。rollbackFor = Exception.class让受检异常也回滚,默认只回滚 RuntimeException,这个坑很多人踩过。先扣库存再插记录,扣减失败直接抛异常,不会产生孤儿记录。
参数说明:rows == 0是库存不足的信号,因为reduceStock的 WHERE 条件带了库存判断。rollbackFor建议统一写Exception.class,别省。
5. 避坑与排查:SSM + JSP 毕设最容易翻车的 5 个点
5.1 中文乱码:POST 和 GET 要分开治
现象:JSP 表单提交的中文耗材名称存进数据库变成问号,或者页面显示乱码。
原因:POST 请求体编码由request.setCharacterEncoding("UTF-8")控制,GET 请求参数编码由 Tomcat 的server.xml里 Connector 的URIEncoding控制,两者不是一回事。
解决:在web.xml里配 Spring 的CharacterEncodingFilter,强制 POST 用 UTF-8;Tomcat 的server.xml里 Connector 加URIEncoding="UTF-8"。数据库连接 URL 加useUnicode=true&characterEncoding=utf8。三处都配齐才彻底。
5.2 Mapper 扫描不到:NoSuchBeanDefinitionException
现象:启动报No qualifying bean of type 'xxxMapper',或者调用时NullPointerException。
原因:Mapper 接口没被 MyBatis 的MapperScannerConfigurer扫到,或者applicationContext.xml和spring-mvc.xml的扫描包路径重叠导致 Bean 重复注册。
解决:在applicationContext.xml里配<mybatis:scan base-package="com.lab.mapper"/>,确保包路径写对。同时把 Service 和 Controller 的组件扫描分开:applicationContext.xml只扫 Service,spring-mvc.xml只扫 Controller,避免事务失效。
5.3 事务不生效:库存扣了记录没插
现象:领用后库存减少了,但领用记录表里没有对应数据,或者反过来。
原因:@Transactional加在了 Controller 上而不是 Service 上,或者 Service 类没被 Spring 管理(自己 new 出来的),或者异常被 catch 后没重新抛出。
解决:事务注解只加在 Service 实现类的方法上。确保 Service 通过@Autowired注入而不是 new。catch 块里如果要吞异常,必须手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),否则事务照样提交。
5.4 JSP 页面 404:视图解析器前缀后缀配错
现象:Controller 返回"stock/list",浏览器报 404。
原因:spring-mvc.xml里InternalResourceViewResolver的prefix和suffix配错,或者 JSP 文件没放在WEB-INF下对应目录。
解决:配prefix为/WEB-INF/jsp/,suffix为.jsp,那么返回"stock/list"实际找的是/WEB-INF/jsp/stock/list.jsp。JSP 放WEB-INF下是安全做法,防止直接 URL 访问绕过 Controller。
5.5 低库存预警不触发:SQL 比较条件写反
现象:库存明明低于预警线,预警列表却是空的。
原因:查询条件写成stock > warn_line,或者warn_line字段有 null 值导致比较结果为 null。
解决:条件必须是stock < warn_line。建表时warn_line给默认值 10 并设 NOT NULL,避免 null 参与比较。测试时手动把某耗材库存改成 5、预警线 10,验证预警列表能查出来。
6. 让这套系统在答辩里站住脚:两个进阶技巧
第一个技巧是把低库存预警做成登录后的首页提醒。很多人只做了预警查询页面,但答辩老师不会主动去点。在登录成功的 Controller 里查一次stock < warn_line的耗材数量,放进 session,JSP 首页顶部用<c:if>判断数量大于 0 就显示一条红色提示条。这个改动不到二十行代码,但演示效果直接拉满,老师一登录就看到系统在「主动工作」。
// 登录成功后 int warnCount = consumableMapper.countWarn(); session.setAttribute("warnCount", warnCount);<c:if test="${warnCount > 0}"> <div class="warn-bar"> 当前有 ${warnCount} 种耗材低于预警线,请及时补货 </div> </c:if>逻辑说明:countWarn是一条SELECT COUNT(*) FROM consumable WHERE stock < warn_line。session 里存数量而不是列表,避免 session 过大。JSP 用<c:if>控制提示条显隐,数量为 0 时不渲染。
参数说明:warnCount在每次领用和入库后应该刷新,简单做法是在这两个操作的重定向目标里重新查一次。别用定时任务,毕设场景没必要。
第二个技巧是给论文里的「系统测试」章节准备可复现的测试数据。别用「测试耗材1、测试耗材2」这种名字,用真实的实验室耗材名:移液枪枪头、离心管、EP 管、色谱纯乙腈。测试用例写成表格,每条包含操作步骤、预期结果、实际结果。比如「领用数量超过库存」这条,预期是提示库存不足且库存不变,实际结果截图放论文里。答辩老师看的是你有没有验证边界,不是系统有多复杂。
| 测试项 | 操作 | 预期结果 |
|---|---|---|
| 正常领用 | 领用库存充足的耗材 5 个 | 库存减 5,记录新增 |
| 超量领用 | 领用数量大于当前库存 | 提示库存不足,库存不变 |
| 低库存预警 | 库存改为低于预警线 | 首页显示预警提示 |
| 未登录访问 | 直接访问领用接口 | 跳转登录页 |
这套系统我前后带过几届学生,最大的体会是:别在框架版本上追新,JDK 8 + SSM 5.3 + Tomcat 8.5 这套老组合反而最稳,能把精力留给业务逻辑和论文。答辩前一定把「库存不足」和「未登录」两条路径亲手走一遍,这两个是老师最爱问的边界。希望帮到你。
本文还有配套的精品资源,点击获取