SSM实验室耗材管理系统毕业设计:从表设计到事务控制的完整实战

发布时间:2026/10/5 6:14:26
SSM实验室耗材管理系统毕业设计:从表设计到事务控制的完整实战
简介这份资源是面向计算机专业应届生与Java初学者的一套毕业设计完整方案主题为基于SSM框架SpringSpringMVCMyBatis与JSP技术实现的实验室耗材管理系统可用于毕设选题参考、课程设计或SSM入门练手。系统围绕耗材入库、出库、库存查询与统计报表等核心业务展开并涉及多用户并发操作、角色权限分配等进阶设计前端通过JSP页面完成视图展示与数据交互整体结构清晰、便于二次开发与功能扩展。压缩包为rar格式体积约24.61MB内含毕业论文与配套源代码可帮助读者对照论文理解需求分析、数据库设计与各层代码实现快速搭建可运行项目并梳理SSM整合思路。目前已有71人学习下载适合需要完整赛题方案与源码参考的读者。1. 从一份 SSM 实验室耗材管理系统说起毕业设计选题里被低估的“真业务”实验室耗材管理这件事听起来像是“又一个增删改查”但真到实验室里跑一圈你会发现它比大多数毕业设计选题都更接近真实业务。试剂有批次、有有效期、有危化品属性耗材有库存下限、有领用审批、有归还与报废一个课题组领走两盒枪头背后牵扯的是库存扣减、领用记录、责任人追溯和低库存预警。用 SSMSpring SpringMVC MyBatis加 JSP 做一套实验室耗材管理系统恰好能把 Java Web 的经典分层、事务控制、权限拦截、分页查询这些核心能力全部串起来这也是它常年出现在“计算机毕业设计选题”和“基于 Java 的毕业设计选题”榜单里的原因。这篇文章面向三类人正在为毕业设计选题发愁、想找一个业务真实又不至于做不完的本科生已经选了 SSM 方向、但卡在“表怎么设计、事务怎么加、JSP 页面怎么和后台对接”的开发者以及想拿一个完整项目练手 Java Web 全流程的入门者。我会按“业务拆解 → 环境搭建 → 数据库与核心代码 → 避坑 → 进阶技巧”的顺序把一套能跑通、能答辩、能讲清楚技术点的方案讲透。SSM 不是新技术但它的分层思想和事务模型放到今天依然是理解 Java 后端的地基这一点在“java 面试题”里也反复被考。2. 需求拆解与 SSM 分层耗材管理系统到底要建哪几张表2.1 先想清楚业务角色再谈技术选型耗材管理系统的角色通常分三种实验室管理员、普通教师/研究员、学生。管理员负责耗材入库、修改库存、审批领用、查看报表教师可以提交领用申请、查看自己课题组的领用历史学生一般只能查看可领用耗材和提交申请。这个角色划分直接决定了权限拦截怎么做——SSM 里常见做法是用 SpringMVC 拦截器配合 Session 中的用户角色做判断而不是把权限逻辑散落在每个 Controller 里。选 SSM 而不是 Spring Boot毕业设计的语境下有两个现实理由。第一SSM 的 XML 配置虽然繁琐但能让你在答辩时讲清楚“Spring 的 IOC 容器是怎么装配 Bean 的”“MyBatis 的 Mapper 接口是怎么被扫描成代理对象的”这些是面试高频点。第二很多学校的实验环境 JDK 版本偏旧SSM 对 JDK 8 的兼容性更稳不像某些新框架对版本挑剔。当然如果你时间充裕用 Spring Boot 简化配置也完全可以但本文锁定 SSM因为标题写的就是它。核心业务实体有五个用户user、耗材分类category、耗材consumable、入库记录stock_in、领用记录stock_out。耗材表里要存当前库存、库存下限、有效期、存放位置领用记录表要存领用人、领用数量、领用时间、审批状态。这里有个容易忽略的点库存扣减不能只改耗材表的 stock 字段必须同时写一条领用记录这两步要在同一个事务里否则会出现“库存扣了但没记录”的对不上账。2.2 数据库表设计与建表 SQL下面是我一般会用的建表语句字段命名用下划线风格方便 MyBatis 映射。注意consumable表的stock和min_stock用 int有效期用 date危化品标识用 tinyint。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 2 COMMENT 1管理员 2教师 3学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE consumable ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, spec VARCHAR(100) COMMENT 规格, unit VARCHAR(20) COMMENT 单位, stock INT NOT NULL DEFAULT 0, min_stock INT NOT NULL DEFAULT 5, expire_date DATE, location VARCHAR(100) COMMENT 存放位置, is_hazard TINYINT DEFAULT 0 COMMENT 是否危化品, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_out ( id INT PRIMARY KEY AUTO_INCREMENT, consumable_id INT NOT NULL, user_id INT NOT NULL, quantity INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待审批 1已通过 2已驳回, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, approve_time DATETIME, remark VARCHAR(255) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有两个参数值得说明。stock和min_stock都用 int 而不是 decimal因为耗材按件/盒/瓶领用不存在小数。status用 tinyint 而不是 varchar是为了后续用WHERE status 1做索引查询时效率更高。expire_date单独存日期方便做“临期耗材预警”这个加分功能。分类表和入库记录表结构类似这里不重复贴核心是stock_out表必须和consumable表通过consumable_id关联审批通过时才真正扣库存。2.3 SSM 三层怎么分包名怎么起包结构建议按controller、service、service.impl、mapper、entity、interceptor、util来分。Controller 只做参数接收和视图返回Service 写业务逻辑和事务注解Mapper 只写 SQL 映射。很多同学把事务写在 Controller 里这是典型的翻车点——Spring 的Transactional在 Controller 上虽然能生效但一旦你在 Service 里调用另一个 Service 方法事务传播行为会变得难以预测。正确做法是事务边界放在 Service 层。MyBatis 的 Mapper 接口用Mapper注解或者在 Spring 配置里开MapperScannerConfigurer扫描。XML 映射文件放在resources/mapper下namespace指向 Mapper 接口全限定名。这里有个细节如果 Mapper 接口方法有多个参数XML 里要用Param注解指定名称否则 MyBatis 会按arg0、arg1或param1、param2来命名容易在动态 SQL 里写错。3. 从零搭起 SSM 环境依赖、配置与第一个能跑的查询3.1 Maven 依赖与版本选择依赖版本不要盲目追新SSM 这套组合里 Spring 5.x 配 MyBatis 3.5.x 是最稳的。下面是我常用的 pom 片段JDK 编译级别设 1.8。properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies 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-spring/artifactId version2.1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies参数说明spring-webmvc负责 DispatcherServlet 和注解驱动mybatis-spring是 MyBatis 和 Spring 的粘合包版本必须和 MyBatis 主版本匹配druid做连接池比默认的 DriverManagerDataSource 更适合生产jstl是 JSP 页面用c:forEach标签的前提漏了它页面会报NoClassDefFoundError。servlet-api的 scope 必须是 provided否则打包成 war 时会和 Tomcat 自带的冲突。3.2 Spring 与 MyBatis 的 XML 配置要点applicationContext.xml里要配数据源、SqlSessionFactory、Mapper 扫描和事务管理器。关键片段如下bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lab_consumable?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.lab.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.lab.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/url里的serverTimezoneAsia/Shanghai是 MySQL 8 的必填项不写会报时区错误。mapperLocations用classpath:mapper/*.xml表示扫描类路径下 mapper 目录所有 XML。typeAliasesPackage让实体类在 XML 里可以直接写类名而不用全限定名。tx:annotation-driven开启注解事务这样 Service 方法上的Transactional才会生效。注意MapperScannerConfigurer和dataSource不要放在同一个配置类里用Value注入否则会出现扫描顺序问题这是很多教程没讲的坑。3.3 写第一个查询耗材列表分页分页是耗材管理系统里出现频率最高的功能。MyBatis 分页有两种做法一是用LIMIT手动分页二是用 PageHelper 插件。毕业设计里我建议手动写因为答辩时能讲清楚LIMIT offset, size的计算逻辑。Mapper 接口和 XML 如下public interface ConsumableMapper { ListConsumable selectByPage(Param(keyword) String keyword, Param(offset) int offset, Param(size) int size); int countByKeyword(Param(keyword) String keyword); }select idselectByPage resultTypeConsumable SELECT * FROM consumable where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY id DESC LIMIT #{offset}, #{size} /select select idcountByKeyword resultTypeint SELECT COUNT(*) FROM consumable where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) /if /where /select逻辑说明Param注解让 XML 里能直接用keyword、offset、size三个名字。where标签会自动处理第一个条件前的AND避免手写WHERE 11。CONCAT(%, #{keyword}, %)用#{}而不是${}前者是预编译占位符能防 SQL 注入后者是字符串拼接千万别用。Service 层计算 offset 的公式是(pageNum - 1) * pageSizepageNum 从 1 开始。Controller 接收pageNum和pageSize两个参数默认值设 1 和 10然后调用 Service 返回一个包含列表和总条数的 Map 给 JSP。4. 核心业务落地领用审批、库存扣减与事务控制4.1 领用申请到审批通过的完整链路领用流程分两步用户提交申请管理员审批。提交时只写stock_out表status 为 0不动库存。审批通过时先查该耗材当前库存是否足够足够则扣减consumable.stock同时把stock_out.status改为 1记录approve_time。这两步必须在同一个事务里。Service 方法如下Service public class StockOutServiceImpl implements StockOutService { Autowired private StockOutMapper stockOutMapper; Autowired private ConsumableMapper consumableMapper; Override Transactional(rollbackFor Exception.class) public void approve(Integer stockOutId, Integer adminId) { StockOut record stockOutMapper.selectById(stockOutId); if (record null || record.getStatus() ! 0) { throw new RuntimeException(申请不存在或已处理); } Consumable c consumableMapper.selectById(record.getConsumableId()); if (c.getStock() record.getQuantity()) { throw new RuntimeException(库存不足当前库存 c.getStock()); } // 扣减库存用乐观锁防止并发超卖 int rows consumableMapper.reduceStock(c.getId(), record.getQuantity(), c.getStock()); if (rows 0) { throw new RuntimeException(库存已被其他操作修改请刷新重试); } stockOutMapper.updateStatus(stockOutId, 1, new Date()); } }参数说明Transactional(rollbackFor Exception.class)里的rollbackFor必须写因为 Spring 默认只对RuntimeException回滚如果抛出的是受检异常不会回滚。reduceStock的 SQL 用乐观锁UPDATE consumable SET stock stock - #{qty} WHERE id #{id} AND stock #{oldStock}第三个参数传入查询时的旧库存值如果返回影响行数为 0说明在查询和更新之间有其他事务改了库存此时抛异常回滚。这是防止并发超卖的关键比synchronized更可靠因为后者在集群环境下失效。4.2 库存预警与临期提醒怎么查库存预警的 SQL 很简单SELECT * FROM consumable WHERE stock min_stock。临期提醒用WHERE expire_date IS NOT NULL AND expire_date DATE_ADD(CURDATE(), INTERVAL 30 DAY)表示 30 天内到期。这两个查询可以合并成一个“预警列表”接口在管理员首页用 JSP 的c:forEach渲染成表格。注意expire_date为 null 的耗材不应该出现在临期列表里所以IS NOT NULL不能省。JSP 页面渲染时日期格式用fmt:formatDate value${item.expireDate} patternyyyy-MM-dd/需要引入% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %。如果日期显示为英文格式检查是否漏了 fmt 标签库。库存低于下限的行可以用c:if test${item.stock item.minStock}加红色样式这是答辩时容易被问到的“前端如何体现业务规则”的加分点。4.3 权限拦截器与登录态管理登录成功后把 User 对象放进session.setAttribute(currentUser, user)。拦截器实现HandlerInterceptor在preHandle里判断 session 是否有 currentUser没有则重定向到登录页。对于管理员专属接口再判断user.getRole() 1不是则返回 403 页面。拦截器配置在 SpringMVC 的 XML 里mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.lab.interceptor.AuthInterceptor/ /mvc:interceptor /mvc:interceptorsexclude-mapping必须排除登录接口和静态资源路径否则登录页的 CSS、JS 都会被拦截导致页面样式全丢。这是新手最常见的翻车现场之一。另外拦截器里重定向用response.sendRedirect(request.getContextPath() /login)不要漏掉 contextPath否则部署到 Tomcat 的非根路径下会 404。5. 避坑与排查SSM 项目里最容易翻车的 5 个点5.1 中文乱码现象是页面显示问号原因是字符编码没统一现象JSP 页面提交的中文耗材名称存进数据库变成???或者查询出来显示乱码。原因通常有三处数据库连接 URL 没加characterEncodingutf8web.xml 没配CharacterEncodingFilter或者 JSP 页面pageEncoding写成了 ISO-8859-1。解决URL 加useUnicodetruecharacterEncodingutf8web.xml 里配 Spring 的CharacterEncodingFilterforceEncoding设为 trueJSP 头部统一写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。三处都对了才不会乱码。5.2 事务不生效现象是库存扣了但状态没改原因是注解没被代理现象审批通过后库存扣减成功但stock_out.status还是 0或者抛异常后库存没回滚。原因通常是Transactional写在了 Controller 上或者 Service 类没有被 Spring 扫描到或者方法不是 public。解决确认applicationContext.xml里开了tx:annotation-driven确认 Service 实现类在component-scan的包路径下确认注解方法用 public 修饰。还有一个隐蔽原因如果 Service 类实现了接口Spring 默认用 JDK 动态代理此时注解要写在接口方法上或改用 CGLIB 代理proxy-target-classtrue。5.3 MyBatis 映射失败现象是查出来字段为 null原因是列名和属性名没对上现象SELECT * FROM consumable查出来的expireDate是 null但数据库里明明有值。原因是数据库列名expire_date和 Java 属性expireDate不匹配MyBatis 默认不会自动转下划线到驼峰。解决在applicationContext.xml的 SqlSessionFactory 里加property nameconfiguration配置mapUnderscoreToCamelCasetrue或者在 XML 里手动写resultMap做映射。前者更省事但要注意它只对下划线转驼峰有效像is_hazard转isHazard也能生效。5.4 JSP 页面 404现象是 Controller 返回视图名后找不到页面原因是视图解析器前缀后缀配错现象访问/consumable/list后报 404控制台没有异常。原因是 SpringMVC 的InternalResourceViewResolver配置的prefix和suffix与 JSP 实际存放路径不一致。解决检查配置常见写法是prefix为/WEB-INF/jsp/suffix为.jsp那么 Controller 返回consumable/list时实际找的是/WEB-INF/jsp/consumable/list.jsp。如果 JSP 放在webapp根目录下而不是WEB-INF下前缀就要改成/。另外WEB-INF下的 JSP 不能被浏览器直接访问这是安全设计不是 bug。5.5 连接池耗尽现象是运行一段时间后报GetConnectionTimeoutException原因是连接没释放现象系统跑一段时间后所有数据库操作都卡住日志报获取连接超时。原因是某处代码手动拿了 Connection 没 close或者 MyBatis 的 SqlSession 没关闭。解决用 MyBatis-Spring 的SqlSessionTemplate时SqlSession 由 Spring 管理不需要手动关闭但如果代码里用了sqlSessionFactory.openSession()手动创建必须在 finally 里 close。另外检查 Druid 的maxActive是否设得太小默认 8 在高并发下不够设 20 比较稳妥。排查时可以在 Druid 监控页面看活跃连接数这是定位连接泄漏最快的方法。6. 让答辩加分把耗材管理系统做出“可讲的技术深度”6.1 用 AOP 记录操作日志答辩时能讲清楚切面毕业设计如果只做增删改查答辩老师很容易问“你的技术亮点在哪”。一个成本低但效果好的加分项是用 Spring AOP 做操作日志在 Service 方法上加自定义注解LogRecord切面拦截后把操作人、操作类型、时间写入operation_log表。这样你能在答辩时讲清楚“什么是切点、什么是通知、AOP 和拦截器的区别”。切面代码大致如下Aspect Component public class LogAspect { Autowired private OperationLogMapper logMapper; Around(annotation(logRecord)) public Object around(ProceedingJoinPoint pjp, LogRecord logRecord) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); OperationLog log new OperationLog(); log.setMethod(pjp.getSignature().getName()); log.setCost(System.currentTimeMillis() - start); log.setCreateTime(new Date()); logMapper.insert(log); return result; } }参数说明Around表示环绕通知能在方法执行前后都插入逻辑pjp.proceed()是真正调用目标方法不写它目标方法不会执行logRecord是自定义注解的实例可以从中取操作描述。注意切面类要被 Spring 扫描到且EnableAspectJAutoProxy要开启Spring 5 的 XML 配置里用aop:aspectj-autoproxy/。这个功能代码量不大但能让你在答辩时把 AOP 讲得有理有据。6.2 用 JMeter 压一下领用接口用数据说明并发安全乐观锁到底有没有用压测一下就知道。用 JMeter 开 50 个线程同时请求审批接口观察stock_out表里成功审批的记录数和consumable.stock的最终值是否一致。如果没用乐观锁很可能会出现库存扣成负数的情况用了乐观锁后部分请求会返回“库存已被修改请刷新重试”但库存值始终正确。这个对比实验可以直接写进论文的“测试与分析”章节比单纯贴截图有说服力。压测时注意把 Druid 的maxActive调大否则瓶颈会出现在连接池而不是业务逻辑上。6.3 一个我踩过的坑别在 JSP 里写业务逻辑最后说一个血泪经验。我早期做 JSP 页面时喜欢在c:forEach里用c:if判断库存状态、计算领用上限甚至拼接 SQL 条件。结果页面越来越难维护改一个业务规则要翻好几个 JSP 文件。后来我把所有判断逻辑挪到 Service 层JSP 只负责渲染 Service 返回的 VO 对象页面立刻清爽了。具体做法是 Service 返回的 ConsumableVO 里直接带stockStatus字段“充足”“预警”“缺货”JSP 只做c:if test${item.stockStatus 预警}这样的简单判断。这个习惯让我后来做任何 Java Web 项目都受益也建议你在毕业设计里就这么做——答辩老师问“为什么这么分层”时你能答出“关注点分离”而不是“教程这么写的”。希望帮到你。本文还有配套的精品资源点击获取