JavaWeb公寓管理系统:Servlet+DAO分层设计与答辩要点解析
2026/9/14 3:41:01 网站建设 项目流程

简介:一份基于JavaWeb+MySQL的大学生公寓管理系统完整课程设计源码包,面向计算机专业正在完成期末大作业或课程设计的学生,以及需要项目实战练习的初学者。系统覆盖登录认证、宿舍楼管理、学生信息维护、记录管理等典型模块,代码分层清晰,适合作为JavaWeb+Servlet+JSP+JDBC技术栈的参考项目。资源共242个文件,其中含25个Java源文件、25个class字节码文件、24个JSP页面以及78个JS脚本,另有SQL数据库脚本和CSS样式表等,压缩包整体仅6.7MB,便于快速下载部署。项目附带导师指导并通过的98分课程设计成果,源码与数据库配套完整,可直接导入IDE运行,也可在此基础上扩展功能。当前已有1348人学习下载,尤其适合需要完整项目参考、理解Servlet+Dao分层开发或完成期末答辩准备的读者。

1. 公寓管理系统的Servlet+DAO结构,比SpringBoot版更适合答辩讲解

期末大作业选JavaWeb+MySQL技术栈,常被老师追问一句“你写的Servlet凭什么能支撑三个角色同时使用”。大学生公寓管理系统刚好把这个问题摆到台面上:宿舍管理员、学生、系统管理员三种身份访问同一套数据,却不能互相越权。源码里实际存在的DormBuildServlet、RecordServlet、StudentServlet、LoginServlet与对应DAO组成了一套不用Spring也能讲清楚的分层实现。对正在做课程设计的学生,它提供了可直接运行的期末大作业参考;对写过几年业务代码的人,它恰恰是回顾Servlet原生API、JDBC连接管理与请求分发的最小样本。

2. DormBuildServlet与RecordServlet的模块边界:请求分发与角色操作入口

2.1 DormBuildServlet的action参数约定:一个Servlet撑起宿舍楼管理

DormBuildServlet管理的资源是宿舍楼。页面端通过表单隐藏域或者URL拼接把action参数传过来,Servlet根据action的值决定是新增、修改、删除还是跳转列表页。为什么不用REST风格路径?课程设计阶段保持get/post两种方法,配合action字符串,能让前端JSP的表单写法更直观,答辩时也更容易解释。

@WebServlet("/dormBuild") public class DormBuildServlet extends HttpServlet { private DormBuildDao dormBuildDao = new DormBuildDao(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { request.setAttribute("buildList", dormBuildDao.findAll()); request.getRequestDispatcher("/dormBuildList.jsp").forward(request, response); } else if ("preAdd".equals(action)) { request.getRequestDispatcher("/dormBuildAdd.jsp").forward(request, response); } else if ("preUpdate".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); request.setAttribute("build", dormBuildDao.findById(id)); request.getRequestDispatcher("/dormBuildUpdate.jsp").forward(request, response); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("add".equals(action)) { DormBuild build = new DormBuild(); build.setName(request.getParameter("name")); build.setIntro(request.getParameter("intro")); dormBuildDao.save(build); } else if ("update".equals(action)) { DormBuild build = new DormBuild(); build.setId(Integer.parseInt(request.getParameter("id"))); build.setName(request.getParameter("name")); build.setIntro(request.getParameter("intro")); dormBuildDao.update(build); } else if ("delete".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); dormBuildDao.deleteById(id); } response.sendRedirect("dormBuild?action=list"); } }

这里的doGet只负责读操作和页面跳转,doPost统一处理写操作。关键的参数约定是action:add、update、delete、list、preAdd、preUpdate,分别对应新增、修改、删除、查询列表、跳转新增页、跳转修改页。name和intro是宿舍楼名称与简介字段。需要特别注意的是doPost最后统一重定向到查询列表,避免用户刷新页面时重复提交表单,这是课程设计里很容易被忽略的PRG模式。

