简介:这是基于Java、JavaScript、CSS开发的宿舍报修系统完整设计源码,面向高校后勤、物业管理部门及JavaWeb学习者,用于实现学生在线报修、维修进度跟踪与后台管理一体化流程。资源共137个文件,压缩包14.14MB,包含36个class文件、34个JSP页面、18个java源文件、16张JPG图片、8个JavaScript脚本、8个JAR库、6个XML配置、4个CSS样式及项目配置文件等,清晰覆盖后端业务逻辑、前端交互、页面展示与系统配置各层。已有114人学习下载。源码结构完整,可直接导入Idea运行研究,其中RepairController、StudentController、MangerController等类展示了报修、学生与管理端核心接口实现,适合作为课程设计、毕业设计或中小型报修管理系统的参考范式,能帮助读者快速理解前后端分离开发模式下的JSP+Servlet+JavaBean实际项目组织方式。
1. 宿舍报修系统为什么适合做Java、JavaScript、CSS的组合靶场
宿舍报修系统看起来是个小项目,实际把Web开发的主线都占全了:学生用表单提交报修单,维修后台要能改状态,列表需要异步刷新,不同状态的视觉效果要区分。基于Java、JavaScript、CSS的宿舍报修系统设计源码,正好把这三层分开:Java负责接口和状态流转,JavaScript负责页面交互,CSS负责把状态变成可视化的颜色和布局。对新手来说,这套组合比纯模板渲染的JSP项目更有价值,因为能在一个项目里看到前端怎么调接口、后端怎么返回JSON;对初级工程师来说,它又是一个能在面试里讲清的完整闭环。下面按实体类、状态机、fetch交互和响应式样式一层层拆开讲。
2. 用Java实现报修单的核心流转:实体类、Service与接口
2.1 用实体类把报修单的数据结构钉死
先不管界面长什么样,后端第一件事是定义RepairOrder。常见做法是让实体类和数据库表字段一一对应,这样MyBatis或者JDBC的映射逻辑最省事。代码里我习惯用包装类型Integer而不是int,因为数据库字段允许为null,查询数据库时出现null赋给int会直接NPE。
public class RepairOrder { private Integer id; private String dormNo; // 宿舍号,例如 3-201 private String reporter; // 报修人姓名 private String phone; // 联系电话 private String description; // 故障描述 private Integer status; // 0待受理 1派单中 2维修中 3已完成 4已取消 private java.util.Date createTime; private java.util.Date updateTime; public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } // 其余 getter/setter 略 }字段里最值得说明的是status。很多新手会用String字段存“待受理”这样的中文,这会在后续判断时写出一堆字符串比较,万一数据库里存了错别字,查都查不出来。用Integer保存状态码,再在展示层去翻译成中文,是最稳妥的做法。createTime和updateTime可以直接交给数据库的CURRENT_TIMESTAMP维护,Java侧不赋值。
有些源码包会使用Lombok的@Data,能省掉getter/setter,但如果你要拿到面试场景里讲,还是建议至少手写关键字段的访问方法,省得被问到时说不出原理。实体类本身不承担业务逻辑,它只是一个数据容器,真正决定状态走向的是Service层。
2.2 Service层处理状态流转,而不是把if写到Controller
在宿舍报修系统里,报修单不是随便什么状态都能跳过去的。待受理可以把单子派给维修工,也可以取消;维修中只能变成已完成;已经完成的单子不能再改回维修中。这套规则如果散落在多个接口里,后续维护非常容易漏判。我一般会用一个枚举把状态码和允许的跳转方向集中定义。
public enum RepairStatus { PENDING(0), DISPATCHED(1), REPAIRING(2), DONE(3), CANCELED(4); private final int code; RepairStatus(int code) { this.code = code; } public int getCode() { return code; } public static RepairStatus fromCode(int code) { for (RepairStatus s : values()) { if (s.code == code) return s; } throw new IllegalArgumentException("未知的状态码: " + code); } public boolean canTransferTo(RepairStatus target) { switch (this) { case PENDING: return target == DISPATCHED || target == CANCELED; case DISPATCHED: return target == REPAIRING; case REPAIRING: return target == DONE; default: return false; } } }参数说明:fromCode用来把数据库里读出来的int转成枚举,避免到处写魔法数字。canTransferTo返回true代表允许迁移,PENDING允许派单和取消两步,其他状态都只允许单点前进。这样即使前端把按钮发错了,后端仍然会拒绝非法流转。
Service里的更新代码可以这样写:
public boolean updateStatus(Integer orderId, Integer targetCode) { RepairOrder order = repairDao.findById(orderId); if (order == null) { throw new RuntimeException("报修单不存在"); } RepairStatus current = RepairStatus.fromCode(order.getStatus()); RepairStatus target = RepairStatus.fromCode(targetCode); if (!current.canTransferTo(target)) { throw new IllegalStateException("status is not transferable"); } return repairDao.updateStatus(orderId, targetCode); }这段逻辑里有一个值得注意的点:状态校验要放在权限校验之后,否则修理工可以拿自己的token把别人的单子改成已完成。一般源码包会把角色判断塞在同一个Service方法里,这里为了简化只保留了状态机。如果你要做权限,可以在方法入口再加一个operatorRole参数,用字符串区分。
下面这张表是状态迁移的完整约束,放在数据库设计文档里也很合适:
| 当前状态 | 可跳转状态 | 典型操作 |
|---|---|---|
| 0 待受理 | 1 派单中、4 已取消 | 管理员派单或学生取消 |
| 1 派单中 | 2 维修中 | 维修工开始维修 |
| 2 维修中 | 3 已完成 | 维修工提交完成 |
| 3 已完成 | 无 | 流程结束 |
| 4 已取消 | 无 | 流程结束 |
这里的约束如果放在数据库触发器里也能实现,但会降低可调试性。放在Java枚举里,单元测试可以覆盖每一种组合,比翻触发器脚本快很多。
2.3 用Servlet接口输出JSON给前端调用
既然前端是JavaScript负责,后端接口最好是输出JSON而不是直接渲染HTML。很多源码包用的是Servlet,也有用Spring Boot的,两者在前端调用方式上没有区别。下面是一个最简的Servlet写法,适合内部学习和二次开发,不需要引入额外框架。
@WebServlet("/api/repair/create") public class RepairCreateServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("utf-8"); resp.setCharacterEncoding("utf-8"); resp.setContentType("application/json"); String dormNo = req.getParameter("dormNo"); String reporter = req.getParameter("reporter"); String phone = req.getParameter("phone"); String description = req.getParameter("description"); if (dormNo == null || dormNo.trim().isEmpty() || description == null || description.trim().isEmpty()) { resp.getWriter().write("{\"code\":1,\"msg\":\"宿舍号或故障描述不能为空\"}"); return; } RepairOrder order = new RepairOrder(); order.setDormNo(dormNo.trim()); order.setReporter(reporter == null ? "匿名" : reporter.trim()); order.setPhone(phone); order.setDescription(description.trim()); order.setStatus(0); RepairService service = new RepairService(); boolean ok = service.create(order); if (ok) { resp.getWriter().write("{\"code\":0,\"msg\":\"提交成功\"}"); } else { resp.getWriter().write("{\"code\":1,\"msg\":\"数据库写入失败\"}"); } } }代码逻辑说明:doPost方法一开始设置请求和响应的编码,这能解决绝大多数中文乱码问题。检查参数时用trim()去掉首尾空格,否则" 3-201"和"3-201"会被当成两条记录。这里用application/json而不是text/html,前端fetch照常也能处理。
参数说明:dormNo是宿舍号,reporter是报修人,phone不是必填项,所以为空时直接存null。status设为0,表示新单子默认待受理。业务逻辑全部委派给service.create,Servlet里不写JDBC操作,这样后续切换到Spring依赖注入会很方便。
如果面试或答辩时被问到JavaWeb和Spring Boot的区别,你可以拿这个Servlet举例:它本身就是一个轻量的HTTP入口,核心职责是接收参数、调用Service、返回结果;Spring Boot只是把装配对象这件事自动化了,状态流转和数据库访问依然在Service和Mapper里。
3. JavaScript负责的交互细节:表单提交、列表渲染与防抖搜索
3.1 用fetch把表单数据POST到后端
页面交互的核心是fetch,它比老项目里的XMLHttpRequest更直观,也更容易和async/await配合。下面这段代码监听提交按钮,把表单内容收集成一个对象,再转成URL编码格式发送给Servlet。
document.getElementById('submitBtn').addEventListener('click', async function () { const dormNo = document.getElementById('dormNo').value.trim(); const description = document.getElementById('desc').value.trim(); if (!dormNo || !description) { alert('宿舍号和故障描述不能为空'); return; } const data = { dormNo: dormNo, reporter: document.getElementById('reporter').value.trim(), phone: document.getElementById('phone').value.trim(), description: description }; try { const resp = await fetch('/api/repair/create', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: new URLSearchParams(data) }); const result = await resp.json(); if (result.code === 0) { loadRepairList(); } else { alert(result.msg); } } catch (err) { console.error('请求失败', err); alert('网络异常,请稍后再试'); } });这段代码有几个容易被忽略的细节。new URLSearchParams(data)会把对象转成dormNo=3-201&description=...的格式,这样Servlet的getParameter不用改就能读到。fetch默认的Content-Type不是表单编码,所以这里显式设置一下,否则Servlet拿到的参数可能是null。try/catch的作用是防止接口返回非JSON时直接抛错,比如404页面返回的HTML会让resp.json()失败。
参数说明:data对象的key必须和Servlet里req.getParameter("dormNo")完全一致,差一个字符都会导致后端拿到null。手机号为空时Servlet会存null,前端不需要额外处理。一个常见的坑是按钮没有指定type,默认的submit会让整个页面刷新,导致fetch代码执行完又立刻被form提交覆盖。解决方案是把按钮放在form外,或者给form的submit事件绑定时禁止默认行为。
3.2 渲染报修列表:把状态码翻译成可见标签
后端返回的JSON里通常是一个数组,每个元素包含status整数,前端需要把这个数字翻译成人能看懂的中文。用对象映射比if/else更简洁,也更好扩展。
function renderRepairList(list) { const container = document.getElementById('repairList'); if (!container) return; const statusText = { 0: '待受理', 1: '派单中', 2: '维修中', 3: '已完成', 4: '已取消' }; container.innerHTML = list.map(function (item) { const text = statusText[item.status] || '未知状态'; const className = 'status-badge status-' + item.status; const descText = item.description || ''; return '<div class="repair-card">' + '<span class="' + className + '">' + text + '</span>' + '<h4>' + escapeHtml(item.dormNo) + '</h4>' + '<p>' + escapeHtml(descText) + '</p>' + '</div>'; }).join(''); } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"'); }说明:map返回的是拼接好的HTML字符串,最后用join('')合并,这样比逐行createElement更快。状态数字直接拼进class名,例如status-1,方便CSS按状态着色。escapeHtml函数用来转义用户输入的尖括号和引号,防止有人把描述写成<script>标签,这是一个很常见的安全漏洞。
这里的状态映射和Java枚举里的code是对应关系,如果后端改了状态码,前端要同步修改,否则会出现“未知状态”。比较稳妥的做法是让后端额外返回一个statusText字段,前端直接显示,但这会牺牲一部分模板的灵活性。对于宿舍报修这种小系统,前端维护一份映射表就够用了。
3.3 用防抖处理搜索输入框
报修列表一般会加一个按宿舍号搜索的输入框。最直接的写法是监听input事件,每次按键都调用loadRepairList,但这会让每个字符都触发一次网络请求。这里要用到防抖,这个javascript函数模式本质是做一个延时闭包,把事件合并到最后一次触发。
let searchTimer = null; document.getElementById('searchInput').addEventListener('input', function (e) { clearTimeout(searchTimer); searchTimer = setTimeout(function () { loadRepairList(e.target.value.trim()); }, 300); });参数说明:300毫秒是常见的防抖窗口,低于200毫秒可能会在快速输入时仍触发多次请求,高于500毫秒会感觉搜索明显延迟。clearTimeout每次重置计时器,所以只要用户还在连续输入,函数就不会执行。e.target.value在回调执行时会保留当时的输入值,不需要额外存储。注意这里的searchTimer是全局变量,如果页面有多个输入框需要防抖,最好用一个Map保存各自的timer id。
下面这张表列出fetch和XMLHttpRequest的差异,方便你判断为什么源码包普遍用fetch:
| 对比项 | fetch | XMLHttpRequest |
|---|---|---|
| 语法 | Promise链式 | 回调+事件 |
| 处理JSON | resp.json() | JSON.parse(responseText) |
| 取消请求 | AbortController | xhr.abort() |
| 上传进度 | 需要额外流读取 | onprogress事件 |
如果你想支持IE浏览器,那么fetch不能用,只能退回到XHR。宿舍报修系统通常在内网使用,浏览器版本往往不新,这一点要在源码说明里写清楚。
4. CSS决定源码的观感:卡片布局、伪类选择器与交互动效
4.1 用Flex布局实现卡片式列表
报修列表最常见的展示方式是卡片。相比table,卡片能装下更多信息块,比如状态标签、宿舍号、描述文本和操作按钮。CSS里用Flex布局排卡片,比float更简单,也不会出现父元素高度塌陷的问题。
.repair-list { display: flex; flex-wrap: wrap; gap: 16px; padding: 20px; justify-content: flex-start; } .repair-card { width: 280px; border: 1px solid #e5e5e5; border-radius: 8px; padding: 16px; background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); transition: transform 0.15s ease, box-shadow 0.15s ease; } .repair-card:hover { transform: translateY(-3px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); }说明:gap是flex和grid都支持的间距属性,不需要再给每个卡片单独设margin。flex-wrap: wrap让卡片在窄屏幕上自动换行。justify-content: flex-start保证最后一行卡片靠在左边,不会因为一行只有两个就居中拉伸。hover上的translateY(-3px)是常见的鼠标移入事件反馈,配合transition能得到很轻的浮起效果。
这里要注意transition的耗时一般设0.15s到0.2s,短了没感觉,长了会显得拖泥带水。如果项目中已经有了动画库,不要在这里叠加animation,避免两个动效同时控制transform导致跳动。
4.2 状态标签的伪类选择器与颜色语义
状态标签是一个span,用不同底色区分报修进度。CSS里可以直接通过类名控制颜色,也可以用属性选择器。下面这段是类名方案,清晰直观:
.status-badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; line-height: 20px; } .status-0 { color: #d48806; background: #fffbe6; } .status-1 { color: #1677ff; background: #e6f4ff; } .status-2 { color: #722ed1; background: #f9f0ff; } .status-3 { color: #389e0d; background: #f6ffed; } .status-4 { color: #8c8c8c; background: #fafafa; }说明:状态“0”用黄色系表示等待,“1”用蓝色表示派单,“2”用紫色表示维修中,“3”用绿色表示完成,“4”用灰色表示取消。颜色语义和主流设计体系对齐,用户不需要读文字也能快速感知。
除了hover,CSS里还有几个在这类系统里常用的伪类选择器。表格如下:
| 伪类 | 作用 | 在报修系统中的使用场景 |
|---|---|---|
| :hover | 鼠标移入 | 卡片浮起、按钮变色 |
| :focus | 获得焦点 | 输入框高亮 |
| :disabled | 禁用状态 | 已完成的单子禁用编辑按钮 |
| :nth-child | 第几个元素 | 表格里给奇数行加底色 |
如果描述中出现超时,可以在HTML里加一个<span class="overdue">已超时</span>,然后:
.overdue { text-decoration: line-through; color: #bfbfbf; }删除线在这里不是单纯的装饰,它明确告诉用户这条记录已经被系统标记为过期,比加红字更克制,也更容易和报错状态区分开。
4.3 媒体查询让小屏设备不破版
宿舍管理员经常用手机接收报修通知,所以CSS必须处理窄屏。媒体查询是在不同宽度下覆盖默认样式,而不是单独写一套页面。
@media (max-width: 640px) { .repair-list { padding: 12px; gap: 12px; } .repair-card { width: calc(100% - 24px); } .submit-form button { width: 100%; } }参数说明:640px是常见的手机断点,针对超过这个宽度的设备保持原样式。calc(100% - 24px)是为了减去父容器padding,避免卡片宽度超出屏幕。按钮改成100%宽的按钮在手机上更容易点击,热区面积也更大。
这里有一个常见的误用:在宽度选择器里重复设置已经在默认样式里写过的属性。媒体查询只需要覆盖需要变化的属性,不需要把border-radius或background再抄一遍,否则维护起来非常累。查看CSS源码时,如果发现大量重复样式,说明写代码的人没有利用层叠覆盖的特性。
5. 数据库与接口联调:用SQL和curl验证Java接口闭环
5.1 用SQL初始化报修表和状态数据
Java代码和前端逻辑再完整,没有数据库支撑也跑不起来。报修系统最核心的表是repair_order,下面是建表语句,兼容MySQL 5.7及以上版本。
CREATE TABLE repair_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', dorm_no VARCHAR(20) NOT NULL COMMENT '宿舍号', reporter VARCHAR(50) NOT NULL COMMENT '报修人', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', description VARCHAR(500) NOT NULL COMMENT '故障描述', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待受理 1派单中 2维修中 3已完成 4已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍报修单';说明:status字段用TINYINT就够,不会超过127。create_time默认CURRENT_TIMESTAMP,插入时不用显式赋值。update_time加ON UPDATE,每次更新记录时自动刷新,省去Java侧手动set。表名用单数还是复数取决于团队规范,这里用order而不是orders,方便和Java实体类RepairOrder对应。KEY idx_status是普通索引,主要给管理后台“按状态筛选”的查询加速。
初始化数据可以写三条:
INSERT INTO repair_order (dorm_no, reporter, phone, description, status) VALUES ('3-201', '张伟', '13800001111', '水龙头漏水', 0), ('5-302', '李丽', '13800002222', '空调不制冷', 2), ('6-101', '王强', '13800003333', '灯泡损坏', 3);这三条数据可以覆盖待受理、维修中、已完成三种状态,前端渲染和上面的状态机都能直接验证。注意宿舍号用字符串而不是整数,因为可能包含“-”或者“栋”字,字符串类型对这类编码更合适。
5.2 用curl验证接口时的常见用例
写完代码不是直接打开网页点一下就算通过,先用curl把接口调通,能省掉浏览器里排查前端错误的时间。创建接口的验证命令是:
curl -X POST http://localhost:8080/api/repair/create \ -d "dormNo=3-201&reporter=张伟&phone=13800001111&description=水龙头漏水" \ -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8"预期返回{"code":0,"msg":"提交成功"}。如果返回code=1,说明参数校验或数据库写入出问题。查询接口可以带上status参数:
curl "http://localhost:8080/api/repair/list?status=0"这里注意curl在终端里发送中文时,某些Windows终端会使用GBK编码,导致后端接收乱码。可以在请求里加--data-urlencode或者用文件保存请求体再-c,但更简单的是先用英文字符串验证接口通路,再把中文场景交给前端页面测。
下面这张表是联调时的常用测试用例,放在源码里的README上也合适:
| 场景 | 请求方式 | 期望结果 |
|---|---|---|
| 正常提交 | POST | 返回code=0,数据库新增status=0 |
| 宿舍号为空 | POST | 返回code=1和提示信息 |
| 故障描述超长 | POST | 后端拒绝或数据库截断 |
| 非法状态流转 | POST修改接口 | 返回异常码,数据库状态不变 |
| 未登录访问 | GET | 返回401或跳转登录页 |
5.3 用浏览器开发者工具定位404和乱码
curl验证通过后,再回到浏览器。如果页面报404,先打开DevTools的Network标签,找到那条红色的请求,看请求URL和实际接口路径是否一致。常见原因是页面部署在/repair/这个二级路径下,但fetch里写的/api/repair/create没有带上项目名。解决办法是用相对路径或者从页面标签里动态拼接contextPath,不建议写死。
中文乱码的定位顺序是:先看Network里的Response Headers有没有Content-Type: application/json;charset=utf-8,没有说明Servlet的resp.setCharacterEncoding没生效;再看响应体里的中文是否已经乱码,如果乱码则去检查数据库连接串是否加了characterEncoding=utf8。前端显示正常但数据库里是乱码,问题一般出在MySQL连接配置而不是Java代码。
还有一个容易被忽略的点:Servlet的@WebServlet注解在某些源码包里会被web.xml覆盖掉,导致你改了注解路径但访问的还是老地址。检查一下WEB-INF/web.xml里有没有相关的servlet-mapping,如果有,以web.xml配置为准。这个坑能卡住半天,在源码排错时优先排查。
6. 源码运行排错:三个最影响体验的坑
最后一个建议,与其去翻整套源码,不如先学会用浏览器验证前端和后端各自是否正常。下面三个坑是宿舍报修系统源码里最常见的,每个都附一个定位思路。
第一个,页面加载后样式全丢了。打开Network标签,看CSS请求的状态码是不是404。如果是,检查HTML里引用的css路径,href="css/style.css"和href="/css/style.css"在部署路径不同时表现完全不同。前者是相对当前页面路径,后者是从应用根路径开始,两者差一个斜杠就访问不到。快速验证方法是在地址栏直接输入CSS的完整URL,能打开说明路径没错,打不开就是路径问题。
第二个,点击提交后没有反应。先把Console里的报错截图,如果看到Unexpected token '<',说明fetch访问到了HTML页面而不是JSON接口,通常是因为后端返回了404错误页。如果看到Cannot read properties of null,说明DOM元素的id写错了,检查document.getElementById的参数和页面里的id是否完全一致。
第三个,接口返回500。先看后端日志的堆栈,常见原因是数据库驱动没打包到WEB-INF/lib下,或者MySQL版本和驱动版本不兼容。用下面这段代码可以快速验证数据库连接是否正常:
try (Connection conn = dataSource.getConnection()) { System.out.println(conn.isClosed()); } catch (SQLException e) { e.printStackTrace(); }把这段放到Servlet的init方法里,启动后看控制台输出。如果输出false,说明连接池正常。如果抛ClassNotFoundException,就去检查驱动的jar包位置。
排错不要凭感觉改代码,每步都基于Network、Console和后端日志来定位。这三个面板一起看,通常能在五分钟内找到问题源头。
本文还有配套的精品资源,点击获取