ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于SSM框架的实验室设备管理系统设计与实现

2026/9/16 14:55:14 拓冰建站 浏览量
基于SSM框架的实验室设备管理系统设计与实现 简介一套基于SSM框架开发的实验室设备管理系统完整工程面向JavaWeb初学者、毕业设计与课程设计人群覆盖设备申请、维修登记、预约使用、状态查询等业务模块并围绕设备、申请、维修等数据表构建。压缩包共385个文件31.34MB内含源码、配置文件、数据库脚本、演示文稿和运行所需依赖包编译后的类文件对应各业务实体与数据库映射便于对照学习持久层写法属性配置和模板文件可辅助理解数据流转。前端采用Layui布局配合AngularJS与后端进行Json交互后端基于SSM架构开发环境为JDK1.8、Tomcat7、IDEA数据库MySQL可直接导入运行。压缩包内还包含war与SQL脚本便于部署迁移或二次开发。已有598人学习浏览配套演示文稿可辅助答辩汇报文档补充设计说明与实现思路适合快速搭建同类型管理系统。1. 实验室设备管理系统为什么总在“能用”和“好用”之间差一个SSM框架实验室设备管理的痛点从来不在“登记”本身而在设备从入库、领用、借用、维修到报废的全生命周期里每一次状态变化都要有人记录、有据可查。拿Excel登记设备去向靠问拿纸质单据流转月底对账靠猜。所谓基于SSM框架的实验室设备管理系统本质上就是把这一套台账和流程搬进数据库再用Spring管理业务逻辑、Spring MVC接收请求、MyBatis操作数据最终让“这台设备现在在谁手里、什么时候该还、维修过几次”变成一条SQL就能回答的问题。这套系统的价值不在代码量而在数据模型的完整性和状态流转的严谨性。对于做Java课程设计、数据库课程设计的在校生或者刚接触SSM整合的开发者来说一个能跑通“增删改查权限控制状态变更”的完整项目比零散的功能demo更能说明问题。本文章不讲花哨的架构只讲一套最常见的落地路径怎么建表、怎么写Service、怎么调页面、怎么打包部署。适合手里有SSM框架基础、想直接套一套完整方案的读者。2. 从数据库设计谈起设备管理系统的表结构与权限模型2.1 核心业务表设备台账、领用记录、维修记录、报废记录怎么建实验室设备管理系统的数据库设计第一步不是急着写代码而是把业务对象拆清楚。常见的做法是围绕“设备”这一主实体向外延伸出设备分类、领用记录、维修记录、报废记录、用户信息这几张核心表。设备表存放静态属性如设备编号、名称、型号、所属实验室、采购日期、状态领用表存放动态流转记录谁在什么时间借走了哪台设备、预计归还时间和实际归还时间维修表记录故障描述、维修费用、维修结果报废表则是对生命周期终结的设备做一次归档。以下是一套可以直接套用的MySQL建表语句字段类型和长度按常见实验室场景设定CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(32) NOT NULL UNIQUE COMMENT 设备编号业务唯一键, device_name VARCHAR(64) NOT NULL COMMENT 设备名称, model VARCHAR(64) DEFAULT NULL COMMENT 规格型号, lab_room VARCHAR(64) DEFAULT NULL COMMENT 所属实验室, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1在库 2借出 3维修中 4已报废, buy_date DATE DEFAULT NULL COMMENT 采购日期, price DECIMAL(10,2) DEFAULT NULL COMMENT 采购价格, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备台账表; CREATE TABLE device_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL COMMENT 设备ID关联device.id, user_id INT NOT NULL COMMENT 借用人ID关联sys_user.id, borrow_time DATETIME NOT NULL COMMENT 借出时间, expect_return_time DATETIME NOT NULL COMMENT 预计归还时间, actual_return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1借用中 2已归还 3逾期未还, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, FOREIGN KEY (device_id) REFERENCES device(id), FOREIGN KEY (user_id) REFERENCES sys_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备借用记录表; CREATE TABLE device_repair ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL, report_user_id INT NOT NULL COMMENT 报修人, fault_desc VARCHAR(255) NOT NULL COMMENT 故障描述, repair_result VARCHAR(255) DEFAULT NULL COMMENT 维修结果, cost DECIMAL(10,2) DEFAULT NULL COMMENT 维修费用, status TINYINT NOT NULL DEFAULT 1 COMMENT 1维修中 2已完成 3无法修复, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (device_id) REFERENCES device(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备维修记录表; CREATE TABLE device_scrap ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL, scrap_reason VARCHAR(255) NOT NULL COMMENT 报废原因, scrap_time DATETIME NOT NULL COMMENT 报废时间, operator_id INT NOT NULL COMMENT 经办人, FOREIGN KEY (device_id) REFERENCES device(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备报废记录表;这段DDL的关键点有三个。第一device_code加了UNIQUE约束这是业务上的硬要求同一台设备编号在系统里不能出现两次否则数据对不上实物。第二device表里的status字段用TINYINT存数字状态而不是字符串目的是让状态流转逻辑写在Java代码里而不是散落在SQL里后面在Service层会集中处理。第三device_borrow表通过外键关联device和sys_user查询时用JOIN一次就能把“谁借了什么设备”捞出来不需要反范式地在借用表里冗余设备名和用户名。这里要注意一个常见的设计分歧既然device表已经有status字段借用记录表里也有status字段是不是冗余不是。device.status表示设备当下的物理状态device_borrow.status表示某一次借用业务的完成度两者服务于不同的查询维度。前者回答“现在能借吗”后者回答“这笔单子完结没有”缺一不可。2.2 权限模型用户、角色、权限三张表关联查询的思路实验室设备管理系统的用户角色一般分三类管理员、实验室管理员、普通用户教师或学生。如果系统规模不大直接在sys_user表上加一个role字段也能跑但考虑到后续可能要扩展“只能查看本实验室设备”“只能审批本学院借用申请”这类细粒度控制三张表的标准RBAC模型更稳妥。常见的建表方式如下CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 建议存MD5或BCrypt哈希, real_name VARCHAR(32) NOT NULL, role_id INT NOT NULL, status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, FOREIGN KEY (role_id) REFERENCES sys_role(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_role ( id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(32) NOT NULL UNIQUE, role_desc VARCHAR(128) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_permission ( id INT PRIMARY KEY AUTO_INCREMENT, perm_name VARCHAR(64) NOT NULL COMMENT 如device:add、device:delete, perm_desc VARCHAR(128) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_role_permission ( role_id INT NOT NULL, permission_id INT NOT NULL, PRIMARY KEY (role_id, permission_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;权限字段perm_name建议写成模块:操作的形式比如device:add、device:borrow、user:resetPwd。这样在Spring MVC的拦截器里做URL权限判断时可以通过请求路径直接映射到权限标识不需要额外维护URL和权限的对照表。登录时查出用户的角色名和权限集合存进Session后续每个请求做校验时直接比对Session里的权限列表比每次都查数据库效率高得多。如果觉得四张表对课程设计来说偏重可以退化成“用户表存角色ID角色表存权限字符串”的两表模型用逗号分隔的权限串存字段里。这个方案在数据量小、权限点少的时候完全够用而且写起来快很多。但从数据库课程设计的评分角度看三张核心业务表加RBAC四张权限表、再配合外键关联与联合查询整体完整性会明显更强。3. 用SSM把核心流程跑通设备借用的Service层事务与状态机设计3.1 Controller-Service-Mapper三层怎么拆事务注解加在哪一层SSM框架整合完毕之后业务代码的组织方式基本是约定俗成的Controller只做参数接收和视图跳转Service写业务逻辑Mapper接口定义数据库操作方法XML文件写SQL。设备借用这个场景是演示三层分界和事务控制的经典用例。先定义Mapper接口public interface DeviceMapper { Device selectById(Integer id); int updateStatus(Param(id) Integer id, Param(status) Integer status); } public interface DeviceBorrowMapper { int insert(DeviceBorrow borrow); int updateStatus(Param(id) Integer id, Param(status) Integer status); }注意updateStatus接收两个参数一个是设备ID一个是目标状态。在MyBatis里多参数必须用Param注解标明参数名否则XML里#{id}和#{status}会因无法解析参数名而报错。XML对应的写法如下update idupdateStatus UPDATE device SET status #{status}, update_time NOW() WHERE id #{id} /updateService层是状态流转的核心。借用设备的逻辑不是简单UPDATE两条记录而是一个复合操作先校验设备状态为“在库”再把设备状态改为“借出”同时插入一条借用记录。这三步任何一步失败前面的操作都要回滚否则就会出现“借用记录写了但设备状态没变”的数据不一致。Service public class DeviceBorrowService { Autowired private DeviceMapper deviceMapper; Autowired private DeviceBorrowMapper borrowMapper; Transactional(rollbackFor Exception.class) public void borrowDevice(Integer deviceId, Integer userId, Date expectReturnTime) { Device device deviceMapper.selectById(deviceId); if (device null) { throw new RuntimeException(设备不存在); } if (device.getStatus() ! 1) { throw new RuntimeException(设备当前不可借用状态 device.getStatus()); } int updated deviceMapper.updateStatus(deviceId, 2); if (updated 0) { throw new RuntimeException(设备状态更新失败可能已被他人借用); } DeviceBorrow borrow new DeviceBorrow(); borrow.setDeviceId(deviceId); borrow.setUserId(userId); borrow.setBorrowTime(new Date()); borrow.setExpectReturnTime(expectReturnTime); borrow.setStatus(1); borrowMapper.insert(borrow); } }这段代码有三个地方值得推敲。第一Transactional(rollbackFor Exception.class)必须加在Service方法上而不是Controller或Mapper上。Controller负责调度Mapper负责单表操作只有Service才是有事务边界的业务单元。rollbackFor指定遇到任何异常都回滚否则Spring默认只在运行时异常时回滚RuntimeException的子类没问题但如果代码里抛的是受检异常事务不会自动回滚这会造成状态更新成功但后面的流程失败时数据不一致。第二更新状态时检查updated 0。这一步是为了处理并发场景两个用户同时申请借用同一台设备都执行了selectById都看到状态是1一个更新成功另一个更新时WHERE条件里如果带上status 1就会影响0行。所以更稳的写法是把updateStatus的SQL改成UPDATE device SET status #{newStatus} WHERE id #{id} AND status #{expectStatus}用乐观锁的思路避免超卖问题。第三借用记录的status是业务状态和设备表的status是两个维度。设备表状态从1变成2表示物理位置发生变化借用记录状态从1变成2已归还表示这笔借用业务闭环。归还设备时反过来操作更新设备状态为1更新借用记录状态为2填实际归还时间。同样是复合操作同样需要事务。3.2 状态机设计设备状态流转的合法路径与非法操作拦截设备管理系统的状态不是随便改的。一台“借出”状态的设备不能直接变成“已报废”必须先归还再走报废流程一台“维修中”的设备不能被人借用。把这些规则写死在Service层代码里比在页面端做限制可靠得多因为绕过页面直接调接口的情况太多了。常见做法是在Service里建立一个状态流转的校验方法。以设备状态为例定义常量或枚举来管理状态值然后每项操作前判断当前状态是否合法public class DeviceStatus { public static final Integer IN_STOCK 1; // 在库 public static final Integer BORROWED 2; // 借出 public static final Integer REPAIRING 3; // 维修中 public static final Integer SCRAPPED 4; // 已报废 public static boolean canBorrow(Integer currentStatus) { return IN_STOCK.equals(currentStatus); } public static boolean canRepair(Integer currentStatus) { return IN_STOCK.equals(currentStatus) || BORROWED.equals(currentStatus); } public static boolean canScrap(Integer currentStatus) { return IN_STOCK.equals(currentStatus) || REPAIRING.equals(currentStatus); } }这套校验类写好后每个Service方法里调用一次比把状态判断散落在各处要清晰得多。比如报修设备时如果设备正在维修中就不允许重复报修报废设备时借出状态的设备要先收回才能报废。这类规则不需要做成配置文件或者数据库表用代码表达反而更直观——状态机本身就是一个业务常量池和一组判断逻辑的组合。对代码质量要求更高的项目可以用枚举代替常量类枚举里直接定义每个状态的名称和可流转的下游状态列表。本文选择常量类是为了减少阅读负担实际项目中 MongoDB 或 Redis 里存状态机的做法也不少见但那张状态表往往是给运维和产品看流程的最终执行层还是代码。4. 写设备管理页面时最容易踩的3个坑分页、多表查询与前端回显4.1 分页查询用PageHelper的姿势以及count查询的性能隐患实验室设备列表动辄几百上千条记录全量查出来再在前端翻页是不现实的。SSM项目里最常见的分页方案是PageHelper插件不需要手写LIMIT #{offset}, #{pageSize}只需要在执行SQL前调用静态方法即可。Service public class DeviceService { Autowired private DeviceMapper deviceMapper; public PageInfoDevice queryDevicePage(Integer pageNum, Integer pageSize, String deviceName, Integer status) { PageHelper.startPage(pageNum, pageSize); ListDevice devices deviceMapper.selectDeviceList(deviceName, status); return new PageInfo(devices); } }Mapper XML对应的查询语句select idselectDeviceList resultTypeDevice SELECT id, device_code, device_name, model, lab_room, status, buy_date, price FROM device where if testdeviceName ! null and deviceName ! AND device_name LIKE CONCAT(%, #{deviceName}, %) /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /selectPageHelper的底层原理是在执行selectDeviceList之前拦截SQL自动改写出一条SELECT COUNT(*)来统计总数再拼上LIMIT分页。用的时候有一个坑PageHelper.startPage只对紧接着的一条查询生效如果中途多执行了一条其他SQL分页就会失效。所以不能在startPage和Mapper查询之间插入任何无关的数据库操作PageHelper官方文档把这个称为“只对第一个查询生效”。另一个坑是COUNT(*)性能。设备表数据量到了几十万条之后COUNT(*)会扫描全表。设备管理系统通常不会到这个量级但如果查询条件里有LIKE %xxx%这种前置模糊匹配索引就失效了count也会跟着慢。真到了需要优化的时候可以关掉PageHelper的count查询自己写一个count方法或者直接在页面统计栏显示“共查询到前1000条”不要精确显示总条数。4.2 多表关联查询在MyBatis里的resultMap映射dto和vo怎么选设备列表页往往需要同时显示“当前借用人”“所属实验室”“借用时间”等信息。如果只查device表这些信息拿不到如果查完设备再循环查用户就是经典的N1查询问题。常见做法是写JOIN查询用一个扩展的VO对象接收多表数据。public class DeviceBorrowVO { private Integer id; private String deviceCode; private String deviceName; private String model; private String labRoom; private Integer status; private String borrowerName; // 关联查询出来的字段 private Date borrowTime; // 关联查询出来的字段 private Date expectReturnTime; // getter/setter省略 }对应的Mapper XMLselect idselectBorrowListVO resultTypeDeviceBorrowVO SELECT d.id, d.device_code AS deviceCode, d.device_name AS deviceName, d.model, d.lab_room AS labRoom, d.status, u.real_name AS borrowerName, b.borrow_time AS borrowTime, b.expect_return_time AS expectReturnTime FROM device_borrow b LEFT JOIN device d ON b.device_id d.id LEFT JOIN sys_user u ON b.user_id u.id WHERE b.status 1 ORDER BY b.borrow_time DESC /select注意这里resultType直接用了VO类靠AS别名把列名和VO属性对起来。如果不想写别名可以用resultMap显式配置但字段多的时候XML会很长。我的习惯是查询结果扁平、不涉及嵌套对象用resultType加别名查询返回一对多结构比如一个设备对应多条借用记录用resultMap的collection标签。设备管理系统的页面查询基本都是扁平的resultType够用。有人会把这种查询结果类命名为vo放在vo包下还有人命名为dto放在dto包下。两种命名都没问题区别在于语义DTO侧重传输VO侧重视图展示。同一张列表页的查询结果本质上就是为前端展示服务的叫DeviceBorrowVO更准确。4.3 前端回显时日期格式化、状态数字转文字、下拉框默认选中的处理后端把数据查出来了最后一步是页面展示。JSPJSTL是SSM项目最常见的组合这里有几个隐蔽的坑。第一个是日期格式化。MySQL的DATETIME类型通过MyBatis查到Java里是java.util.Date对象JSP页面直接${device.buyDate}显示输出的是Tue May 14 10:30:00 CST 2024这种格式完全不能看。有两个方案一是在实体类的getBuyDate()上加JsonFormat或DateTimeFormat注解但那是给JSON用的对JSP无效二是用JSTL的fmt标签格式化。% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % tdfmt:formatDate value${device.buyDate} patternyyyy-MM-dd//td第二个是状态数字转文字。数据库里存的是1、2、3、4页面上不能直接显示数字。可以在实体类里加一个getStatusDesc()方法返回状态对应的中文描述public String getStatusDesc() { switch (this.status) { case 1: return 在库; case 2: return 借出; case 3: return 维修中; case 4: return 已报废; default: return 未知; } }页面直接${device.statusDesc}即可。这样做的优势是状态文字和状态值只在一个地方维护不会出现页面里写死多个if判断导致改状态值后文案不同步的问题。第三个是下拉框回显。编辑设备信息时设备状态下拉框需要默认选中当前设备的状态值如果用select硬编码选项很容易忘记加selected。推荐写法是JSP里用三元表达式select namestatus option value1 ${device.status 1 ? selected : }在库/option option value2 ${device.status 2 ? selected : }借出/option option value3 ${device.status 3 ? selected : }维修中/option option value4 ${device.status 4 ? selected : }已报废/option /select这个逻辑放页面上简洁直接不需要把整个页面交给前端框架。如果项目改用Vue或React做前后端分离上述JSP技巧自然就失效了替换方案是初始化数据时把status赋值给Vue实例的form.status再配合v-model实现选中。5. 用Maven打包时值得顺手检查的4个验证点SSM项目开发完最终交付形态通常是war包加SQL脚本。用Maven打包之前建议按下面4个验证点检查一遍。mvn clean package -DskipTests -Pprod第一条命令执行完后检查target/目录下生成的war包大小是否合理。如果war包只有几KB大概率是webapp目录下的静态资源没打进去检查pom.xml里maven-war-plugin的webResources配置。如果war包几十MB检查是否把node_modules或target目录一并打进去了在pom.xml里加excludes排除。第二条验证配置文件的jdbc.properties里数据库连接信息是否与交付的.sql脚本一致。很多SSM项目交付后打不开原因就是数据库密码没改成用户本地的密码。常见做法是spring-datasource.xml里读取jdbc.properties打包时用Maven Profile切换不同环境的配置但课程设计级别的交付可以直接在SQL脚本头部注释里写清楚数据库名和账号密码。第三条检查事务配置里是否启用了注解驱动。Spring和Spring MVC配置文件容易漏配tx:annotation-driven导致Service层Transactional不生效。验证方式很简单在borrowDevice方法的deviceMapper.updateStatus之后手动throw new RuntimeException再执行一次借用操作看设备状态会不会变回原来的值。如果没变说明事务没生效必须检查配置。第四条确认MyBatis的Mapper XML文件打包后还在classes目录里。Maven默认只把src/main/resources下的文件打进classpath如果XML文件放在src/main/java目录下需要额外配置build资源目录。用jar tf 目标war包查看是否存在XML文件是最直接的验证方式。再补一个实用技巧用浏览器开发者工具看接口响应时注意HTTP状态码200但页面白屏的情况。这类问题八成是JSP渲染时抛了异常但异常被Spring MVC吞掉了建议在web.xml里配一个全局错误页面把异常信息输出到页面上。error-page exception-typejava.lang.Throwable/exception-type location/WEB-INF/views/error.jsp/location /error-pageerror.jsp里用${exception.message}打印异常信息排错效率远高于看日志翻堆栈。等彻底修完再把错误页换成通用提示。本文还有配套的精品资源点击获取