提示:如果部署后出现中文乱码,先检查doPost里是否有request.setCharacterEncoding("UTF-8"),而且要放在读取任何参数之前。

2.2 RecordServlet的归寝记录与卫生记录合并查询

RecordServlet承担的是公寓日常管理里出现频率最高的操作:归寝记录和卫生检查记录。它的难点在于查询条件不固定——宿舍管理员可能只查某一个宿舍楼,也可能按日期查某一天的未归寝人员;学生端则只关心自己的记录。

@WebServlet("/record") public class RecordServlet extends HttpServlet { private RecordDao recordDao = new RecordDao(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String studentNumber = request.getParameter("studentNumber"); String buildId = request.getParameter("buildId"); String date = request.getParameter("date"); List<Record> records = recordDao.search(studentNumber, buildId, date); request.setAttribute("recordList", records); request.getRequestDispatcher("/recordList.jsp").forward(request, response); } }

这个查询接口把三个参数全部交给RecordDao去拼接SQL。studentNumber精确匹配学生学号,buildId关联宿舍楼,date用于筛选某一天的记录。三个参数都允许为空,意味着管理员可以不带任何条件直接查全部记录,也可以只带一个条件缩小范围。这种设计比写死三个查询方法更省代码,但要求DAO里对空值做判断。

2.3 StudentServlet与DormManagerServlet的权限边界

StudentServlet负责学生档案的维护,DormManagerServlet负责宿舍管理员的维护。两者结构相似,但权限范围不同:学生档案的修改通常由系统管理员操作,宿舍管理员只能查看;而DormManagerServlet只在系统管理员模块里暴露。源码里将二者拆成两个Servlet而不是合并,主要就是为了配合JSP页面的目录权限控制。

Servlet类管理资源可执行操作面向角色
DormBuildServlet宿舍楼增删改查系统管理员
StudentServlet学生档案增删改查、按宿舍楼过滤系统管理员、宿舍管理员
RecordServlet归寝/卫生记录查询、录入宿舍管理员、学生(只读)
DormManagerServlet宿舍管理员账号增删改查系统管理员
PasswordServlet当前登录者密码修改三种角色通用
LoginServlet登录态登录、注销三种角色共用

这张表在答辩时可以直接用作功能清单的口述提纲,也解释了为什么模块划分不能照搬SpringBoot的三层架构:这里每个Servlet对应一类资源,而不是对应一种角色操作,角色过滤放在登录后的JSP页面判断里。这种简化在数据量不大的课程设计场景里完全够用。

3. MySQL表结构与DAO层SQL:从建表语句到多表联查的写法

3.1 四张核心表的字段设计与外键关联

公寓管理系统的数据模型围绕四条线展开:学生、宿舍楼、宿管、记录。大多数学员的表设计会忽略一个点:记录表需要同时关联学生和宿舍楼,才能支持“查某栋楼的未归寝人数”这类常见问题。

