JSP教学设备报修系统实战:状态机与Servlet闭环设计
2026/9/13 21:00:11 网站建设 项目流程

简介:这套毕业设计资源围绕高校教学设备报修场景,基于JSP与MySQL实现B/S架构的管理平台,面向计算机相关专业学生、毕业设计开发者及需要快速搭建报修系统的技术人员。资源共662个文件,压缩包约15.94MB,其中包含87个JSP页面、29个JS脚本、39个CSS样式,以及419个GIF演示图与32张JPG图片等,覆盖前端页面、后端逻辑与界面素材;另含SQL数据库脚本、Eclipse工程配置文件和论文文档,便于导入开发环境后直接运行或二次修改。同时资料内配有演示视频,可辅助理解系统操作与设计说明,适合学习JSP项目开发、Servlet/JSP整合与数据库设计的入门及进阶人群。目前已有200人学习下载,可作为课程设计或毕业设计的完整参考。整体架构包含系统首页、用户注册、设备报修、后台管理等功能模块,从需求分析、数据库设计到详细实现均有说明,能帮助读者节省搭建时间,快速掌握报修系统从零到一的开发流程。

1. 教学设备报修系统,一个 JSP 课设能不能写出闭环业务

设备报修系统是 JSP 技术栈里被选次数最多的课设题目之一,原因很直白:业务边界清楚、角色不超过四种、核心对象只有“报修单”这一张表。但“能跑”和“能答辩”之间差着的恰恰是一条业务闭环——提交报修之后由谁受理、怎么派单、维修进度去哪里看、结束后如何归档,这四步只要断一环,演示现场就会被问住。这篇博客不写空框架,直接按一个可运行的 JSP 教学设备报修系统来拆:从数据模型和状态机开始,到 JSP + Servlet + DAO 的实现链路,再到 Tomcat 部署时最让人头疼的 JSP 编译与 class 定位,最后补三个能在答辩里加分的小改造。正在做课设或开题的在校生可以照路径走,想对比 JSP 与 Spring Boot 差异的开发者可以直接看第 5 部分。

2. JSP教学设备报修系统的角色权限与四态数据模型

2.1 角色只要三种,状态却要四段

先决定权限模型,再写页面,这是做 JSP 项目最不会返工的顺序。教学设备报修系统最常见的角色划分是三种:报修人(学生或任课教师)、维修员(后勤或实验室管理员)、系统管理员(教务处)。也有人把“审核人”单独拆出来,但那样就要多写一套审核页面、多一组动态 SQL,工作量上并不划算。三种角色的页面复用度很高,列表页和表单页是同一套 JSP,只是查询条件不同。

状态流转比角色更容易被忽略。“待受理 → 维修中 → 已完成”看起来够了,实际演示时你会发现少了两个口子:报修人提交后想撤销怎么办?维修完成后要不要留痕存档?所以完整一点的状态机应该是四个节点:

状态值状态名可执行动作操作角色
1待受理派单、撤销管理员 / 报修人
2维修中提交维修结果维修员
3已完成评价、归档报修人 / 管理员
4已归档仅浏览

这个设计在答辩里非常占分。导师问“为什么不能从待受理直接跳已完成”,你可以说:派单动作把报修单绑定到具体维修员,是责任闭环的第一步;已完成状态需要维修员先填写维修结果和耗材,管理员确认后才能归档。把状态值设计成 int,再配一个常量类或 Java 枚举,比用字符串“pending / doing”省空间,也比散落的魔法数字更安全。

2.2 三张核心表,字段怎么定

