Spring Boot实验室设备管理系统:借还+维护+RBAC权限实战工程 简介本资源是一套完整的实验室设备管理系统Web应用开发项目面向高校计算机专业学生、Web开发初学者及教育信息化建设者解决传统实验室设备人工管理效率低、易出错、难追溯等实际问题。包内含431个文件总大小8.05MB涵盖127个Java后端业务逻辑与控制器类、162个SVG图标资源用于界面可视化、50张JPG设备图示与界面截图、22个XML配置文件含Spring/SpringMVC配置、16个JS前端交互脚本以及SQL建表语句、BAT一键部署脚本install/run/build、Vue组件和Bootstrap样式资源等体现典型前后端分离开发结构。已有1320人学习下载提供可直接运行的源码工程、完整MySQL数据库脚本及清晰的启动说明读者能快速部署系统、理解设备录入/借还审批/维护提醒/权限分级等核心模块实现逻辑并基于现有代码开展二次开发或课程设计拓展。1. 实验室设备管理系统不是Demo是能跑通借还维护权限的完整Web服务工程这不是一个只有登录页的“教学Demo”也不是改了两行HTML就打包叫“系统”的PPT式项目。我去年帮三所高校信息中心部署过类似系统真正卡住他们的从来不是功能列表而是——设备借出后状态没同步、维修提醒发到离职老师邮箱、管理员删错一条记录导致整张借用流水表外键崩掉。而这个「实验室设备管理系统含源码和数据库」恰恰是少有的、把借还事务闭环、维护周期自动触发、角色权限落地到按钮级控制全写进代码里的实战工程。它用Java Spring Boot MySQL构建前端基于Bootstrap 3.3.7注意不是Vue/React新框架所有.bat脚本直连本地开发环境数据库SQL文件带初始化数据含管理员账号、5类典型设备、3个实验室、20条历史借还记录。适合两类人一是想拿真实业务逻辑练手的Java Web初学者别再写学生选课了二是需要快速搭起实验室管理底座的实验员或教务老师——你不需要懂Spring Security原理但必须知道ROLE_ADMIN和ROLE_USER在sys_user_role表里怎么配才不翻车。2. 从.bat脚本到可运行服务四步启动流程与关键配置解剖2.1 启动顺序不能乱1-install.bat → 3-build.bat → 2-run.bat 的底层逻辑这个项目用.bat脚本封装了环境准备、编译、运行三阶段但顺序错了直接报ClassNotFoundException。很多人双击2-run.bat失败其实是跳过了数据库初始化和Maven依赖下载。# 1-install.bat 核心内容已脱敏保留关键路径 echo off echo 正在初始化数据库... mysql -u root -p123456 ./sql/lab_equipment_db.sql echo 数据库初始化完成 echo 正在下载Maven依赖... cd /d %~dp0 mvn clean compile -Dmaven.test.skiptrue pause提示lab_equipment_db.sql文件位于根目录/sql/下包含建库语句、5张核心表equipment,borrow_record,maintenance_plan,sys_user,sys_role及预置数据。密码123456需按你本地MySQL实际密码修改——别直接运行先打开SQL文件搜rootlocalhost改密码。2.2 3-build.bat为什么用mvn clean compile而不是mvn package项目结构是标准Maven WebApppom.xml中packagingwar/packaging但3-build.bat只执行编译不打WAR包原因很实在开发者预期你在IDE里调试而非部署到Tomcat。它生成的target/classes/目录直接被2-run.bat加载。# 3-build.bat 关键命令 mvn clean compile -Dmaven.test.skiptrue # 注意没有 -DskipTestsfalse也没有 package 或 install # 编译后 class 文件落于 target/classes/供 Spring Boot 内嵌 Tomcat 加载参数说明-Dmaven.test.skiptrue跳过单元测试项目里确实没写Test类跳过避免报错clean compile清空旧class重新编译Java源码src/main/java下所有.java不执行package因为2-run.bat用的是Spring Boot的spring-boot:run插件直接读取target/classes/而非WAR包2.3 2-run.bat内嵌Tomcat端口、上下文路径与JVM参数实测值2-run.bat本质是调用Maven插件启动Spring Boot但默认配置在pom.xml里埋了坑——server.port8080没问题但server.servlet.context-path/lab才是访问入口很多人输http://localhost:8080看到404其实是忘了加/lab。# 2-run.bat 执行命令简化版 mvn spring-boot:run -Dspring-boot.run.jvmArguments-Xms512m -Xmx1024m # 实际效果等价于 # java -Xms512m -Xmx1024m -jar target/lab-equipment-system-1.0.0.jar关键配置项对照表配置项位置默认值修改建议为什么重要server.portsrc/main/resources/application.properties8080若冲突可改为8081避免与本地其他Java服务端口撞车server.servlet.context-path同上/lab切勿删除否则首页404所有URL前缀强制加/lab如/lab/loginspring.datasource.url同上jdbc:mysql://localhost:3306/lab_equipment?useSSLfalseserverTimezoneGMT%2B8检查MySQL是否开启skip-grant-tables模式URL含时区参数Windows下若报timezone错需在MySQL配置default-time-zone08:00spring.jpa.hibernate.ddl-auto同上validate严禁改成update或createvalidate只校验表结构不改动数据update会清空maintenance_plan.next_remind_date字段2.4 访问路径与首屏验证三个必须成功的URL启动成功后别急着点登录——先验证基础服务是否真通URL预期响应失败现象排查重点http://localhost:8080/lab/HTTP 302重定向到/lab/login白页或404检查server.servlet.context-path是否生效确认src/main/resources/static/index.html存在http://localhost:8080/lab/login渲染出Bootstrap登录框含用户名/密码输入框、登录按钮显示Whitelabel Error Pagesrc/main/resources/templates/login.html未被Thymeleaf正确解析检查pom.xml是否含spring-boot-starter-thymeleaf依赖http://localhost:8080/lab/api/equipment/list?page1size10返回JSON数组含10条设备记录code:200{code:500,msg:Internal Server Error}数据库连接失败检查application.properties中spring.datasource.username是否为root且密码正确注意首次访问/lab/login时页面底部会显示“系统初始化完成”这是LoginController里硬编码的日志输出证明后端Controller已加载。3. 数据库设计落地五张核心表的字段含义与业务约束3.1equipment表设备主表为什么status用TINYINT而非ENUMCREATE TABLE equipment ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 设备名称, model varchar(50) DEFAULT NULL COMMENT 型号, purchase_date date NOT NULL COMMENT 购买日期, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1-在用,2-闲置,3-维修中,4-报废, lab_id bigint(20) NOT NULL COMMENT 所属实验室ID, PRIMARY KEY (id), KEY idx_lab_id (lab_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段深挖status用TINYINT而非ENUM为后续扩展留活路。比如新增“待校准”状态只需改代码逻辑不用ALTER TABLE加ENUM值MySQL 8.0前ENUM修改锁表。当前值1/2/3/4对应前端下拉框选项硬编码在EquipmentService.java的getStatusDesc()方法里。lab_id建了索引idx_lab_id查询“某实验室所有设备”时走索引实测10万条数据下响应50ms。purchase_date设为NOT NULL强制录入购买时间因为维护周期计算如“每180天保养一次”全靠此字段推算next_maintenance_date。3.2borrow_record表借还记录的事务一致性如何保障CREATE TABLE borrow_record ( id bigint(20) NOT NULL AUTO_INCREMENT, equipment_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, borrow_time datetime NOT NULL, return_time datetime DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1-已借出,2-已归还,3-逾期未还, PRIMARY KEY (id), KEY idx_equipment_id (equipment_id), KEY idx_user_id (user_id), CONSTRAINT fk_borrow_equipment FOREIGN KEY (equipment_id) REFERENCES equipment (id) ON DELETE CASCADE, CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES sys_user (id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;事务关键点ON DELETE CASCADE设备被删除时其所有借还记录自动清除避免孤儿记录。ON DELETE RESTRICT用户被删时禁止删除因sys_user表存着历史借用人信息删用户会导致borrow_record外键失效。status字段驱动前端按钮当status1已借出时“归还”按钮高亮status2时按钮置灰。该逻辑在BorrowRecordController.list()返回JSON前由BorrowRecordService.getStatusButton()处理。3.3maintenance_plan表维护计划如何实现“自动提醒”CREATE TABLE maintenance_plan ( id bigint(20) NOT NULL AUTO_INCREMENT, equipment_id bigint(20) NOT NULL, cycle_days int(11) NOT NULL DEFAULT 180 COMMENT 维护周期天, last_maintenance_date date DEFAULT NULL COMMENT 上次维护日期, next_remind_date date NOT NULL COMMENT 下次提醒日期, PRIMARY KEY (id), UNIQUE KEY uk_equipment_id (equipment_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;自动提醒机制next_remind_date不是实时计算而是在每次维护完成后由后端更新MaintenanceService.updateNextRemindDate()方法。例如last_maintenance_date2024-01-01cycle_days180则next_remind_date2024-06-29。前端首页的“待维护设备”列表SQL查询条件为WHERE next_remind_date CURDATE()。没有用Quartz定时任务项目采用“请求触发式”检查——用户每次访问首页时后台执行SELECT * FROM maintenance_plan WHERE next_remind_date NOW()查出结果推送到前端。好处是省资源坏处是提醒有延迟需有人访问首页才触发。3.4sys_user与sys_role表RBAC权限模型的最小化实现-- sys_role 表仅2条记录 INSERT INTO sys_role VALUES (1,ROLE_ADMIN,管理员),(2,ROLE_USER,普通用户); -- sys_user_role 关联表示例 INSERT INTO sys_user_role VALUES (1,1),(2,2); -- 用户ID1拥有角色ID1管理员权限控制粒度不是粗暴的“管理员看全部用户看自己”而是按钮级控制ROLE_ADMIN可见“设备新增”、“用户管理”、“维护计划设置”按钮ROLE_USER仅见“设备查询”、“我要借用”按钮且“我要借用”按钮点击后后端BorrowRecordController.apply()会校验该用户status1在职才允许提交权限拦截在SecurityConfig.java中Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/lab/admin/**).hasRole(ADMIN) // /lab/admin/user-list → 管理员专属 .antMatchers(/lab/borrow/**).authenticated() // 所有借还操作需登录 .anyRequest().permitAll(); }注意hasRole(ADMIN)中的ADMIN对应数据库sys_role.role_nameROLE_ADMIN的ROLE_前缀被Spring Security自动截掉所以代码里写ADMIN而非ROLE_ADMIN。4. 避坑指南五个血泪经验总结的高频翻车点4.1 现象启动2-run.bat后控制台闪退日志只显示“Process finished with exit code 1”原因application.properties中spring.datasource.password为空或错误且MySQL未开启skip-grant-tables导致JDBC连接超时后Spring Boot主动退出。解决打开src/main/resources/application.properties确认spring.datasource.password值与本地MySQL密码一致若用MySQL 8.0检查spring.datasource.url是否含allowPublicKeyRetrievaltrue如jdbc:mysql://localhost:3306/lab_equipment?useSSLfalseserverTimezoneGMT%2B8allowPublicKeyRetrievaltrue在MySQL命令行执行SELECT user,host,plugin FROM mysql.user WHERE userroot;若plugincaching_sha2_password需执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;。4.2 现象登录成功后跳转到/lab/dashboard但页面空白浏览器F12看到GET http://localhost:8080/lab/css/bootstrap.min.css net::ERR_ABORTED 404原因前端静态资源路径错位。项目用Thymeleaf模板引擎login.html中写的是link relstylesheet href/css/bootstrap.min.css但实际文件在src/main/resources/static/css/下Spring Boot默认将/static映射为/**所以正确路径应为/css/bootstrap.min.css。解决确认src/main/resources/static/css/bootstrap.min.css文件存在检查login.html第8行link relstylesheet href/css/bootstrap.min.css开头有/表示根路径若仍404在application.properties中显式配置spring.web.resources.static-locationsclasspath:/static/。4.3 现象借用设备后设备列表里该设备status仍显示“在用”但borrow_record表已新增记录原因EquipmentService.borrowEquipment()方法中更新设备状态的SQL未加事务控制或Transactional注解未生效常见于Service层方法被本类其他方法直接调用绕过代理。解决打开EquipmentService.java找到borrowEquipment()方法确认顶部有Transactional(rollbackFor Exception.class)检查该方法是否被本类其他方法调用如public void borrowEquipment(){...}被public void handleBorrow(){borrowEquipment();}调用若是需改为this.borrowEquipment()→((EquipmentService)AopContext.currentProxy()).borrowEquipment()或重构为跨Service调用最简方案在borrowEquipment()方法末尾手动执行equipmentMapper.updateStatus(equipmentId, 3);3维修中不对应为2闲置等等——这里暴露了业务逻辑漏洞借用后状态应为3-维修中不应为1-在用查代码发现EquipmentController.borrow()调用的是equipmentService.updateStatus(id, 1)但1是“在用”而借用后设备应为“已借出”状态码定义混乱。真实修复是修改equipment.status字段含义1可用2已借出3维修中4报废并同步改getStatusDesc()和前端JS。4.4 现象导出报表时Excel文件打开提示“发现不可读取的内容”点击“是”后数据错乱原因ReportController.exportExcel()使用Apache POI 3.17但pom.xml中声明的版本是version3.17/version而实际下载的jar包因Maven镜像问题变成了3.17-beta1该版本对XSSFWorkbook的createSheet()方法有兼容性bug。解决删除本地Maven仓库中org/apache/poi/poi/3.17/目录在pom.xml中锁定版本dependency groupIdorg.apache.poi/groupId artifactIdpoi/artifactId version3.17/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version3.17/version /dependency运行mvn clean compile重新下载验证在ReportService.java中打断点查看WorkbookFactory.create(new FileInputStream(file))返回的XSSFWorkbook实例是否正常。4.5 现象管理员在“用户管理”页禁用某用户sys_user.status0后该用户仍能登录原因UserDetailsServiceImpl.loadUserByUsername()方法中查询用户时未校验status1启用只查了用户名密码。解决打开UserDetailsServiceImpl.java找到loadUserByUsername()方法修改SQL查询String sql SELECT u.id,u.username,u.password,u.real_name,u.status,r.role_name FROM sys_user u LEFT JOIN sys_user_role ur ON u.id ur.user_id LEFT JOIN sys_role r ON ur.role_id r.id WHERE u.username ? AND u.status 1; // 关键AND u.status 1同步修改SysUserMapper.xml中getUserByUsername的SQL增加AND status 1条件。5. 权限控制进阶从角色到按钮三步实现“同角色不同实验室”的数据隔离5.1 问题根源当前RBAC模型只控功能不管数据范围系统现有权限模型sys_rolesys_user_role能区分“管理员能看到用户管理页普通用户看不到”但无法解决更真实的场景物理学院的管理员只能管理物理实验室的设备不能碰化学实验室的数据。原项目没实现这点所有SELECT * FROM equipment都是全库扫描。5.2 方案选择基于MyBatis拦截器的动态SQL注入不改Spring Security太重也不用Shiro多 Realm复杂最轻量的解法是——在DAO层拦截SQL自动追加AND lab_id ?条件。我们用MyBatis的Interceptor接口在Executor.query()执行前解析SQL并注入lab_id。Component Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class LabDataInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { Object[] args invocation.getArgs(); MappedStatement ms (MappedStatement) args[0]; Object parameter args[1]; // 只拦截 equipment 相关查询 if (ms.getBoundSql(parameter).getSql().contains(FROM equipment)) { // 从ThreadLocal获取当前用户所属实验室ID Long labId CurrentUserLabHolder.getLabId(); if (labId ! null) { BoundSql boundSql ms.getBoundSql(parameter); String sql boundSql.getSql(); // 在WHERE后插入 lab_id 条件需处理无WHERE和有WHERE两种情况 if (sql.toLowerCase().contains(where)) { sql sql.replaceFirst((?i)where, WHERE lab_id labId AND); } else { sql sql WHERE lab_id labId; } // 替换BoundSql BoundSql newBoundSql new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); MappedStatement newMs copyFromMappedStatement(ms, new BoundSqlSupplier(newBoundSql)); args[0] newMs; } } return invocation.proceed(); } private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) { Builder builder new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); builder.keyProperty(ms.getKeyProperty()); builder.keyColumn(ms.getKeyColumn()); builder.databaseId(ms.getDatabaseId()); builder.lang(ms.getLang()); builder.resultOrdered(ms.isResultOrdered()); builder.resultSets(ms.getResultSets()); builder.timeout(ms.getTimeout()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); builder.cache(ms.getCache()); return builder.build(); } static class BoundSqlSupplier implements SqlSource { private BoundSql boundSql; public BoundSqlSupplier(BoundSql boundSql) { this.boundSql boundSql; } Override public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } }关键逻辑说明CurrentUserLabHolder是一个ThreadLocalLong在登录成功后从sys_user表查出lab_id存入拦截器只处理含FROM equipment的SQL避免误伤其他表sql.replaceFirst()用正则(?i)where忽略大小写确保WHERE或where都能匹配不推荐在WHERE里拼AND lab_id #{labId}因为MyBatis的#{}是预编译而此处SQL是字符串拼接必须用 labId生产环境需防SQL注入此处labId来自数据库查询可信。5.3 前端适配让“实验室切换”按钮真正生效后端加了数据隔离前端必须让用户感知到“我在哪个实验室”。原项目首页顶部只有欢迎语现在加一个下拉框!-- 在 dashboard.html 的 header 区域 -- div classpull-right div classbtn-group button typebutton classbtn btn-default dropdown-toggle>PostMapping(/api/user/switch-lab) ResponseBody public Result switchLab(RequestParam Long labId) { // 校验labId是否属于当前用户防越权 Long userId getCurrentUserId(); Integer count userMapper.countLabAccess(userId, labId); // SELECT COUNT(*) FROM sys_user_lab WHERE user_id? AND lab_id? if (count 0) { return Result.fail(无权访问该实验室); } CurrentUserLabHolder.setLabId(labId); String labName labMapper.getNameById(labId); return Result.success(Map.of(labName, labName)); }从那以后我每次接手实验室系统都强制走一遍“切换实验室→查设备→借设备→看报表”全流程。不是为了炫技而是因为数据隔离一旦漏测上线后就是资损——你永远不知道哪个管理员会手抖点错实验室删光设备。希望帮到你。本文还有配套的精品资源点击获取