CREATE TABLE t_dorm_build ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '宿舍楼名称', intro VARCHAR(200) COMMENT '宿舍楼简介' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_number VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, sex VARCHAR(10), build_id INT COMMENT '关联宿舍楼', dorm_room VARCHAR(20) COMMENT '房间号', CONSTRAINT fk_student_build FOREIGN KEY (build_id) REFERENCES t_dorm_build(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_dorm_manager ( id INT PRIMARY KEY AUTO_INCREMENT, manager_name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, build_id INT, CONSTRAINT fk_manager_build FOREIGN KEY (build_id) REFERENCES t_dorm_build(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, record_date DATE NOT NULL, status VARCHAR(20) COMMENT 'normal-正常 late-晚归 absent-未归', type VARCHAR(20) COMMENT 'attendance-归寝 health-卫生', detail VARCHAR(200), CONSTRAINT fk_record_student FOREIGN KEY (student_id) REFERENCES t_student(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里把宿舍管理员也挂到了t_dorm_build上,含义是一个宿管负责一栋楼。t_record通过student_id关联t_student,而t_student又通过build_id关联t_dorm_build,所以从记录到宿舍楼是两级关联。设计上的取舍是把卫生和归寝放同一张表,用type字段区分,好处是减少表数量,坏处是查询时要多带一个type条件。

3.2 StudentDao与DormBuildDao的JDBC模板写法

DAO层的写法基本是固定的:获取连接、预编译SQL、设置参数、执行查询、封装结果集、关闭资源。这门课程设计里的StudentDao核心方法包括findAll、findById、save、update、deleteById,以及一个按宿舍楼过滤学生的findByBuildId。

public class StudentDao { public List<Student> findByBuildId(int buildId) { String sql = "SELECT id, student_number, name, sex, build_id, dorm_room " + "FROM t_student WHERE build_id = ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, buildId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setStudentNumber(rs.getString("student_number")); s.setName(rs.getString("name")); s.setSex(rs.getString("sex")); s.setBuildId(rs.getInt("build_id")); s.setDormRoom(rs.getString("dorm_room")); list.add(s); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }

这里使用PreparedStatement而不是Statement,第一个原因是参数通过setInt传入,避免手工拼接整数时出现SQL注入;第二个原因是MySQL会对预编译SQL做执行计划缓存。配置MySQL连接串时,建议在JDBC URL后加上useSSL=false和characterEncoding=utf8,否则高版本MySQL客户端会在连接时报警告。DBUtil.getConnection()是自定义的工具类,内部读取db.properties里的driver、url、username、password。

3.3 RecordDao的联表查询与条件拼接

RecordDao的search方法需要处理2.2里传进来的三个可选参数。SQL层面要注意JOIN的方向:以t_record为主表,LEFT JOIN t_student和t_dorm_build,这样即使某条记录的学生被删除,历史记录仍然能查询出来。

public List<Record> search(String studentNumber, String buildId, String date) { StringBuilder sql = new StringBuilder(); sql.append("SELECT r.id, s.student_number, s.name, s.dorm_room, ") .append("d.name AS build_name, r.record_date, r.status, r.type, r.detail ") .append("FROM t_record r ") .append("LEFT JOIN t_student s ON r.student_id = s.id ") .append("LEFT JOIN t_dorm_build d ON s.build_id = d.id ") .append("WHERE 1 = 1 "); List<Object> params = new ArrayList<>(); if (studentNumber != null && !studentNumber.trim().isEmpty()) { sql.append("AND s.student_number = ? "); params.add(studentNumber.trim()); } if (buildId != null && !buildId.trim().isEmpty()) { sql.append("AND s.build_id = ? "); params.add(Integer.parseInt(buildId)); } if (date != null && !date.trim().isEmpty()) { sql.append("AND r.record_date = ? "); params.add(date.trim()); } sql.append("ORDER BY r.record_date DESC, r.id DESC"); // 使用PreparedStatement执行sql.toString()并依次绑定params }

“WHERE 1 = 1”在这里不是多余写法,它的作用是让后续每个AND条件都无需判断是否是第一个条件,代码结构清晰很多。date参数直接传字符串,依赖MySQL对'2025-06-01'的隐式转换,但更稳妥的做法是用java.sql.Date而非字符串。这个search方法体现了课程设计里最常见的查询封装套路:StringBuilder拼接SQL主体,List收集参数,最后统一绑定。

查询场景WHERE条件参数来源
查全部记录无条件
查某学号记录s.student_number = ?学生输入框
查某栋楼记录s.build_id = ?楼栋下拉框
查某天记录r.record_date = ?日期选择器
组合查询同时满足多个条件三个控件组合

提示:导入数据库脚本时如果报错,先把SQL文件里的表顺序理清楚,先导t_dorm_build,再导t_student和t_dorm_manager,最后导t_record。外键依赖要求父表必须先存在。

4. LoginServlet登录鉴权与PasswordServlet密码修改的实现细节

4.1 LoginServlet的会话管理与角色标识写入

登录逻辑是所有模块的门禁。LoginServlet根据用户名和密码分别去t_dorm_manager和t_student两张表里查询,查不到就说明账号不存在,查到了就把用户信息写入session。

@WebServlet("/login") public class LoginServlet extends HttpServlet { private DormManagerDao dormManagerDao = new DormManagerDao(); private StudentDao studentDao = new StudentDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); DormManager manager = dormManagerDao.findByUsernameAndPassword(username, password); if (manager != null) { request.getSession().setAttribute("userId", manager.getId()); request.getSession().setAttribute("userName", manager.getManagerName()); request.getSession().setAttribute("userType", "manager"); request.getSession().setAttribute("buildId", manager.getBuildId()); response.sendRedirect("dormBuild?action=list"); return; } Student student = studentDao.findByNumberAndPassword(username, password); if (student != null) { request.getSession().setAttribute("userId", student.getId()); request.getSession().setAttribute("userName", student.getName()); request.getSession().setAttribute("userType", "student"); request.getSession().setAttribute("buildId", student.getBuildId()); response.sendRedirect("student?action=detail&id=" + student.getId()); return; } request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }

这里先查询宿管表,再查询学生表,两个表都命中不了就返回登录页并显示错误信息。userId、userName、userType、buildId这四个session属性中,userType是后续所有页面判断权限的依据,buildId则保存了当前用户所属的宿舍楼,避免学生跨楼查看数据。注意宿管登录后跳转到宿舍楼列表页,学生登录后跳转到个人详情页,这是两种角色在系统里的默认首页差异。

4.2 页面端角色判断与JSP标签的使用

登录后不能让每个Servlet都重复判断角色。这个项目在JSP页面端通过session里的userType配合c:if标签做菜单级的权限控制,是一种零成本的轻量方案。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <div class="menu"> <c:if test="${sessionScope.userType == 'manager'}"> <a href="dormBuild?action=list">宿舍楼管理</a> <a href="dormManager?action=list">宿管账号管理</a> </c:if> <c:if test="${sessionScope.userType == 'manager' || sessionScope.userType == 'student'}"> <a href="record?action=list">查寝记录</a> </c:if> <a href="password?action=preUpdate">修改密码</a> <a href="login?action=logout">退出登录</a> </div>

sessionScope.userType是上一节LoginServlet里写入的会话属性。manager可以看到宿舍楼管理和宿管账号管理菜单,student看不到;查寝记录两个角色都能看,但RecordServlet里还要根据userType决定是否过滤当前学生的记录。这种页面端控制在课程设计场景下比写拦截器更直观,缺点是如果一个JSP被直接访问,无法防止绕过菜单操作,所以关键写操作仍要靠Servlet端再次判断。

4.3 PasswordServlet的密码修改流程

密码修改是这个项目里容易被忽略的模块,但答辩时老师几乎必问。它的流程是:登录用户输入旧密码和新密码,Servlet先根据session里的userId去对应角色表里核对旧密码,核对通过后再执行UPDATE。

@WebServlet("/password") public class PasswordServlet extends HttpServlet { private DormManagerDao dormManagerDao = new DormManagerDao(); private StudentDao studentDao = new StudentDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); HttpSession session = request.getSession(); String userType = (String) session.getAttribute("userType"); int userId = (Integer) session.getAttribute("userId"); String oldPwd = request.getParameter("oldPassword"); String newPwd = request.getParameter("newPassword"); String confirmPwd = request.getParameter("confirmPassword"); if (!newPwd.equals(confirmPwd)) { request.setAttribute("errorMsg", "两次输入的新密码不一致"); request.getRequestDispatcher("/passwordUpdate.jsp").forward(request, response); return; } boolean ok = false; if ("manager".equals(userType)) { ok = dormManagerDao.updatePassword(userId, oldPwd, newPwd); } else if ("student".equals(userType)) { ok = studentDao.updatePassword(userId, oldPwd, newPwd); } if (ok) { session.invalidate(); response.sendRedirect("login.jsp"); } else { request.setAttribute("errorMsg", "旧密码错误或更新失败"); request.getRequestDispatcher("/passwordUpdate.jsp").forward(request, response); } } }

统一从session里取userType来决定调用哪个DAO的updatePassword,避免了再写一个专门的密码表。在updatePassword内部,SQL是UPDATE t_student SET password = ? WHERE id = ? AND password = ?,一次UPDATE同时完成旧密码校验和新密码修改。改密成功后直接session.invalidate()销毁会话,强制用户重新登录,这是一种简单但有效的安全习惯。

角色修改密码后会话处理
系统管理员重新登录invalidate
宿舍管理员重新登录invalidate
学生重新登录invalidate

5. 答辩演示时最容易翻车的三个细节

5.1 分页查询:用LIMIT限制记录条数

RecordServlet的列表页在数据量小的时候没问题,但答辩演示时老师可能会往数据库里插入几十条记录,然后问“为什么页面没有分页”。给查询加上LIMIT是关键的第一步,也是最容易在DAO层实现的优化。常见做法是给search方法增加offset和limit两个参数,SQL末尾拼接LIMIT ?, ?,第一个参数是起始下标,第二个参数是每页条数。这里要注意PreparedStatement对LIMIT参数的绑定方式,MySQL支持setInt直接设置这两个占位符,不需要手工拼进SQL字符串。

sql.append("ORDER BY r.record_date DESC, r.id DESC LIMIT ?, ?"); // 从0开始计算偏移量:(pageNum - 1) * pageSize

在JSP端配合一个简单的页码条,把总记录数先COUNT出来再除以pageSize得到总页数,就能解决课程设计里的分页问题。这个实现不依赖PageHelper,因为原生JDBC场景下引入分页插件反而会增加配置复杂度,答辩时也不好解释。

5.2 删除宿舍楼之前先检查外键引用

t_student表的build_id和t_dorm_manager表的build_id都关联了t_dorm_build,直接执行DELETE FROM t_dorm_build WHERE id = 1会触发外键约束错误。答辩现场如果演示到这个操作,报错信息会直接暴露在外键处理上的缺失。常见做法是删除前先查两张关联表是否有引用数据。

SELECT COUNT(*) FROM t_student WHERE build_id = 1; SELECT COUNT(*) FROM t_dorm_manager WHERE build_id = 1;

两条查询都返回0时才允许删除,否则在Servlet里返回提示“该宿舍楼仍有学生或宿管关联,无法删除”。也可以使用ON DELETE SET NULL让外键字段自动置空,但这样做会让学生失去所属宿舍楼信息,不建议在公寓管理场景使用。

5.3 用SQL直接验证数据变化

答辩演示代码操作结束后,老师往往会直接打开MySQL命令行查看数据。建议在演示前准备好几条验证SQL:查某个宿舍楼的学生数量、查某天的归寝记录、查某学生最近一条记录。这些查询能证明系统写入的数据真正落到了数据库,而不是只存在内存里。

SELECT d.name AS 宿舍楼, COUNT(s.id) AS 学生数 FROM t_dorm_build d LEFT JOIN t_student s ON d.id = s.build_id GROUP BY d.id; SELECT s.student_number, s.name, r.record_date, r.status FROM t_record r JOIN t_student s ON r.student_id = s.id WHERE r.record_date = '2025-06-01';

第一条SQL用LEFT JOIN保证没有学生的宿舍楼也会出现在结果里,第二条SQL展示指定日期的归寝状态。这两条查询分别对应“宿舍楼管理”和“记录查询”两个模块的数据库验证,答到点上比背概念更有说服力。

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

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

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

立即咨询