不引入复杂外键,也不把权限表单独拆出来,三张表就能支撑整个业务:sys_userequipmentrepair_order。如果教室和实验室需要单独统计,可以在设备表上加一个location字段,不必额外建房间表。核心建表 SQL 如下:

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '加盐后的密码', role TINYINT NOT NULL DEFAULT 1 COMMENT '1报修人 2维修员 3管理员', real_name VARCHAR(32) COMMENT '真实姓名', phone VARCHAR(20) COMMENT '联系电话', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE equipment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_no VARCHAR(32) NOT NULL COMMENT '设备编号,如 DM-2024-001', name VARCHAR(64) NOT NULL COMMENT '设备名称', location VARCHAR(128) COMMENT '存放位置,如 教学楼A301', status TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1报修中 2报废', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备表'; CREATE TABLE repair_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '报修单号', device_id BIGINT NOT NULL COMMENT '设备ID', reporter_id BIGINT NOT NULL COMMENT '报修人ID', assignee_id BIGINT DEFAULT NULL COMMENT '维修员ID', description VARCHAR(500) NOT NULL COMMENT '故障描述', priority TINYINT DEFAULT 2 COMMENT '1低 2中 3高', status TINYINT NOT NULL DEFAULT 1 COMMENT '1待受理 2维修中 3已完成 4已归档', repair_result VARCHAR(500) COMMENT '维修结果', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL COMMENT '完成时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修单表';

这里有几个容易在后期翻车的决策。order_no不要用自增 ID 替代,报修单号要展示给用户看,也经常被打印出来贴在设备上,格式建议是“BX + 年月日 + 三位流水号”,生成逻辑放在 Servlet 或独立工具类里。equipment.statusrepair_order.status存在语义重叠,约定为“设备状态由最近一条未完结的报修单决定”,不需要写触发器去同步。物理外键可以不加,学校机房的老 MySQL 版本对级联限制较多,逻辑外键配合索引是更省心的做法。

2.3 初始数据与演示数据的准备

答辩演示最怕打开页面上全是空表。初始化脚本里要把三种角色各建一个账号,再插入几台设备和两条不同状态的报修单,前台页面一打开就能看到列表、状态标签和操作按钮的效果。密码字段要统一加密,不要明文入库,课设里用 MD5 加固定盐足够说明安全意识。

INSERT INTO sys_user (username, password, role, real_name, phone) VALUES ('admin', MD5('admin123'), 3, '教务处管理员', '13800001111'), ('repair01', MD5('123456'), 2, '张师傅', '13800002222'), ('student01', MD5('123456'), 1, '李同学', '13800003333'); INSERT INTO equipment (device_no, name, location, status) VALUES ('DM-2024-001', '多媒体投影仪', '教学楼A301', 1), ('DM-2024-002', '智慧黑板', '实验楼B201', 0), ('DM-2024-003', '功放音响', '报告厅', 0); INSERT INTO repair_order (order_no, device_id, reporter_id, description, priority, status) VALUES ('BX20241215001', 1, 3, '投影仪开机无画面,电源指示灯亮', 3, 2), ('BX20241216002', 2, 3, '黑板触摸失效,部分区域无响应', 2, 1);

MD5()函数是 MySQL 内置的,这样写能让初始化脚本独立运行,不需要先启动 Java 应用再执行加密逻辑。演示数据要覆盖“不同优先级、不同状态”的组合,这样后面做状态筛选和统计图表时才有内容可以展示。

3. JSP 实现报修主链路:页面、Servlet、DAO 的三层写法

3.1 JSP 列表页用 JSTL 遍历,状态码转换成标签

JSP 页面里最忌讳大段 Java 脚本片段混着 HTML,即使用<%! %>把方法都写进页面,后期也基本没法维护。常见做法是 JSTL + EL 表达式,页面只负责展示和条件判断。报修列表页的核心片段如下:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> ... <table> <tr> <th>单号</th><th>设备</th><th>故障描述</th> <th>优先级</th><th>状态</th><th>提交时间</th> </tr> <c:forEach items="${orderList}" var="o"> <tr> <td>${o.orderNo}</td> <td>${o.deviceName}</td> <td title="${o.description}">${o.description}</td> <td> <c:choose> <c:when test="${o.priority == 1}">低</c:when> <c:when test="${o.priority == 2}">中</c:when> <c:when test="${o.priority == 3}">高</c:when> </c:choose> </td> <td> <c:if test="${o.status == 1}"><span class="badge warn">待受理</span></c:if> <c:if test="${o.status == 2}"><span class="badge info">维修中</span></c:if> <c:if test="${o.status == 3}"><span class="badge ok">已完成</span></c:if> </td> <td><fmt:formatDate value="${o.createTime}" pattern="yyyy-MM-dd HH:mm"/></td> </tr> </c:forEach> </table>

