简介:这是一套基于Java开发的完整学生宿舍管理系统毕业设计项目源码,面向计算机专业本科生及Java初学者,解决高校宿舍管理中学生、楼宇、宿舍、入住、宿管等多角色协同管理的实际需求。资源包共661个文件,涵盖35个核心Java类、400个前端交互JS脚本、46个CSS样式文件、28个Less/SCSS预编译样式、8个XML配置与SQL建表脚本,以及MP4操作演示视频和DOCX文档说明,整体72.37MB,结构清晰、前后端分离明确。已有468人学习下载,适合用于课程设计、毕设参考或SpringBoot+MySQL全栈开发实践。读者可直接运行管理员、宿管、学生三类角色界面,获得含登录鉴权、批量导入导出Excel、CRUD全流程、数据库初始化脚本及MySQL 5/8双版本适配方案在内的完整可部署系统。
1. 一个连数据库连接池都配不上的宿舍系统,凭什么让三届学生拿它交毕业设计?
去年帮某高校实验室做毕设答辩支持时,翻了二十多个“基于Java的学生宿舍管理系统”源码包,八成卡在登录页——不是MySQL驱动版本错,就是Hibernate映射文件里把student_id写成stu_id,再或者Tomcat启动后报java.lang.NoClassDefFoundError: javax/servlet/Filter。但真有那么一套代码,从JDK 8到17全兼容,用HikariCP做连接池、Spring Boot 2.7起步、Thymeleaf渲染页面,连宿管阿姨导出Excel的按钮都加了防重复点击。它不炫技,不堆微服务,就老老实实把“查空床位→分配房间→登记维修→统计水电”这四件事闭环跑通。适合两类人:一是大三刚学完Servlet+JDBC想落地练手的,二是导师催得紧、需要三天内跑通+改界面+写文档的毕设党。项目源码结构干净,没冗余模块,所有Java源码都在src/main/java/com/example/dorm下,连Lombok注解都配好了——你删掉@Data,编译直接报红,逼你养成规范习惯。
2. 环境准备与一键启动:从JDK安装到首页弹出只差5分钟
2.1 JDK与Maven环境:别再用“我电脑上能跑”糊弄自己
这个项目明确要求JDK 8u202+或JDK 11(推荐11),不是“JDK 8以上就行”。为什么?因为pom.xml里用了<java.version>11</java.version>,且DormApplication.java主类上有@SpringBootApplication,Spring Boot 2.7.x已停止对JDK 8的官方支持。若强行用JDK 8,编译时会卡在org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration类加载失败。
验证方式不是看java -version返回值,而是执行:
javac -version && java -cp . TestVersion.class其中TestVersion.java内容为:
public class TestVersion { public static void main(String[] args) { System.out.println("JVM Version: " + System.getProperty("java.version")); System.out.println("JVM Spec: " + System.getProperty("java.vm.specification.version")); } }提示:很多同学装了JDK 17但IDEA默认用JDK 8编译,务必在IDEA的
File → Project Structure → Project → Project SDK和Project language level双处确认。
Maven必须3.6.3+,低于此版本无法解析Spring Boot的BOM依赖。检查命令:
mvn -v | grep "Apache Maven" # 正确输出应为:Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)2.2 数据库初始化:用schema.sql比手动建表少踩7个坑
项目不带H2内存库兜底,必须连真实MySQL。但别急着打开Navicat——先看src/main/resources/schema.sql:
-- src/main/resources/schema.sql CREATE DATABASE IF NOT EXISTS dorm_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE dorm_db; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, gender ENUM('MALE','FEMALE') DEFAULT 'MALE', room_id BIGINT, phone VARCHAR(11), CHECK (phone REGEXP '^1[3-9]\\d{9}$') ); CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building VARCHAR(20) NOT NULL, floor TINYINT NOT NULL, number VARCHAR(10) NOT NULL, capacity TINYINT DEFAULT 4, current_occupancy TINYINT DEFAULT 0, UNIQUE KEY uk_building_floor_number (building, floor, number) );关键点有三:
CHARACTER SET utf8mb4:避免微信昵称(含emoji)存不进数据库;CHECK (phone REGEXP ...):MySQL 8.0.16+才支持正则校验,低版本会报错,此时需删掉该行;UNIQUE KEY uk_building_floor_number:防止同一楼层建两个“301”房间,这是业务强约束,不是可选项。
执行顺序必须是:
- 在MySQL中创建用户:
CREATE USER 'dorm_user'@'localhost' IDENTIFIED BY 'D0rmP@ss2024'; - 授权:
GRANT SELECT,INSERT,UPDATE,DELETE ON dorm_db.* TO 'dorm_user'@'localhost'; - 刷新权限:
FLUSH PRIVILEGES; - 再执行
schema.sql(注意:不要用source schema.sql,用客户端工具的“执行SQL文件”功能)
2.3 Spring Boot配置:application.yml里藏着三个生死开关
src/main/resources/application.yml是启动成败的关键,重点改三处:
spring: datasource: url: jdbc:mysql://localhost:3306/dorm_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: dorm_user password: D0rmP@ss2024 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 # 必须开启,否则Thymeleaf模板不渲染 spring: thymeleaf: cache: false enabled: true prefix: classpath:/templates/ suffix: .html # JPA/Hibernate核心配置,漏掉这行,save()方法永远不触发INSERT spring: jpa: hibernate: ddl-auto: validate # 生产环境必须为validate,开发期可设update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8DialectserverTimezone=Asia/Shanghai:不加此参数,MySQL 8+会报The server time zone value 'XXX' is unrecognized,这是血泪经验;ddl-auto: validate:开发时设update能自动建表,但上线前必须切回validate,否则可能误删生产数据;MySQL8Dialect:用错成MySQL57Dialect会导致JSON字段解析失败,而本项目room表虽无JSON,但预留了extra_info TEXT字段供后续扩展。
启动命令(Linux/macOS):
cd dorm-system-root mvn clean package -DskipTests java -jar target/dorm-system-1.0.0.jarWindows用户请用PowerShell执行,CMD常因路径空格报Could not find or load main class。
3. 核心模块拆解:从分配房间到维修登记的完整链路
3.1 宿舍分配服务:RoomAllocationService里的事务边界
分配逻辑不在Controller里硬写,而封装在com.example.dorm.service.RoomAllocationService中。关键方法allocateStudentToRoom()代码如下:
@Service @Transactional(rollbackFor = Exception.class) public class RoomAllocationService { @Autowired private StudentRepository studentRepository; @Autowired private RoomRepository roomRepository; @Autowired private AllocationLogRepository logRepository; // 分配日志表,用于审计 public Result<String> allocateStudentToRoom(Long studentId, Long roomId) { // 1. 双重校验:学生存在且未分配,房间存在且有空位 Student student = studentRepository.findById(studentId) .orElseThrow(() -> new BusinessException("学生不存在")); if (student.getRoomId() != null) { return Result.fail("该学生已分配宿舍"); } Room room = roomRepository.findById(roomId) .orElseThrow(() -> new BusinessException("房间不存在")); if (room.getCurrentOccupancy() >= room.getCapacity()) { return Result.fail("房间已满员"); } // 2. 事务内原子操作:更新学生room_id + 房间占用数 + 记录日志 student.setRoomId(roomId); studentRepository.save(student); // 触发UPDATE student SET room_id=? WHERE id=? room.setCurrentOccupancy(room.getCurrentOccupancy() + 1); roomRepository.save(room); // UPDATE room SET current_occupancy=? WHERE id=? AllocationLog log = new AllocationLog(); log.setStudentId(studentId); log.setRoomId(roomId); log.setOperator("system"); log.setCreateTime(LocalDateTime.now()); logRepository.save(log); return Result.success("分配成功"); } }逻辑说明:
@Transactional(rollbackFor = Exception.class)确保三步操作要么全成功,要么全回滚。若logRepository.save()抛出异常,前面两个save()自动回滚;student.setRoomId(roomId)后调用studentRepository.save(),而非直接UPDATE,是因为JPA需维护一级缓存一致性;AllocationLog表非必需,但毕业设计答辩时,老师常问“如何追溯谁在什么时候分配了谁”,此表即答案。
3.2 维修工单管理:RepairOrderController的RESTful设计陷阱
维修模块采用标准REST风格,但RepairOrderController里有个易忽略的设计:
@RestController @RequestMapping("/api/repair") public class RepairOrderController { @Autowired private RepairOrderService repairOrderService; // POST /api/repair → 创建工单 @PostMapping public Result<RepairOrder> createOrder(@Valid @RequestBody RepairOrderCreateDTO dto) { return Result.success(repairOrderService.createOrder(dto)); } // GET /api/repair?status=PENDING → 查询待处理工单 @GetMapping public Result<List<RepairOrder>> listOrders( @RequestParam(required = false, defaultValue = "ALL") String status, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("createTime").descending()); List<RepairOrder> orders = repairOrderService.listByStatus(status, pageable); return Result.success(orders); } // PATCH /api/repair/123 → 更新工单状态(仅允许PENDING→PROCESSING→COMPLETED) @PatchMapping("/{id}") public Result<String> updateStatus( @PathVariable Long id, @Valid @RequestBody RepairOrderUpdateDTO updateDTO) { repairOrderService.updateStatus(id, updateDTO.getStatus()); return Result.success("状态更新成功"); } }参数说明:
@Valid触发DTO校验,RepairOrderCreateDTO中@NotBlank标注了description字段,前端传空字符串会直接400返回;@RequestParam(defaultValue = "ALL")让GET /api/repair默认查全部,避免学生测试时因没传参返回空列表而以为接口坏了;PATCH而非PUT:因只更新状态字段,符合HTTP语义,且Spring Boot默认启用PATCH方法(无需额外配置)。
3.3 水电统计报表:ReportService中的时间范围计算玄学
水电统计接口/api/report/water-electric需按月聚合,但学生常把LocalDate.now().withDayOfMonth(1)写成LocalDate.now().withDayOfYear(1),导致跨年统计出错。正确实现见ReportService.generateMonthlyReport():
@Service public class ReportService { @Autowired private WaterElectricRecordRepository recordRepository; public ReportData generateMonthlyReport(int year, int month) { // 关键:构造当月第一天和下月第一天,避免月末天数不一致问题 LocalDate firstDay = LocalDate.of(year, month, 1); LocalDate nextMonthFirstDay = firstDay.plusMonths(1); List<WaterElectricRecord> records = recordRepository.findByDateBetween( firstDay.atStartOfDay(), nextMonthFirstDay.atStartOfDay() ); BigDecimal totalWater = BigDecimal.ZERO; BigDecimal totalElectric = BigDecimal.ZERO; Map<String, BigDecimal> buildingWater = new HashMap<>(); Map<String, BigDecimal> buildingElectric = new HashMap<>(); for (WaterElectricRecord r : records) { totalWater = totalWater.add(r.getWaterConsumption()); totalElectric = totalElectric.add(r.getElectricConsumption()); String building = r.getBuilding(); buildingWater.merge(building, r.getWaterConsumption(), BigDecimal::add); buildingElectric.merge(building, r.getElectricConsumption(), BigDecimal::add); } return new ReportData(totalWater, totalElectric, buildingWater, buildingElectric); } }避坑点:
nextMonthFirstDay.atStartOfDay()生成的是2024-04-01T00:00,而firstDay.plusDays(30)可能落到5月,造成漏统计;merge(..., BigDecimal::add)替代put(building, get(building).add(...)),避免NPE;ReportData是DTO,不含任何JPA注解,防止序列化时触发懒加载。
4. 避坑指南:那些让毕设答辩前夜崩溃的5个真实问题
4.1 现象:登录成功后跳转到空白页,浏览器控制台报Thymeleaf template not found
原因:src/main/resources/application.yml中spring.thymeleaf.prefix被误写为classpath:/templates/(末尾多了一个斜杠),或LoginController返回的视图名是"login.html"而非"login"。Thymeleaf默认查找classpath:/templates/login.html,若写成"login.html",实际路径变成classpath:/templates/login.html.html。
解决:
检查LoginController:
@Controller public class LoginController { @GetMapping("/login") public String loginPage() { return "login"; // ✅ 正确:不带.html后缀 // return "login.html"; // ❌ 错误 } }并确认application.yml中:
spring: thymeleaf: prefix: classpath:/templates/ # ✅ 末尾有斜杠 suffix: .html # ✅ 以点开头4.2 现象:分配宿舍时提示“房间已满”,但数据库里current_occupancy明明是2,capacity是4
原因:Room实体类中current_occupancy字段用了@Column(name = "current_occupancy"),但MySQL表字段名为current_occupancy(下划线),而JPA默认开启spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy,该策略会把驼峰转下划线。若手动指定了@Column(name = "..."),又未关闭自动命名,会导致JPA生成的SQL查currentoccupancy(无下划线)字段,始终读到NULL,比较时NULL >= 4结果为false,但current_occupancy实际为NULL,条件判断失效。
解决:
统一命名策略,在application.yml中添加:
spring: jpa: hibernate: naming: physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy并删除Room.java中所有@Column(name = "..."),让JPA自动生成字段名;或彻底关闭自动命名:
spring: jpa: hibernate: naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl4.3 现象:导出Excel时中文乱码,显示为??
原因:ExportService.exportStudentsToExcel()中使用WorkbookFactory.create(new FileInputStream(file))读取模板,但未指定字符集。更致命的是,response.setHeader("Content-Type", "application/vnd.ms-excel")缺少charset=utf-8。
解决:
修改导出方法:
@GetMapping("/export/students") public void exportStudents(HttpServletResponse response) throws IOException { List<Student> students = studentService.findAll(); Workbook workbook = exportService.exportStudentsToExcel(students); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); // ✅ 改为xlsx类型+UTF-8 response.setHeader("Content-Disposition", "attachment; filename=\"students_" + System.currentTimeMillis() + ".xlsx\""); workbook.write(response.getOutputStream()); }同时确保Excel模板本身保存为UTF-8编码(用WPS或LibreOffice另存为时勾选UTF-8)。
4.4 现象:修改学生信息后,页面显示更新成功,但数据库没变
原因:StudentController.updateStudent()方法中,接收参数用的是@RequestBody Student student,但Student实体类未加@Entity和@Table注解,或id字段缺少@Id。JPA无法识别该对象为托管实体,studentRepository.save(student)实际执行的是INSERT而非UPDATE。
解决:
检查Student.java:
@Entity @Table(name = "student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; // 其他字段... }并确认StudentRepository继承JpaRepository<Student, Long>,而非CrudRepository。
4.5 现象:Tomcat启动后控制台无报错,但访问http://localhost:8080返回404
原因:DormApplication.java主类未加@SpringBootApplication,或该类不在默认包路径下(如放在com.example.dorm.config而非com.example.dorm)。Spring Boot扫描不到@Controller和@Service。
解决:
确保主类结构为:
package com.example.dorm; // ✅ 必须是根包 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // ✅ 必须有 public class DormApplication { public static void main(String[] args) { SpringApplication.run(DormApplication.class, args); } }若包名是com.example.dorm.main,则需在@SpringBootApplication中显式指定扫描路径:
@SpringBootApplication(scanBasePackages = "com.example.dorm")5. 二次开发实战:给系统加“夜间模式切换”只需改3个文件
毕业设计答辩常被问:“如果要加XX功能,你怎么实现?”——与其背概念,不如真动手改一个。这里以“夜间模式切换”为例,演示从需求分析到上线验证的全流程。它不涉及数据库变更,只改前端样式和后端状态存储,完美匹配毕设改造场景。
5.1 需求拆解:什么是“夜间模式”在此系统中的最小可行定义?
不是全站CSS变量切换,而是:
- 用户登录后,右上角多一个🌙图标按钮;
- 点击后,全局背景色从
#f8f9fa变为#1a1a1a,文字色从#333变为#e0e0e0; - 切换状态需持久化,关闭浏览器再打开仍保持上次选择;
- 不影响任何业务逻辑(增删改查照常运行)。
这意味着只需动三处:
- 前端:
templates/layout.html加按钮和CSS类; - 后端:新增
ThemeController存取用户偏好; - 全局:
templates/base.html注入主题CSS类。
5.2 前端改造:Thymeleaf模板注入与CSS变量
第一步,在templates/layout.html的导航栏右侧插入按钮:
<!-- templates/layout.html --> <div class="navbar-nav ms-auto"> <!-- 原有用户菜单 --> <div class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button">/* static/css/custom.css */ :root { --bg-color: #f8f9fa; --text-color: #333; --card-bg: #fff; --border-color: #dee2e6; } .theme-dark { --bg-color: #1a1a1a; --text-color: #e0e0e0; --card-bg: #2d2d2d; --border-color: #444; } body { background-color: var(--bg-color); color: var(--text-color); } .card { background-color: var(--card-bg); border-color: var(--border-color); }第三步,让所有页面继承主题类。修改templates/base.html的<body>标签:
<!-- templates/base.html --> <body th:classappend="${#request.getAttribute('theme') != null} ? ${#request.getAttribute('theme') == 'dark' ? 'theme-dark' : ''} : ''">5.3 后端状态管理:用HttpSession存主题偏好
新建ThemeController.java:
@Controller public class ThemeController { @PostMapping("/api/theme/toggle") @ResponseBody public Result<String> toggleTheme(HttpServletRequest request) { String currentTheme = (String) request.getSession().getAttribute("theme"); String newTheme = "light"; if ("light".equals(currentTheme)) { newTheme = "dark"; } request.getSession().setAttribute("theme", newTheme); return Result.success(newTheme); } // 供页面首次加载时获取当前主题 @GetMapping("/api/theme/current") @ResponseBody public Result<String> getCurrentTheme(HttpServletRequest request) { String theme = (String) request.getSession().getAttribute("theme"); return Result.success(theme != null ? theme : "light"); } }然后在LayoutController.java中,让每个请求都把主题注入到Model:
@Controller public class LayoutController { @ModelAttribute public void addAttributes(Model model, HttpServletRequest request) { String theme = (String) request.getSession().getAttribute("theme"); model.addAttribute("theme", theme != null ? theme : "light"); } }5.4 JavaScript联动:点击按钮触发后端并刷新样式
在templates/layout.html底部加JS:
<script th:inline="javascript"> function changeTheme(current) { fetch('/api/theme/toggle', { method: 'POST', headers: {'Content-Type': 'application/json'} }) .then(response => response.json()) .then(data => { const newTheme = data.data; // 切换body类 document.body.className = newTheme === 'dark' ? 'theme-dark' : ''; // 更新按钮图标和文字 const toggleLink = document.getElementById('themeToggle'); toggleLink.innerHTML = '<i class="bi ' + (newTheme === 'dark' ? 'bi-moon-fill' : 'bi-sun-fill') + '"></i> ' + (newTheme === 'dark' ? '切换为日间模式' : '切换为夜间模式'); // 存入session以便刷新后保持 sessionStorage.setItem('theme', newTheme); }); } </script>注意:此处用
sessionStorage而非localStorage,因主题偏好绑定登录态,用户登出后应重置。
验证方式:
- 启动项目,登录后打开浏览器开发者工具,Application → Storage → Session Storage,确认有
theme键; - 点击🌙按钮,观察URL是否发起
POST /api/theme/toggle请求,响应体为{"code":200,"data":"dark","msg":"success"}; - 检查
<body>标签是否动态添加theme-dark类; - 关闭浏览器重开,登录后主题是否延续上次选择。
从那以后我每次接毕设需求,都强制走一遍“最小功能闭环”:先写一个按钮、一个API、一个CSS类,跑通再扩。宁可少十个功能,不能有一个半吊子。希望帮到你。
本文还有配套的精品资源,点击获取