尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

SSM实验室故障报修系统:状态流转、权限控制与数据库设计实战

发布时间:2026/9/29 17:15:27

资讯中心
01
ARTICLE

SSM实验室故障报修系统:状态流转、权限控制与数据库设计实战

SSM实验室故障报修系统:状态流转、权限控制与数据库设计实战
计算机实验室这种场景我太熟了的大学四年有三年的课程设计都泡在机房里电脑蓝屏、鼠标失灵、键盘按键弹不出来这种事情几乎每节课都能碰见。最痛苦的是报修方式要么口头跟实验员说一声要么在机房的登记本上写一句话过两天根本不知道有没有人处理、什么时候能修好。我当时选这个题目就是想把整个报修流程搬到线上让学生能提交、老师能审核、维修员能处理每一步状态都看得见。今天这篇博文我就把整个项目的设计思路、表结构、核心代码和踩坑过程完整拆一遍给正在做类似SSM课程设计的同学一个可以直接参考的底稿。1. 一眼看穿这个SSM项目实验室故障报修背后的完整业务链条1.1 报修流程里藏着的三种角色和六种状态别急着写代码先搞清楚一件事一个故障从发生到解决中间经过了哪些人、哪些状态。我在做这个项目之前以为就是一个简单的表单提交加后台列表但真正梳理下来才发现一条报修单至少要经过学生提交—实验员审核—维修员处理—学生确认四个环节。所以我把系统里的角色分成四种角色主要职责能做的事情学生发现故障并上报提交报修单、查看自己报修的进度实验员初次审核、判断是否属实审核报修单、驳回无效报修、指定维修员维修员实际处理故障接单、填写维修结果、标记完成系统管理员全局管理管理实验室、设备、用户、查看统计报表对应地报修单的状态我设计成六种待审核、已派单、维修中、待确认、已完成、已驳回。这个状态机是整个项目的灵魂后面所有权限控制、列表筛选、按钮显隐都围着它转。1.2 状态流转一条报修单的完整生命周期我按真实业务的节奏设计了状态流转每个状态只允许特定角色操作学生提交报修后单据进入待审核此时只有实验员能看到并处理实验员确认故障属实指派给某个维修员状态变为已派单维修员确认接单并开始维修状态变为维修中维修员填写维修结果后状态变为待确认——这一步是让学生确认故障是否真的解决了学生确认无误单据关闭状态变为已完成实验员如果觉得报修信息不实或重复提交可以直接驳回单据进入已驳回。这个设计看起来简单但有一个很关键的好处每一步操作都有明确的操作人和时间记录答辩的时候老师问你如何追踪责任你直接把维修日志表拉出来就行。2. 为什么选了SSM这种老技术栈来做课程设计2.1 Spring、SpringMVC、MyBatis各干了什么活现在很多教程上来就是SpringBoot反而把SSM当成过时技术。但我的看法不一样SSM是理解Java Web开发底层逻辑最好的组合没有之一。Spring负责的是对象管理和事务控制。比如维修员Service、报修单Service这些类如果自己new的话依赖关系会乱成一团还要自己管事务。Spring通过IoC容器统一管理这些Bean声明式事务一个注解就搞定。SpringMVC负责的是Web层的请求分发。前端的请求参数怎么绑定到Java对象、Controller方法怎么返回视图或JSON、异常怎么统一处理这套模型的清晰程度在SpringBoot里反而被约定大于配置掩盖了。MyBatis负责的就是数据库操作。它的灵活之处在于可以自己写SQL多条件动态查询、关联表查询都好控制比Hibernate那种全自动ORM更直观尤其适合报表统计这种复杂SQL场景。2.2 SpringBoot和SSM到底该怎么选你可能会问既然SpringBoot更省事为什么不直接用它我的建议是如果课程设计只要求做一个能跑的系统SpringBoot确实更快但如果老师要求你讲清楚原理或者你想借这个项目搞懂Web开发的核心SSM是更扎实的训练。我当时选SSM还有一个现实原因参考代码多。网上搜java_ssm实验室故障报修系统这种题目大部分是SSM版本的遇到问题容易找到解决方案。这对于独立做项目的学生来说太重要了。2.3 项目分包结构的设计分包这个事很多同学随便建几个包就往里塞类后面越写越乱。我按职责分成这样com.campus.lab ├── controller // 控制层接收请求、参数校验 ├── service // 业务层状态流转、事务管理 ├── dao // 数据层MyBatis的Mapper接口 ├── entity // 实体类对应数据库表 ├── interceptor // 拦截器登录验证、权限控制 ├── utils // 工具类 └── vo // 视图对象展示给前端的数据封装注意我把vo单独提出来了因为entity是直接对应表的比如User表里有password字段但返回给前端时不能把密码带出去这时候就需要一个UserVO来承接展示层的数据需求。这个细节答辩时提一句老师会觉得你考虑得周全。3. 数据库建模七张核心表怎么撑起整个报修闭环3.1 表结构设计的先后顺序建表顺序其实是有讲究的我先建基础数据表再建业务表因为业务表要引用基础表的外键。基础表三张实验室表labCREATE TABLE lab ( id int NOT NULL AUTO_INCREMENT, lab_name varchar(100) NOT NULL COMMENT 实验室名称, location varchar(255) DEFAULT NULL COMMENT 所在位置, device_count int DEFAULT 0 COMMENT 设备数量, status tinyint DEFAULT 1 COMMENT 状态1正常 0停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设备表deviceCREATE TABLE device ( id int NOT NULL AUTO_INCREMENT, lab_id int NOT NULL COMMENT 所属实验室, device_code varchar(50) NOT NULL COMMENT 设备编号, seat_no varchar(20) DEFAULT NULL COMMENT 座位号/位置标识, status tinyint DEFAULT 1 COMMENT 设备状态1可用 2故障 3维修中, remark varchar(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), KEY idx_lab_id (lab_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用户表userCREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, real_name varchar(50) DEFAULT NULL, role varchar(20) NOT NULL COMMENT STUDENT/LAB_ADMIN/REPAIR_USER/SYS_ADMIN, phone varchar(20) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 业务表设计报修单和维修日志报修单表repair_order是整个系统的核心我放了比较多的字段目的是把完整信息都记录清楚CREATE TABLE repair_order ( id int NOT NULL AUTO_INCREMENT, order_no varchar(30) NOT NULL COMMENT 报修单号, device_id int NOT NULL COMMENT 报修设备, user_id int NOT NULL COMMENT 报修人, fault_type varchar(50) NOT NULL COMMENT 故障类型, fault_desc varchar(500) DEFAULT NULL COMMENT 故障描述, image_url varchar(255) DEFAULT NULL COMMENT 故障照片路径, urgency_level tinyint DEFAULT 1 COMMENT 紧急程度1普通 2紧急 3非常紧急, status varchar(20) NOT NULL DEFAULT PENDING_REVIEW COMMENT 状态, assign_user_id int DEFAULT NULL COMMENT 指派的维修员, create_time datetime DEFAULT CURRENT_TIMESTAMP, assign_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, close_time datetime DEFAULT NULL, reject_reason varchar(255) DEFAULT NULL COMMENT 驳回原因, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;维修日志表repair_log用来记录每个环节的操作痕迹CREATE TABLE repair_log ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, operator_id int NOT NULL, action varchar(50) NOT NULL COMMENT 操作类型, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节我特别要说order_no虽然放在业务表里但生成逻辑我建议放在Service层用前缀日期随机数的方式比如BX20241215001。不要在数据库里自增当单号因为自增ID很容易被猜到也看不出业务含义。3.3 数据字典与状态常量管理状态值我没有用数字1、2、3这种而是用了语义化的字符串比如PENDING_REVIEW、ASSIGNED。这样做的最大好处是代码可读性高看代码的时候不用查表就知道这个状态是什么意思。同时我在Java里定义一个常量类来统一管理public class OrderStatus { public static final String PENDING_REVIEW PENDING_REVIEW; public static final String ASSIGNED ASSIGNED; public static final String IN_REPAIR IN_REPAIR; public static final String REPAIRED REPAIRED; public static final String CLOSED CLOSED; public static final String REJECTED REJECTED; }你可能会想为什么不用枚举因为MyBatis对枚举的处理需要额外配置TypeHandler课程设计阶段用常量类最省事也不容易出问题。4. 核心功能实现从故障上报到维修完成的完整代码链路4.1 故障上报前端表单到后端入库的一整套规范学生端上报故障是系统最前端的入口。前端用了一个表单页提交内容包括选择实验室、选择设备、故障类型、故障描述、上传照片、紧急程度。后端Controller接收的时候我强调几个容易忽略的规范参数绑定不能用散字段。我封装了一个RepairOrderSubmitVO来承接前端数据PostMapping(/order/submit) public Result submit(RequestBody Valid RepairOrderSubmitVO vo, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { return Result.error(未登录); } return repairOrderService.submit(vo, loginUser.getId()); }校验不能只在前端做。比如故障描述不能为空、设备必须存在这些Controller层面就要校验我用参数校验注解省了不少事public class RepairOrderSubmitVO { NotNull(message 设备ID不能为空) private Integer deviceId; NotBlank(message 故障类型不能为空) private String faultType; Size(max 500, message 故障描述不能超过500字) private String faultDesc; }上传的图片要限制大小和类型。我当时用的是本地存储文件路径存到数据库实际项目中一般用OSS但课程设计本地存储完全够用。4.2 状态流转的核心业务校验放Service层状态流转如果只在前端控制按钮显隐会存在严重的安全隐患——用户可以直接调接口跳过中间状态。所以我在Service层做了双重校验。拿实验员审核派单这个操作举例Transactional public void assignOrder(Integer orderId, Integer repairUserId, Integer operatorId) { Rep成立airOrder order repairOrderMapper.selectById(orderId); if (order null) { throw new BizException(报修单不存在); } // 关键校验只有待审核状态的单据才能派单 if (!OrderStatus.PENDING_REVIEW.equals(order.getStatus())) { throw new BizException(当前状态不可派单); } // 校验操作人是否有权限 User operator userMapper.selectById(operatorId); if (!LAB_ADMIN.equals(operator.getRole()) !SYS_ADMIN.equals(operator.getRole())) { throw new BizException(无操作权限); } // 更新状态 RepairOrder updateOrder new RepairOrder(); updateOrder.setId(orderId); updateOrder.setStatus(OrderStatus.ASSIGNED); updateOrder.setAssignUserId(repairUserId); updateOrder.setAssignTime(new Date()); repairOrderMapper.updateById(updateOrder); // 记录日志 RepairLog log new RepairLog(); log.setOrderId(orderId); log.setOperatorId(operatorId); log.setAction(ASSIGN); log.setRemark(派单给 repairUserId); repairLogMapper.insert(log); }看到没我做了三件事查原状态、校验角色、更新状态并写日志。这个模式在所有状态流转操作里都复用了。Transactional保证更新和日志要么都成功要么都失败不会出现状态改了但日志没记的脏数据。这里我踩过一个坑必须提醒你抛业务异常记得要全局捕获不然前端只会收到一堆看不懂的500页面。我写了一个GlobalExceptionHandler用RestControllerAdvice统一处理业务异常返回带错误码的JSON消息这样前端才能正确提示用户当前状态不可派单。4.3 维修员耗时统计与超时提醒这个是我自己加上去的功能也是答辩时的加分点。维修员接单后系统记录接单时间维修完成时记录完成时间中间耗时就是实际维修时长。我还在Service里加了一个定时任务每天检查一下所有状态为IN_REPAIR的单子如果超过48小时还没完成就把这个单子的紧急程度自动升级同时给维修员发一条站内信提醒Component public class OverdueChecker { Scheduled(cron 0 0 9 * * ?) public void checkOverdueOrders() { ListRepairOrder orders repairOrderMapper.selectOverdueOrders(); for (RepairOrder order : orders) { notificationService.sendMessage( order.getAssignUserId(), 您负责的报修单[ order.getOrderNo() ]已超过48小时未完成请尽快处理 ); } } }注意Scheduled需要Spring配置文件里开启task:annotation-driven/或者XML里配置好任务扫描路径。这个功能我一开始怎么配都没反应后来发现是SpringMVC的DispatcherServlet配置里漏了context:component-scan的包范围把task包排除了。这也是SSM项目常见的坑Spring容器和SpringMVC容器各扫各的包定时任务、事务这些如果只被一个容器扫描很容易出奇怪的问题。5. 权限控制、并发问题、分页查询三个最容易翻车的技术点5.1 登录拦截器与角色权限的双层控制SSM项目里做权限控制最常见的方式是拦截器加Session判断。我定义了一个LoginInterceptor在preHandle方法里先判断用户是否登录public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { User user (User) request.getSession().getAttribute(loginUser); if (user null) { // 判断是不是Ajax请求 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; }然后单独写一个PermissionInterceptor针对/admin/**、/repair/**这类URL做细粒度的角色判断。这里有个设计技巧SpringMVC的拦截器只配置了URL匹配角色校验逻辑写在业务代码里比如派单操作时校验当前用户角色。因为一个URL需要区分不同角色时拦截器不好直接表达这个逻辑放在Service层里更灵活。5.2 防止重复提交与并发抢单真实场景里存在这种情况实验员觉得A维修员忙同时把一张单子派给两个人或者用户手抖点提交多按了几次按钮生成了两张一模一样的报修单。针对重复提交我在前端加按钮禁用后端在Controller层做了一个重复判断// 判断同一用户同一设备5分钟是否已有未完成的报修单 long count repairOrderMapper.countUnfinishedByUserAndDevice( userId, deviceId, DateUtils.addMinutes(new Date(), -5) ); if (count 0) { throw new BizException(请不要重复提交该设备的报修单); }针对并发派单我的方案是更新时加一个条件判断UPDATE ... WHERE id ? AND status PENDING_REVIEW然后看受影响行数如果返回0说明状态已经被别人改了int rows repairOrderMapper.assignOrderWithCondition(orderId, repairUserId, OrderStatus.PENDING_REVIEW); if (rows 0) { throw new BizException(该报修单已被其他人处理请刷新后重试); }这种做法叫乐观锁不需要在表里加version字段而是通过更新条件来保证数据一致性。这个点你可以重点去理解后面工作中并发场景也经常用这套思路。我当时用JMeter模拟了50个并发请求同时去派同一张单最终只有1个请求成功其余全部返回了友好提示测试结果在答辩现场演示出来效果非常好。5.3 MyBatis动态SQL实现多条件联合查询报修单列表是整个系统用得最多的页面。学生看自己的单、实验员看所有单、维修员看派给自己的单角色不同查询条件就不同。再加上状态筛选、时间范围筛选、关键字搜索SQL就变得很灵活。我在MyBatis的XML里用where和if标签动态拼接条件select idselectOrderList resultTypecom.campus.lab.vo.RepairOrderVO SELECT r.id, r.order_no, r.fault_type, r.fault_desc, r.status, r.create_time, d.device_code, d.seat_no, l.lab_name, u.real_name AS submit_user_name, ru.real_name AS assign_user_name FROM repair_order r LEFT JOIN device d ON r.device_id d.id LEFT JOIN lab l ON d.lab_id l.id LEFT JOIN user u ON r.user_id u.id LEFT JOIN user ru ON r.assign_user_id ru.id where if testuserId ! null AND r.user_id #{userId} /if if testlabId ! null AND l.id #{labId} /if if teststatus ! null and status ! AND r.status #{status} /if if testkeyword ! null and keyword ! AND (r.order_no LIKE CONCAT(%, #{keyword}, %) OR d.device_code LIKE CONCAT(%, #{keyword}, %)) /if if teststartTime ! null AND r.create_time gt; #{startTime} /if if testendTime ! null AND r.create_time lt; #{endTime} /if /where ORDER BY choose when testsortField create_timer.create_time/when otherwiser.id/otherwise /choose DESC /select动态SQL的三个关键细节用LEFT JOIN而不是INNER JOIN保证即使关联用户已被删除(SQL里最好不要物理删除用户只做逻辑删除)报修单仍然能查出来排序字段不要直接拼接用户输入我用choose做了白名单判断防止SQL注入。这一点是大忌课程设计如果被老师发现排序字段裸拼印象分会大打折扣日期条件注意大小比较XML里号要转义成gt;转义成lt;不然XML解析直接报错。5.4 PageHelper分页的引入与常见坑列表页不可能一次全查出来我引入了PageHelper分页插件。引入分页看似简单但有两个坑我花了很久才搞定。一个是分页失效。PageHelper的原理是在MyBatis执行查询前拦截SQL自动拼上LIMIT。但如果你的查询SQL后面跟着多条语句或者查询方法里先执行了其他SQL分页就会乱套。最典型的情况是查询列表时还要查COUNT(*)PageHelper会把所有SQL都加上LIMIT结果统计接口直接报错。我的解决办法是统计查询单独写SQL不要依赖PageHelper的count自动生成列表查询的第一条SQL必须是目标查询语句。另一个是分页插件版本兼容问题。我用的PageHelper 5.1.10配MyBatis 3.4.6没问题但如果你用新版MyBatis(3.5.x)必须换上配套的PageHelper否则分页完全不生效。我在排查这个问题的过程中一度怀疑自己SQL写错了后来在启动日志里看到一个拦截器注册失败的WARN才反应过来是依赖版本冲突。所以我的建议是pom.xml里统一用MyBatis 3.4.6 PageHelper 5.1.10这个组合稳定可靠。6. 课程设计加分项数据分析、消息通知与答辩前的检查清单6.1 故障统计报表如何用一条SQL搞定管理人员除了处理报修更关心哪些类型的故障最多哪个实验室故障率最高维修平均耗时多长。这些统计用聚合SQL非常高效我写了三个核心统计接口。故障类型占比select idcountByFaultType resultTypemap SELECT fault_type AS name, COUNT(*) AS value FROM repair_order WHERE create_time BETWEEN #{startTime} AND #{endTime} GROUP BY fault_type ORDER BY value DESC /select各实验室故障数量排名(按最近30天)select idcountByLab resultTypemap SELECT l.lab_name AS name, COUNT(r.id) AS value FROM repair_order r JOIN device d ON r.device_id d.id JOIN lab l ON d.lab_id l.id WHERE r.create_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY l.id, l.lab_name ORDER BY value DESC /select这些统计结果用ListMapString, Object返回前端遍历出来画成饼图、柱状图用的是ECharts整个管理后台瞬间就有了大数据分析的味道。答辩的时候老师看到图表基本都会追问一句数据怎么来的这时候你直接把SQL讲一遍这段对话基本就成了你答辩的高光片段。6.2 站内信与短信提醒的消息通知设计报修状态变化时相关人员需要及时得到通知。我用一个notification表实现站内信同时接入一个简单邮件发送工具public class NotificationService { public void sendMessage(Integer userId, String content) { // 插入站内信记录 Notification notification new Notification(); notification.setUserId(userId); notification.setContent(content); notification.setRead(false); notificationMapper.insert(notification); // 如果用户绑定了邮箱则发送邮件 User user userMapper.selectById(userId); if (StringUtils.isNotBlank(user.getEmail())) { emailSender.sendSimpleMail(user.getEmail(), 实验室设备报修系统通知, content); } } }注意站内信和邮件发送的先后顺序先插库再发邮件。这样即使邮件发送失败用户登录系统后还是能在收件箱看到通知。邮件发送用了JavaMailSender需要自己在Spring配置里定义好mailSender这个Bean。6.3 答辩前必须自查的六个细节清单最后一个部分分享一份检查清单是我做完整项目后总结出来的照着走一遍能避免很多演示翻车默认数据要全。系统里必须预置好各角色测试账号学生、实验员、维修员、系统管理员各一个而且要在演示文档里明确写出来。老师可能现场要试你总不能临时去数据库insert账号。三种状态路径都要演示。建议你提前准备好一条正常完成单和一条被驳回单的数据演示时先看历史数据再走一遍新流程完整展示从提交到确认全链路这样流程闭环讲得非常顺。404和500页面要做友好处理。这虽然不是功能需求但老师如果操作时看到一个Tomcat默认的报错页面印象分会掉很多。我写了一个自定义错误页把异常信息转换成提示文字还打上了系统已记录该问题这样的话术。数据库连接配置别写死本机IP。有些老师会要求你在他的电脑上跑一遍你数据库连接写localhost没问题但如果他数据库密码不一样直接起不来。建议把jdbc.properties里的账号密码做成可配置并写好README说明。接口尽量统一返回格式。我用一个Result类封装了code、msg、data三个字段前端统一处理。不要一会儿返回一个对象一会儿返回一个Map代码风格不统一会让代码评审环节很难看。导出功能值得拥有。我加了一个报修单Excel导出功能用POI生成表格管理员可以把一个月的报修记录导出来归档。这个功能日常工作非常实用也是很多人没做但老师觉得不错的加项。6.4 我对这个项目的整体复盘做这个项目前后花了我大概三周时间第一周梳理业务和数据库第二周写核心功能第三周在优化权限、修分页Bug和做统计报表。最大的体会不是技术有多难而是业务状态流转的设计决定了项目一半以上的复杂度。你如果没有先把角色、状态、权限画清楚代码写到后面会不断推翻重来。另一个收获是排查Bug的思路。SSM项目因为没有SpringBoot那种自动配置的魔法出了问题基本都是自己追链路请求到没到Controller、Service走没走到、SQL能不能查出来。这个过程虽然麻烦但恰恰把Java Web开发该掌握的每一个环节都练到了。我的个人建议是有时间的话在做完课程设计后把SSM版本重构一遍到SpringBoot你会发现框架差异对你的代码几乎没有影响因为你已经理解了底层逻辑。那种好像会了的感觉会变成真的会了。这也是为什么我到现在还是推荐你认认真真做完这个SSM项目的报修系统。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。