${o.deviceName}是联表查询后放进RepairOrder对象的冗余字段,只读展示时不需要每次都去查设备表。fmt:formatDate负责时间格式化,Java 里的Date直接输出会带毫秒数和时区后缀,通过标签统一格式是 JSP 里的标准习惯。状态和优先级各自用<c:if><c:choose>渲染,CSS 上再配不同颜色,比直接输出数字或中文字符串要直观得多。

3.2 Servlet action 参数分发与 JSON 数据返回

Servlet 是整个请求链路的入口。常见的做法是一个业务模块对应一个 Servlet,URL 的action参数决定调哪个方法,避免每个功能都新建一个 Servlet 类,web.xml也不会膨胀。报修单模块的分发逻辑可以写成:

@WebServlet("/repair") public class RepairServlet extends HttpServlet { protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if (action == null || action.isEmpty()) { action = "list"; } switch (action) { case "addPage": addPage(req, resp); break; case "add": add(req, resp); break; case "assign": assign(req, resp); break; case "finish": finish(req, resp); break; case "list": list(req, resp); break; case "my": myList(req, resp); break; default: resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } // add / assign / finish / list 等方法省略 }

@WebServlet注解从 Servlet 3.0 开始支持,Tomcat 7 以上都不用写web.xml映射。action = "list"作为默认分支,是因为列表页入口最稳定,即使有人忘了带参数也不会直接 404。每个 case 分支只负责参数解析和调用 Service,最后统一转发到 JSP 或重定向,不要在 Servlet 里写 SQL。

还有一个实际会踩的坑:报修单的“设备选择”下拉框里,需要选中设备后异步带出位置,这时候接口要返回 JSON 数组。老教程里常见的写法是import net.sf.json.JSONArray,这个 JSON-lib 包早就停更了,而且和较新 JDK 的反射机制不兼容。更稳的方案是引入 Jackson 的ObjectMapper,或手动拼接[{...},{...}]字符串返回。手动拼字符串虽然不优雅,但在只返回几个字段的课设场景里最可控,还不容易引入依赖冲突。

3.3 DAO 的 PreparedStatement 参数与事务边界

DAO 层是整个系统里测试价值最高的部分。设备报修系统的写入链路涉及三处更新:插入报修单、修改设备状态、冲洗当前维护状态。用PreparedStatement是标准做法,既防 SQL 注入,又让参数类型的设置更明确。下面是插入报修单的 DAO 方法:

public int createOrder(RepairOrder order) throws SQLException { String sql = "INSERT INTO repair_order " + "(order_no, device_id, reporter_id, description, priority, status) " + "VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, order.getOrderNo()); ps.setLong(2, order.getDeviceId()); ps.setLong(3, order.getReporterId()); ps.setString(4, order.getDescription()); ps.setInt(5, order.getPriority()); ps.setInt(6, order.getStatus()); int rows = ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { order.setId(rs.getLong(1)); } } return rows; } }

Statement.RETURN_GENERATED_KEYS让 MySQL 返回自增主键,这样后续把“ID 写入日志表”和“修改设备状态”放在同一个业务方法里时,报修单 ID 才能被复用。try-with-resources保证ConnectionPreparedStatement在异常时自动关闭,老项目里常见的finally { conn.close(); }写法可以简化掉。

事务边界放在 Service 层,而不是 DAO 层。比如新增报修操作要做两件事:插入repair_order、把equipment.status改成 1。这两个操作必须同生共死,所以 Service 方法里先conn.setAutoCommit(false),两个 DAO 都成功后再commit(),任何一个抛异常就rollback()。如果事务放在 DAO 里,两个 DAO 各用各的连接,事务就失去了意义。

3.4 状态流转用乐观锁,避免重复派单

从待受理变成维修中,本质是一个状态机的跃迁。多人同时操作时可能出现两个管理员同时给同一张报修单派单的情况,最后结果变成被后来者覆盖。解决的办法不需要上分布式锁,一行 UPDATE 配合状态条件就够:

UPDATE repair_order SET status = 2, assignee_id = ? WHERE id = ? AND status = 1

接口收到WHERE status = 1这个条件,影响行数如果返回 0,说明这张单子已经被别人操作过,前端提示“该报修单已受理,请刷新后重试”。同理,从维修中到已完成可以用WHERE status = 2来约束。这套思路和乐观锁是一致的,把“前一个状态”作为更新条件,业务上不会有重复派单,代码里也不需要加synchronized。答辩的时候把这个点讲出来,非常加分。

4. Tomcat 部署与 JSP 编译排错:怎么定位 class 文件

4.1 部署前需要确定的项目结构与 lib 依赖

JSP 项目的部署结构比 Spring Boot 的重。用 IDEA 或 Eclipse 导出 WAR 包时,要确保这几个目录是完整的:WEB-INF/web.xmlWEB-INF/libWEB-INF/classes、页面文件直接放在根目录或WEB-INF/pages下。不少报错“404 页面找不到”其实是目录层级不对,JSP 文件放进了WEB-INF里,同时又没有在 Servlet 里转发。

MySQL 驱动 jar 要放到WEB-INF/lib下,不是放在 IDEA 的外部 Library 里就行。老生常谈但每次都会遇到:直接把mysql-connector-java-8.0.x.jar加入构建路径,应用启动正常,一执行查询就报ClassNotFoundException: com.mysql.jdbc.Driver。原因是 Tomcat 运行时的类加载器不会去找冷启动时构建工具提供的库,WEB-INF/lib才是它固定的扫描位置。把 jar 放进去之后,去部署结构页面确认没有勾选“lib 目录排除”。

4.2 JSP 编译后的 class 文件保存在哪里:work 目录定位

“JSP 编译 class 文件保存在哪里”是排查页面白屏和代码不生效问题的关键。JSP 第一次被请求时,Tomcat 会用 Jasper 引擎把它转成 Java 源文件,再编译成 class 文件,这些文件统一存放在 Tomcat 的work目录下,路径规则是:

${CATALINA_HOME}/work/Catalina/localhost/${contextPath}/org/apache/jsp/

比如通过http://localhost:8080/repair/login.jsp访问,对应文件就在:

find ${CATALINA_HOME:-/opt/tomcat}/work \ -type f -name "login_jsp.class" 2>/dev/null

Windows 环境默认路径是C:\Program Files\Apache Software Foundation\Tomcat 9.0\work\Catalina\localhost\repair\org\apache\jsp。文件命名规则是把 JSP 文件名中的点和斜杠全部替换成下划线,repair/order/list.jsp生成list_jsp.javalist_jsp.class

排查代码不生效时,直接打开这个.java源文件,就能看到 JSP 里的 HTML 标签和 Java 代码具体被翻译成了什么。很多诡异问题的根源都在这里:${pageContext.request.contextPath}在 JSP 里正常,但翻译成 Java 后拼接的字符串路径不对,导致静态资源全部 404。work目录里还能看到每次修改 JSP 后的重新编译时间,如果时间没有变化,说明浏览器可能缓存了页面,和服务器无关。

4.3 毕业设计现场最常见的六个报错

报错关键字常见原因快速定位方法
UnsupportedClassVersionError编译用的 JDK 比 Tomcat 运行的 JDK 版本高java -version和 Tomcat 启动日志版本对比
中文乱码POST 请求没设置request.setCharacterEncoding("UTF-8"),或 JSP 的pageEncoding不一致统一在过滤器里设置 UTF-8,web.xml里映射/*
404 找不到 Servlet@WebServleturlPatterns与页面action不一致打开 JSP 查看表单action属性值,再对比注解
ClassNotFoundException: com.mysql.jdbc.DriverMySQL 驱动 jar 不在WEB-INF/lib解压 WAR 包看 jar 是否真实存在于 lib 目录
JSP 页面白屏无报错RuntimeException 被 JSP 的isErrorPage吞掉,或导入类不存在去 Tomcat 的logs/localhost.yyyy-MM-dd.log翻异常堆栈
The method getXxx() is undefinedEL 表达式里属性名拼写和实体类 getter 不一致ELo.deviceName对应的必须存在getDeviceName()

最后一类报错在课设里出现频率极高。JSP 页面编译成普通 Java 类之后,EL 表达式对属性名的解析完全依赖 getter 方法,IDE 里不报错是因为没把它当 Java 源码编译,启动后一访问就是 500。改实体类方法名比改 JSP 更麻烦,因为关联页面多,所以建议属性一开始就按“驼峰命名”定好,不要出现device_nodeviceNo混用。

5. JSP 报修系统的答辩加分改造:状态机、统计页、Spring Boot 迁移

5.1 用 Java 枚举拆掉魔法数字

四态状态机刚开始是int+ 注释,代码量变大后,order.getStatus() == 2这种判断散落在 Servlet、DAO、JSP 三处,改一个状态含义要全文替换。抽出枚举类其实不复杂,但价值非常明显:

public enum RepairStatus { PENDING(1, "待受理"), PROCESSING(2, "维修中"), FINISHED(3, "已完成"), ARCHIVED(4, "已归档"); private final int value; private final String label; RepairStatus(int value, String label) { this.value = value; this.label = label; } public int value() { return value; } public String label() { return label; } public static RepairStatus from(int value) { for (RepairStatus s : values()) { if (s.value == value) return s; } throw new IllegalArgumentException("未知状态: " + value); } }

配合from(int)静态工厂,DAO 里查询结果转实体时调用RepairStatus.from(rs.getInt("status")).value(),JSP 里也能直接通过from拿到标签文本。这比在页面上写三次<c:if>更干净,也是“设计模式 Java 实现”这个热词背后最基本的思想——用类型和常量约束住状态空间。答辩时可以说“状态从值对象变成了领域对象”,这句话比“我会 CRUD”有说服力得多。

5.2 报修历史页加筛选和统计

默认的列表页做了分页后,再加两个筛选条件:优先级状态,SQL 在 Service 层拼好之后传给 DAO。这样页面直接变成一个迷你报表入口,不需要额外开发。如果报修单数据超过几百行,给查询条件加复合索引,也就是device_id + status + create_time的联合索引,查询响应从 200 毫秒降到 30 毫秒是很正常的提升。这比“优化 JVM 参数”更实在,也更容易在答辩时演示效果。

5.3 迁移到 Spring Boot 的最小代价

做完整套 JSP 系统之后,会有不少同学被问“你了解 Spring Boot 吗”。迁移并不需要重写所有代码:DAO 层的 SQL 基本不动,改成 Spring JdbcTemplate 或 MyBatis 的 Mapper;Servlet 变成@Controllerservice方法拆成@GetMapping/@PostMapping;JSP 页面替换成 Thymeleaf,但 EL 表达式的写法和th:text非常接近,学习成本不高。关键是数据库表和状态机设计不重构,意味着整条业务逻辑可以直接平移。把这样的迁移思路列成表格,作为结课报告最后一张 PPT,比自己写“项目展望”更有实际信息量。

建议再给前端页面补一个小功能:当前用户的历史报修记录里增加一个“催办”按钮,待受理状态超过 24 小时才能点击,点击后给管理员页面的列表多一个置顶标记。这个功能改动范围小,但能把报修系统的完整度从“课设水平”拉到“可用工具”水平,整个过程只需要在repair_order表加一个urgent_flag字段,并在列表查询 ORDER BY 里加上这个字段的排序规则。验证方式也很简单:造一条 25 小时前创建且状态为 1 的报修单,用报修人账号点击催办,再切到管理员账号查看该单是否排到了列表顶部。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询