FEATURED · 精选文章

基于SSM的实验室设备管理系统:从设备台账到全流程审批

发布时间 / 2026/9/16 18:11:09
来源 / 创域科博编辑部
栏目 / 资讯中心
基于SSM的实验室设备管理系统:从设备台账到全流程审批 简介一套基于SSM架构的实验室设备管理系统毕业设计资料适合计算机相关专业学生用于课程设计、毕业设计或Java Web开发练习。项目采用面向对象设计思想完成设备与用户等核心实体的封装并针对数据库操作做了安全性与扩展性优化技术栈涵盖Spring、Tomcat与MySQL。压缩包共65个文件大小约15.85MB其中包含38个PNG截图、4个JPEG图片及17个XML配置/文档文件另有Word文档、嵌入式对象等素材可清晰看到系统运行界面、项目配置和参考文档。配套资料内含项目完整代码、数据库脚本及报告可直接导入开发环境运行调试也可参照代码结构快速理解SSM项目的分层写法。目前已有912人学习浏览适合希望快速搭建同类管理系统或参考完整毕设写法的开发者。1. 基于SSM的实验室设备管理系统把设备台账从Excel里解放出来基于SSM的实验室设备管理系统在毕业设计里出现频率极高业务边界清晰管理对象是设备、借用记录、维修记录数据关系不复杂却要有完整的增删改查和状态流转把Spring、SpringMVC、MyBatis三个框架各自擅长的部分都压进了同一套代码。系统的重点不是把前端做得花哨而是把设备台账从Excel挪进数据库让每次借用、归还、审批都留下可追溯的数据痕迹。使用者分三类管理员维护台账、处理借用审批实验员提交申请、登记维修学生只读查询。对毕业生来说这套题的价值是把SSM的依赖注入、请求映射、动态SQL串成一套能跑的真代码论文里的E-R图和模块图都能对上。2. 实验室设备管理系统的SSM框架分层与容器配置2.1 按设备业务划分SSM三个层次的工作边界Spring负责管理对象和事务SpringMVC负责请求分发和参数绑定MyBatis负责SQL执行和结果映射。落到设备管理这个域里边界要具体到类和方法的级别才算数。常见做法是工程分三层EquipController只做参数接收、简单校验和JSON返回不写任何状态判断EquipService处理借用审批、归还入库这类流程事务边界和状态校验都在这层EquipMapper接口配XML文件只关心SQL和结果集映射。如果设备状态判断写在Controller里后续调整审批流程时要同时改两层如果Service里直接拼SQLMyBatis的动态SQL就白配了。分层不是写论文用的概念是让改需求时只动一个文件。比如新增「设备报废」功能Controller加一个入口方法Service加一个带事务的报废方法Mapper加一条更新SQL三个文件各改各的互不牵连。层核心类职责最容易越界的地方表现层EquipController参数绑定、权限判断、JSON封装写业务状态判断业务层EquipServiceImpl事务控制、状态流转、借用校验拼SQL、直接操作连接持久层EquipMapper XMLSQL执行、结果集映射在Mapper里写循环调用2.2 applicationContext.xml中的组件扫描、数据源与事务配置SSM毕业设计工程体量不大用XML配置比纯注解直观答辩时也好讲。核心配置集中在applicationContext.xml和spring-mvc.xml两个文件里前者管数据源、SqlSessionFactory、Mapper扫描和事务后者只管Controller。context:component-scan base-packagecom.lab context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lab_equip?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueroot/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /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/组件扫描里用exclude-filter把Controller排除掉Controller统一交给spring-mvc.xml扫这是双容器配置的标准写法。最常翻车的三个位置一是MySQL驱动类8.x用com.mysql.cj.jdbc.Driver且连接串要带serverTimezone5.x用com.mysql.jdbc.Driver版本写错启动直接ClassNotFound二是mapperLocations路径写成classpath:mapper/*.xml后XML必须真实存在否则启动不报错运行到第一个Mapper方法时报Invalid bound statement (not found)这是SSM项目里伪装得最好的隐形错误三是MapperScannerConfigurer负责把接口注册成Bean接口上不用再加Mapper事务管理器漏配或没写tx:annotation-driven是事务不回滚的第一原因。mybatis-config.xml里至少要把mapUnderscoreToCamelCase打开create_time才能自动映射到createTime字段否则实体类每个字段都要手动配resultMap。2.3 spring-mvc.xml与DispatcherServlet的请求映射!-- spring-mvc.xml -- mvc:annotation-driven/ context:component-scan base-packagecom.lab.controller/ mvc:default-servlet-handler/!-- web.xml -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingurl-pattern写/时所有请求先进DispatcherServlet静态资源js、css靠mvc:default-servlet-handler放行。毕业设计高频翻车点是把url-pattern写成/*JSP渲染完返回时又被DispatcherServlet拦一道视图解析直接失败页面永远白板。另一个常见问题是双容器重复扫描applicationContext.xml和spring-mvc.xml都扫com.lab.controllerController会被注册两次事务注解放在Controller上的项目还会出现事务不生效。约定就一条Controller只在spring-mvc.xml里扫其余Service、Repository、Component全留给applicationContext.xml。提示启动日志看到Invalid bound statement先查mapperLocations路径和XML的namespace这两处命中率最高不要先怀疑SQL语法。3. 实验室设备管理系统的核心表设计与MyBatis映射3.1 设备表、借用记录表与用户表的字段定义设备管理的数据模型不复杂三张核心表就能讲清楚业务equip_info存设备台账borrow_record存借用全流程sys_user存三类使用者。关键设计决策是「状态」用TINYINT存数字而不是直接存空闲借用中这类中文一是排序过滤快二是改状态文案不用动数据库。CREATE TABLE equip_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, equip_no VARCHAR(32) NOT NULL COMMENT 设备编号, name VARCHAR(64) NOT NULL COMMENT 设备名称, category_id INT NOT NULL COMMENT 分类ID, location VARCHAR(64) COMMENT 存放位置, status TINYINT NOT NULL DEFAULT 1 COMMENT 1空闲 2借用中 3维修 4报废, price DECIMAL(10,2), buy_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_equip_no (equip_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, equip_id BIGINT NOT NULL COMMENT 设备ID, user_id BIGINT NOT NULL COMMENT 借用人ID, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, plan_return_time DATETIME NOT NULL COMMENT 计划归还时间, actual_return_time DATETIME COMMENT 实际归还时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待审批 2借用中 3已驳回 4已归还, approver_id BIGINT COMMENT 审批人ID, approve_time DATETIME COMMENT 审批时间, remark VARCHAR(255), KEY idx_equip_id (equip_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;equip_no加了唯一索引它是设备台账的天然业务编号但VARCHAR做主键会让外键变长所以表内主键用自增idequip_no只做唯一约束。borrow_record里的status和equip_info里的status是两套枚举含义完全不同联查时最容易看混的就是这个。create_time用DEFAULT CURRENT_TIMESTAMP自动填充省掉Service里手动new Date()的重复代码。sys_user表相对简单字段就是id、username、password、real_name、role、deptrole用1管理员、2实验员、3学生区分权限。两张表的status对应关系表status值含义维度equip_info1/2/3/4空闲/借用中/维修/报废设备维度borrow_record1/2/3/4待审批/借用中/已驳回/已归还单据维度3.2 设备分页查询的动态SQL与PageHelper接入设备列表是主视图查询条件一般三个设备名称模糊匹配、状态下拉筛选、分类筛选。分页在SSM里最常用的方案是PageHelper一个拦截器自动拼LIMIT。注意startPage和Mapper调用之间不能插入任何其它数据库操作PageHelper基于ThreadLocal保存分页参数中间隔一次查询分页参数就被那次查询消费掉症状是列表不分页、一次性返回全部数据。select idselectEquipPage resultTypecom.lab.entity.EquipInfo SELECT id, equip_no, name, category_id, location, status, price, buy_date FROM equip_info where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND status #{status} /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY create_time DESC /selectPageHelper.startPage(pageNum, pageSize); ListEquipInfo list equipMapper.selectEquipPage(name, status, categoryId); PageInfoEquipInfo pageInfo new PageInfo(list);标签自动处理AND前缀条件都不满足时不生成WHERE第一个条件成立时自动去掉开头的AND。name用CONCAT拼%而不是写%#{name}%是因为#{}在预编译阶段被当作参数占位符直接写在字符串里会被当成普通字符匹配结果永远是空集。接口方法带多个参数时每个参数必须加Param(name)这类注解否则XML里的#{name}引用不到参数运行时报BindingException。PageInfo封装了total、pageNum、pageSize和listController把它塞进Result返回前端拿pageInfo.list渲染表格拿total渲染分页条。3.3 借用记录多表联查的resultMap与列名冲突借用列表要同时显示设备名称和借用人姓名必须联查equip_info和sys_user。多表联查最怕两件事列名重复和字段对不上。两张表都有id和create_timeselect列不写别名结果集映射时后面的值覆盖前面的取出来的字段全是错的。resultMap idborrowVOMap typecom.lab.vo.BorrowVO id propertyid columnid/ result propertyequipId columnequip_id/ result propertyequipName columnequip_name/ result propertyuserName columnuser_name/ result propertystatus columnstatus/ result propertyapplyTime columnapply_time/ result propertyplanReturnTime columnplan_return_time/ /resultMap select idselectBorrowPage resultMapborrowVOMap SELECT r.id, r.equip_id, e.name AS equip_name, u.real_name AS user_name, r.status, r.apply_time, r.plan_return_time FROM borrow_record r LEFT JOIN equip_info e ON r.equip_id e.id LEFT JOIN sys_user u ON r.user_id u.id ORDER BY r.apply_time DESC /selectresultMap比resultType更合适BorrowVO里的equipName、userName对应别名列用resultTypeMap虽然也能拿数据但前端解析散列的Map远不如解析有getter的VO方便。LEFT JOIN保证borrow_record有一条就返回一行设备或用户被删时联查字段为null单据不会丢。答辩常问「为什么不用JOIN」答案就一条借用记录是主单据查询不能因为关联表数据缺失而消失。4. 设备借用审批与状态流转的SSM事务实现4.1 借用状态流转的校验规则与状态机定义borrow_record的状态流转是个典型小状态机待审批可以驳回变成已驳回可以审批通过变成借用中借用中只能归还变成已归还。毕业设计里最容易出问题的写法是Service里到处写if (status 1)这类魔法数状态一多就分不清数字含义。先把状态收敛成常量代码按语义引用。public class BorrowStatus { public static final int PENDING 1; public static final int BORROWED 2; public static final int REJECTED 3; public static final int RETURNED 4; }当前状态允许动作目标状态待审批(1)审批通过借用中(2)待审批(1)审批驳回已驳回(3)借用中(2)设备归还已归还(4)审批方法的校验顺序固定三步先查记录是否存在再判当前状态是否待审批最后在事务里同时更新设备状态和单据状态。第一步和第二步防重复处理第三步防并发冲突。只判断record是否为null、不检查status同一张单被提交两次审批时第二次仍会进入更新逻辑把已驳回的单又改成借用中。4.2 审批通过时用CAS条件更新防止设备并发借出同一台设备被两个人同时申请不能两人都审批通过。常见做法是审批通过时抢设备状态UPDATE影响行数为1说明抢到影响行数为0说明设备已被占用或已报废。Transactional(rollbackFor Exception.class) public void approve(Long recordId, Integer approveResult, Long approverId) { BorrowRecord record borrowMapper.selectById(recordId); if (record null || record.getStatus() ! BorrowStatus.PENDING) { throw new BusinessException(申请记录不存在或已被处理); } if (approveResult 1) { int rows equipMapper.updateStatusIfMatch( record.getEquipId(), EquipStatus.FREE, EquipStatus.BORROWED); if (rows 0) { throw new BusinessException(设备当前不可借用审批失败); } borrowMapper.updateStatus(recordId, BorrowStatus.BORROWED, approverId, new Date()); } else { borrowMapper.updateStatus(recordId, BorrowStatus.REJECTED, approverId, new Date()); } }UPDATE equip_info SET status #{newStatus} WHERE id #{equipId} AND status #{expectStatus}先更新设备再更新单据顺序不能反。设备更新成功而单据更新失败时事务回滚把设备状态一并还原反过来先改单据再抢设备设备抢失败抛异常回滚的是单据日志里看到单据已审批、设备却是空闲容易误判。updateStatusIfMatch返回0的分支必须抛异常而不是返回提示因为Transactional默认只对RuntimeException回滚返回0本身不触发回滚。updateStatusIfMatch和updateStatus的Mapper接口方法都要写Param(equipId)、Param(newStatus)这类注解多参数不加Param是MyBatis BindingException的重灾区。4.2.1 自调用导致事务失效的排查同一个类里approve方法调用本类的另一个Transactional方法事务不生效。Spring事务基于代理实现自调用走的是this指向的原对象代理拦不住。典型场景是把归还逻辑抽成returnEquip方法并在approve内部直接调用。解决方式要么把returnEquip放到另一个Service里注入调用要么通过AopContext.currentProxy()取代理对象。把带Transactional的方法调用链过一遍凡是this.xxx()调用另一个事务方法的都是隐患。4.3 Controller层参数校验与统一JSON返回结构Controller做参数格式校验业务规则校验放Service这个分工要立住。审批接口接收recordId、approveResult、approverIdapproveResult必须限定0或1否则非法值落到Service的if判断里返回语义含糊的错误。RestController RequestMapping(/borrow) public class BorrowController { Autowired private BorrowService borrowService; PostMapping(/approve) public Result approve(RequestParam Long recordId, RequestParam Integer approveResult, RequestParam Long approverId) { if (recordId null || (approveResult ! null approveResult ! 0 approveResult ! 1)) { return Result.fail(审批参数不正确); } try { borrowService.approve(recordId, approveResult, approverId); return Result.ok(); } catch (BusinessException e) { return Result.fail(e.getMessage()); } } }public class Result { private int code; private String msg; private Object data; // 构造函数与getter/setter省略 public static Result ok() { return new Result(200, success, null); } public static Result ok(Object data) { return new Result(200, success, data); } public static Result fail(String msg) { return new Result(500, msg, null); } }前端只认Result的code和msg正常返回200业务异常返回500data只在查询接口用。Controller用try-catch包住Service调用把BusinessException转成Result.fail避免SpringMVC把异常回成500错误页那样前端拿到的是HTML而不是JSON联调时最磨人。RequestParam接收的是表单格式参数前端用axios的POST加application/json时要用RequestBody接一个DTO两种方式混用会出现参数全是null。5. 用POI导出设备台账并核对SSM工程自检项设备台账导出是管理员模块标配答辩时被问的概率也高。SSM里导出一般用Apache POI链路是Service查出全部设备Controller设置下载响应头POI把数据写进工作簿最后workbook.write到response.getOutputStream()。导出接口不走视图解析器方法声明返回void。RequestMapping(/export) public void export(HttpServletResponse response) throws IOException { ListEquipInfo list equipService.listForExport(); response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment;filenameequip.xlsx); try (XSSFWorkbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(设备台账); String[] headers {设备编号, 名称, 状态, 位置, 价格}; Row row0 sheet.createRow(0); for (int i 0; i headers.length; i) { row0.createCell(i).setCellValue(headers[i]); } workbook.write(response.getOutputStream()); } }try-with-resources处理Workbook关闭避免导出频繁时文件占用报错。Content-Disposition里的文件名带中文时要用URLEncoder.encode转一遍否则Chrome下保存文件名乱码。状态列导出去是数字1/2/3/4答辩常被问导出前用Map把status翻译成空闲/借用中/维修/报废再写单元格一行代码的功夫。提示自检清单——启动报ClassNotFound查MySQL驱动版本运行报Invalid bound statement查mapper.xml的namespace和路径页面404查url-pattern和Controller包扫描事务不回滚查Transactional位置和自调用乱码查连接串characterEncodingutf8及Tomcat的URIEncoding。导出联调验证不看页面打开F12的Network面板点导出后看响应头里有没有Content-Disposition。有说明POI导出和响应下发都正常没有则多半是Controller抛异常被默认异常处理器拦了去控制台翻堆栈比反复点按钮快。顺手检查equip_info的unique key和borrow_record的idx_equip_id索引有没有建全数据量过千之后借用列表联查变慢十有八九是漏了这两个索引。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