尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb学生宿舍管理系统完整开发指南:表设计、事务与权限控制

JavaWeb学生宿舍管理系统完整开发指南:表设计、事务与权限控制 简介面向 Java Web 课程设计的宿舍管理系统完整源码与数据库项目定位是帮助计算机相关专业学生快速完成毕业设计、期末大作业或课程设计任务也适合作为 Java Web 综合练习的参考案例。项目基于 JSPServlet 经典分层架构后端按 Dao 数据访问层、Servlet 控制层和实体类分层组织前端由 JSP 页面配合 CSS 与 JavaScript 完成交互展示代码结构清晰、命名规范便于阅读与后续二次开发。压缩包内共 133 个文件主要包含 20 个 Java 源文件、18 个 JSP 页面、52 个 JavaScript 脚本、5 个 CSS 样式文件以及 1 个 SQL 数据库脚本和其他配置文件覆盖学生信息管理、宿舍分配管理、水电费账单管理、管理员登录与权限控制等核心业务模块整体体积仅 3.3MB。源码已经过本地编译运行验证评审分达到 98 分内容经助教老师审定难度适中导入开发环境后配合数据库脚本即可部署启动能够快速用于期末答辩或毕设展示。目前已有 143 人学习下载是一份结构规范、可直接落地的宿舍管理系统高分参考资料。1. 为什么学生宿舍管理系统值得认真做一遍学生宿舍管理系统是 JavaWeb 课程设计和毕业设计里出现频率最高的题目之一网上也到处是源码数据库的压缩包。但如果你真下载过几个会发现大多数跑不起来要么 MySQL 版本对不上要么 Tomcat 内置的 Servlet 版本和代码里 import 的包冲突要么数据库脚本里外键一堆但没人说明插入顺序。所谓高分项目分不在功能多而在于一条链路是闭合的页面请求到 Servlet 再到 Service 和 DAO最后落库反过来数据库的表结构能支撑业务约束查询有分页、权限有过滤器。这篇博文按这个思路把学生宿舍管理系统从表设计到代码落地、再到答辩前验证完整走一遍。适合正在做课程设计、需要短期交付可运行项目的同学也适合想看看这类管理系统里事务和权限怎么写的老手。2. 学生宿舍管理系统的表结构设计与数据库脚本2.1 先想清楚业务实体再动手写建表语句学生宿舍管理系统的核心实体并不多但很多初版项目把表拆得过于零碎或者反过来一张大表塞进所有字段。常见做法是拆成六张核心表学生表、宿舍楼表、房间表、床位表、入住记录表、报修表。如果项目要求包含访客或晚归登记也要从入住记录衍生不要直接往学生表里堆字段。我一般会先把每张表的职责定清楚学生表只存学生静态信息学号、姓名、性别、学院、专业、班级、联系电话。宿舍楼表和房间表负责物理位置楼栋名称、楼层、房间号、房间类型、床位数量。床位表是学生和房间之间的关联核心状态字段区分空闲/已入住/维修中。入住记录表记录一条学生从入住到退宿的完整生命周期不要删历史记录状态字段标识当前是否有效。报修表挂到房间维度记录报修描述、报修时间、处理状态。这样设计的好处是后续做某个楼栋还剩多少空床位这种查询不需要去学生表里数人数直接统计床位状态即可。2.2 宿舍管理系统的建表 SQL字段类型与约束以 MySQL 8.0 为例下面是核心建表脚本。需要注意几点字符集统一用 utf8mb4避免中文乱码学号用 varchar 而不是 int因为学号通常以 0 开头所有金额或计数类字段明确默认值避免 NULL 参与计算。CREATE DATABASE IF NOT EXISTS dormitory_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE dormitory_system; -- 宿舍楼表 CREATE TABLE dorm_building ( building_id INT PRIMARY KEY AUTO_INCREMENT, building_name VARCHAR(50) NOT NULL UNIQUE, address VARCHAR(100), total_floors INT NOT NULL DEFAULT 6, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 房间表 CREATE TABLE dorm_room ( room_id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_number VARCHAR(20) NOT NULL, floor_number INT NOT NULL, room_type VARCHAR(20) NOT NULL DEFAULT 四人间, bed_count INT NOT NULL DEFAULT 4, is_public TINYINT NOT NULL DEFAULT 0 COMMENT 0非公共 1公共, UNIQUE KEY uk_building_room (building_id, room_number), CONSTRAINT fk_room_building FOREIGN KEY (building_id) REFERENCES dorm_building(building_id) ) ENGINEInnoDB; -- 学生表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, gender CHAR(2) NOT NULL COMMENT 男/女, college VARCHAR(100), major VARCHAR(100), class_name VARCHAR(50), phone VARCHAR(20), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 床位表 CREATE TABLE bed ( bed_id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, bed_number TINYINT NOT NULL COMMENT 1-4床位编号, bed_status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1已入住 2维修, student_id VARCHAR(20), UNIQUE KEY uk_room_bed (room_id, bed_number), CONSTRAINT fk_bed_room FOREIGN KEY (room_id) REFERENCES dorm_room(room_id), CONSTRAINT fk_bed_student FOREIGN KEY (student_id) REFERENCES student(student_id) ) ENGINEInnoDB;这段脚本里值得说明的参数bed_status用 TINYINT 而不是 VARCHAR查询时用数字比较更快后端代码里定义常量映射状态名student_id在外键上没有加 UNIQUE因为理论上允许一个学生换过床位后旧记录保留但状态置为失效uk_building_room唯一索引保证了同一栋楼内房间号不能重复这个约束比在代码里判断更可靠。2.3 入学分配和报修记录事务的落点学生入住时不能只更新床位状态还要插入入住记录退宿时要把床位释放、入住记录置为失效。这两个动作必须在一个事务里完成。对应的表结构如下-- 入住记录表 CREATE TABLE stay_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL, bed_id INT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在宿 0已退宿, CONSTRAINT fk_stay_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_stay_bed FOREIGN KEY (bed_id) REFERENCES bed(bed_id) ) ENGINEInnoDB; -- 报修表 CREATE TABLE repair_record ( repair_id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, student_id VARCHAR(20), description VARCHAR(500) NOT NULL, report_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, repair_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待处理 1处理中 2已完成, handler_name VARCHAR(50), handle_time DATETIME, CONSTRAINT fk_repair_room FOREIGN KEY (room_id) REFERENCES dorm_room(room_id) ) ENGINEInnoDB;很多人在这个环节犯的错误是把stay_record.status做成布尔值其实用 TINYINT 留出扩展余地更合适。另外如果项目要求显示某学生当前住哪栋楼哪个房间不要直接写一个多表 JOIN 把结果冗余到学生表让大家学会用视图或者直接在 Service 层拼装结果这样后续需求变更时不需要改表结构。3. JavaWeb 后端的入住流程从 DAO 到 Servlet 的完整链路3.1 为什么用 Servlet JDBC 而不是直接用 JSP 写逻辑源码数据库的高分项目评阅老师看的是结构分层是否清晰。常见的技术栈组合是 Servlet JSP JDBC MySQL不用框架因为课程设计阶段重点是理解 HTTP 请求处理流程和 SQL 操作。如果你用 Spring Boot反而容易被问你了解自动配置原理吗这类问题。用原生 Servlet 的好处是每一步都暴露在代码里答辩时讲得出请求从哪里进来、经过谁、到哪里去。我建议的包结构如下com.dorm.servlet // 控制器层只做参数接收和页面跳转 com.dorm.service // 业务层处理事务边界 com.dorm.dao // 数据访问层只写 JDBC 和 SQL com.dorm.entity // 实体类对应数据库表 com.dorm.util // DBUtil、常量类3.2 分配床位的核心代码事务与状态更新分配床位是最能体现业务逻辑完整性的功能。先说需求学生申请入住系统找到指定房间的空床位把床位状态改为已入住写入入住记录如果同一个学生已经有一条 status1 的记录就不能再分配。先写 DAO 层的床位查询public Bed findAvailableBed(int roomId) throws SQLException { String sql SELECT bed_id, room_id, bed_number, bed_status, student_id FROM bed WHERE room_id ? AND bed_status 0 LIMIT 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Bed bed new Bed(); bed.setBedId(rs.getInt(bed_id)); bed.setRoomId(rs.getInt(room_id)); bed.setBedNumber(rs.getInt(bed_number)); bed.setBedStatus(rs.getInt(bed_status)); bed.setStudentId(rs.getString(student_id)); return bed; } } } return null; }LIMIT 1保证只取一个空闲床位避免并发下返回多条让程序不知道该更新哪个。但要注意这个查询本身是读操作真正的并发保护要放在后续事务里。然后是 Service 层的入住逻辑事务边界放在这里而不是 DAO 层因为一次入住要操作 bed 和 stay_record 两张表public boolean assignBed(String studentId, int roomId, String date) throws SQLException { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); try { // 1. 检查学生是否已在宿 String checkSql SELECT COUNT(*) FROM stay_record WHERE student_id ? AND status 1; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, studentId); try (ResultSet rs ps.executeQuery()) { if (rs.next() rs.getInt(1) 0) { return false; } } } // 2. 锁定并更新一个空闲床位 String lockSql SELECT bed_id FROM bed WHERE room_id ? AND bed_status 0 LIMIT 1 FOR UPDATE; String updateSql UPDATE bed SET bed_status 1, student_id ? WHERE bed_id ?; int bedId; try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setInt(1, roomId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { return false; // 没有空闲床位 } bedId rs.getInt(bed_id); } } try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setString(1, studentId); ps.setInt(2, bedId); ps.executeUpdate(); } // 3. 插入入住记录 String insertSql INSERT INTO stay_record (student_id, bed_id, check_in_date, status) VALUES (?, ?, ?, 1); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, studentId); ps.setInt(2, bedId); ps.setDate(3, java.sql.Date.valueOf(date)); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }FOR UPDATE是这里的关键参数它把选中的行锁住直到事务结束两个请求同时进来时后一个会被阻塞。虽然课程设计阶段并发量很小但答辩时你能说出这一句和普通写法就不是一个层次。业务层一定要用 try-catch 包裹并回滚否则第一条 SQL 成功第二条失败数据就错了。3.3 Servlet 层的参数接收与页面流转Servlet 只做三件事接收参数、调用 Service、根据结果跳转。WebServlet(/assignBed) public class AssignBedServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String studentId req.getParameter(studentId); int roomId Integer.parseInt(req.getParameter(roomId)); String date req.getParameter(date); StayService service new StayService(); boolean ok false; String msg; try { ok service.assignBed(studentId, roomId, date); msg ok ? 分配成功 : 分配失败该学生已在宿或无空床位; } catch (SQLException e) { logger.error(assign bed error, e); msg 系统异常请稍后重试; } resp.sendRedirect(req.getContextPath() /assignResult.jsp?msg URLEncoder.encode(msg, UTF-8)); } }req.setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则 POST 请求里的中文会乱码。sendRedirect之后是浏览器重新发起 GET 请求所以这里不能携带太多业务数据只传提示消息。3.4 JDBC 连接参数的配置与常见坑DBUtil 是最容易被拷走但最容易配错的文件。以 MySQL 8.x 为例public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/dormitory_system ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } }参数说明serverTimezone不设置会报时间相关的 SQLExceptioncharacterEncoding和建库时的 utf8mb4 是两回事前者管连接后者管存储两边不一致就会出现乱码allowPublicKeyRetrieval是 MySQL 8.0 用 caching_sha2_password 认证时需要加的否则首次连接会报 Public Key Retrieval 错误。4. 宿舍管理系统的权限控制与多条件查询实现4.1 基于 Filter 的会话校验不引入框架系统里至少有两种角色宿管员和学生。宿管员能管理房间床位、查看所有学生、处理报修学生只能查看自己的入住信息和提交报修。不用 Shiro 或 Spring Security用 Filter 加 Session 就能实现而且答辩时更好解释。登录成功后把用户信息放进 SessionUser user userService.login(username, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); }然后定义一个过滤器拦截所有非登录页面WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String ctxPath request.getContextPath(); // 放行登录相关的请求和静态资源 if (uri.endsWith(login.jsp) || uri.contains(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(req, resp); return; } Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(ctxPath /login.jsp); return; } chain.doFilter(req, resp); } }这里的放行逻辑要写得克制不能因为图省事把所有 JSP 都放行。我见过一个项目把/*放行了一半页面权限形同虚设。扩展做法是检查角色如果访问的路径以/admin/开头要校验登录用户的角色字段User user (User) request.getSession().getAttribute(loginUser); if (uri.startsWith(ctxPath /admin/) !ADMIN.equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; }4.2 学生查询页面的动态 SQL 拼接管理端最常见的场景是组合查询按学号、姓名、学院、宿舍楼、房间类型筛选学生列表。直接用字符串拼接 SQL 会导致 SQL 注入用 PreparedStatement 的占位符按条件逐个添加参数。public ListStudentVO searchStudents(StudentQuery query) throws SQLException { StringBuilder sql new StringBuilder(); sql.append(SELECT s.student_id, s.name, s.gender, s.college, ) .append(b.building_name, r.room_number, bed.bed_number ) .append(FROM student s ) .append(LEFT JOIN bed ON s.student_id bed.student_id ) .append(LEFT JOIN dorm_room r ON bed.room_id r.room_id ) .append(LEFT JOIN dorm_building b ON r.building_id b.building_id ) .append(WHERE 11 ); ListObject params new ArrayList(); if (query.getStudentId() ! null !query.getStudentId().isEmpty()) { sql.append(AND s.student_id LIKE ? ); params.add(% query.getStudentId() %); } if (query.getName() ! null !query.getName().isEmpty()) { sql.append(AND s.name LIKE ? ); params.add(% query.getName() %); } if (query.getBuildingId() ! null) { sql.append(AND b.building_id ? ); params.add(query.getBuildingId()); } sql.append(ORDER BY s.student_id LIMIT ? OFFSET ?); params.add(query.getPageSize()); params.add((query.getPageNum() - 1) * query.getPageSize()); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } // 执行查询并组装 ListStudentVO } }WHERE 11是动态 SQL 的常见技巧避免每个条件判断都要处理前面是否已经有条件代码可读性更高。分页用LIMIT ? OFFSET ?参数类型显式传 int 防止 MySQL JDBC 驱动解析 LIMIT 10 报错。排名靠后的数据量级在课程设计里没必要用游标分页但你要知道这种写法在数据量大时性能会下降。4.3 权限与常用查询的接口清单路径方法角色功能/loginPOST所有登录写入 Session/logoutGET所有退出销毁 Session/assignBedPOST管理员分配床位/checkoutPOST管理员退宿释放床位/student/listGET管理员学生列表及组合查询/myStayGET学生查看本人入住信息/repair/addPOST学生提交报修/repair/listGET管理员报修列表及状态更新这个表格可以直接作为项目 README 的接口说明也可供答辩时展示系统功能边界。5. 宿舍管理系统的前端页面与批量导入导出方案5.1 JSP JSTL 渲染列表页避免 Java 代码穿插JSP 页面里写% %脚本片段是老项目的通病分高不了。用 EL 表达式配合 JSTL 标签渲染页面逻辑清楚又容易维护。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle学生列表/title/head body table border1 tr th学号/th th姓名/th th性别/th th学院/th th楼栋/th th房间/th th床位/th /tr c:forEach varstu items${studentList} tr td${stu.studentId}/td td${stu.name}/td td${stu.gender}/td td${stu.college}/td td${stu.buildingName}/td td${stu.roomNumber}/td td${stu.bedNumber}/td /tr /c:forEach /table /body /html注意items${studentList}这里的 studentList 必须在 Servlet 里用req.setAttribute(studentList, list)设置不能放在 Session 里否则列表数据会影响其他请求。JSTL 的 uri 在 Tomcat 10 上要改成jakarta.tags.core这是很多旧项目代码直接迁移到新版 Tomcat 后页面报 500 的原因。5.2 用 POI 批量导入学生数据学生初始数据几百条一条条录不现实。常见做法是写一个 Excel 导入的 Servlet用 Apache POI 读取 .xlsx 文件逐行校验后批量插入。WebServlet(/admin/importStudents) MultipartConfig(maxFileSize 10 * 1024 * 1024) public class ImportStudentServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part filePart req.getPart(file); int successCount 0; int failCount 0; try (InputStream in filePart.getInputStream(); Workbook workbook new XSSFWorkbook(in)) { Sheet sheet workbook.getSheetAt(0); StudentDAO dao new StudentDAO(); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) continue; String studentId getCellString(row.getCell(0)); String name getCellString(row.getCell(1)); // 简单的非空校验 if (studentId.isEmpty() || name.isEmpty()) { failCount; continue; } if (dao.insertStudent(studentId, name, ...)) { successCount; } else { failCount; } } } catch (Exception e) { log(import failed, e); } resp.sendRedirect(req.getContextPath() /admin/importResult.jsp?success successCount fail failCount); } }MultipartConfig是必须的不写这个注解req.getPart(file)会抛出 IllegalStateException。注意getLastRowNum()返回的是索引比如有 100 行数据它返回 100但第 0 行是标题所以循环从 1 开始。如果有人往 Excel 里加了空行row null的判断能拦住空行但单元格为空的场景要看getCellString的实现通常用DateUtil和DataFormatter统一转字符串。5.3 答辩前能用的报表导出方案导出报表如果不想引入 POI 的重量级依赖可以用简单的 CSV 写法但中文内容在 Excel 打开时容易乱码需要输出带有 BOM 头。POI 本身不算复杂写入 Workbook 后设置响应头强制浏览器下载resp.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); resp.setHeader(Content-Disposition, attachment; filename URLEncoder.encode(宿舍入住报表.xlsx, UTF-8));Content-Disposition参数里文件名要 URLEncode否则不同浏览器对中文文件名解析不一致有的会显示乱码。这是课程设计里很多同学忽略的点。6. 数据校验、并发场景与答辩演示脚本6.1 宿舍管理系统的边界校验清单答辩时老师常问如果学生重复入住怎么办或退宿之后床位状态怎么恢复。把关键校验点整理成表格放在项目文档里也方便自己检查环节校验规则实现位置入住学生必须存在且未在宿Service入住房间必须存在且有床位状态为 0Service退宿student_id 必须有 status1 的记录Service移除学生学生不能在宿有 status1 记录时禁止删除Service删除房间房间下不能存在 bed_status 为 1 的床位DAO 关联查询导入数据学号不能与现有记录重复DAO INSERT 前查重数据库层加上外键约束后很多校验其实有双重保险但 Service 层的显式判断能给出友好的业务提示而不是抛出 SQLIntegrityConstraintViolationException 直接 500。6.2 用两个并发请求验证事务是否真的生效写完入住功能后用一个简单手法验证事务和行锁是否生效在 Service 的锁查询后面加一个Thread.sleep(5000)模拟慢处理然后开两个浏览器标签页同时提交两个不同学生的入住请求到同一个房间。正常情况下第二个请求会等待第一个提交后才执行如果第二个请求直接返回无空床位说明FOR UPDATE生效了。如果测试时发现两个请求同时成功大概率是setAutoCommit(false)没生效或者 Connection 不是同一个。检查一下 DBUtil 是不是每次调用getConnection()都返回同一个连接了比如有人图方便把 Connection 设为 static 字段这会在并发下直接串数据。6.3 答辩演示时按这条脚本走演示系统不要从头挨个点功能按一条业务主线走登录系统演示 Session 失效跳转直接访问studentList.jsp被 Filter 拦回登录页。导入一份 Excel 学生数据打开数据表让学生列表页出现新记录。选择一名未入住学生分配到一个四人间页面显示床位状态变为已入住。再点一次分配同一名学生提示已在宿验证业务校验。对该房间发起一条报修管理员列表页看到新报修更新状态为处理中。退出登录用学生账号进入系统尝试访问/admin/studentList页面返回 403。每一步之间如果有查询操作都指向同一个功能后再切下一个不要频繁在多个页面间跳来跳去演示节奏越紧凑老师对系统的印象分越高。项目根目录里放好README.md把 JDK 版本、Tomcat 版本、MySQL 初始化脚本和默认账号写清楚这也是高分项目里常被忽略的加分项。本文还有配套的精品资源点击获取
返回列表