news 2026/10/7 6:30:19

学生宿舍管理系统实战:基于Servlet+JSP+MySQL的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
学生宿舍管理系统实战:基于Servlet+JSP+MySQL的完整实现

简介:基于 Servlet、JSP 与 MySQL 实现的 JavaWeb 学生宿舍管理系统项目,适合正在学习 Java Web 的初学者,以及需要完成毕业设计或课程设计的学生。项目围绕宿舍管理这一常见业务场景展开,能够帮助读者理解浏览器与服务器之间的请求处理、数据库连接与数据访问、以及 MVC 分层架构的基本思想。压缩包内共包含 295 个文件,整体大小约十六点八八兆字节,主要文件包括 Java 源码、编译后的类文件、JSP 页面、MySQL 数据库脚本、样式表、脚本语言文件、图片字体等资源。项目既可以用作完整系统直接部署,也适合逐行阅读源码进行学习。目前已有五千九百二十一人浏览学习,说明这份资料在同类项目中具有不错的参考与下载价值。功能方面,系统涵盖登录验证、楼栋管理、宿舍分配、学生信息维护和宿舍管理员管理等模块,并提供数据库脚本与前端页面。阅读源码可以梳理数据访问对象封装、业务逻辑处理、页面跳转与前端渲染的完整流程,适合在此基础上二次开发、完善功能,也可作为期末项目答辩的补充材料。

1. 学生宿舍管理系统:Servlet+JSP+MySQL 组合下的 JavaWeb 入门必修课

学生宿舍管理系统是很多 JavaWeb 学习者的第一个完整项目:用 Servlet 处理业务请求、JSP 渲染页面、MySQL 持久化数据,三者拼起来就是一个覆盖学生入住、退宿、报修、访客登记的小型后勤系统。这套技术栈谈不上新潮,但架子小、链路完整,能让人在很短时间里把“浏览器 → Servlet → DAO → MySQL”这条主线跑通。如果你正在找课程设计题目,或者刚学完 Servlet 和 JSP 想动手做点东西,这个方向很合适:数据表不复杂,业务边界清楚,踩坑也都集中在真实项目里最常见的几类,不会一上来就被框架细节淹没。

2. 先建模再写码:五张核心表与宿舍分配的业务约束

2.1 数据关系与建表脚本:学生、宿舍、报修、管理员四张基准表

宿舍管理系统的数据模型,说到底是“谁住在哪、谁报修了什么、谁来访过”。最常见的做法是拆出五张表:宿舍表、学生表、报修表、访客登记表、管理员表。学生和宿舍是多对一关系,报修、访客分别跟学生关联,管理员独立负责后台操作。

建表时我习惯把约束一次写到位,避免后面业务代码里到处补判断。宿舍的“楼栋+房间号”要唯一,学生的“学号”要唯一,床位数和已住人数用默认值 0 初始化,这些都在 DDL 里声明。下面这份 SQL 是整篇文章的基座,后面所有代码都围绕它写。

CREATE TABLE dormitory ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '宿舍ID', building VARCHAR(20) NOT NULL COMMENT '楼栋,如 1号楼', room_no VARCHAR(10) NOT NULL COMMENT '房间号', bed_count INT NOT NULL DEFAULT 4 COMMENT '床位总数', occupied INT NOT NULL DEFAULT 0 COMMENT '已住人数', state TINYINT NOT NULL DEFAULT 0 COMMENT '0可用 1已满', UNIQUE KEY uk_building_room (building, room_no), KEY idx_state (state) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍表'; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT '学号', name VARCHAR(50) NOT NULL, gender CHAR(1) NOT NULL COMMENT '男/女', dorm_id INT NULL COMMENT '宿舍ID,退宿后置空', bed_no VARCHAR(10) NULL COMMENT '床位号,如 A-01', phone VARCHAR(20) NULL, check_in_date DATE NULL COMMENT '入住日期', UNIQUE KEY uk_student_no (student_no), KEY idx_dorm (dorm_id), CONSTRAINT fk_student_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, dorm_id INT NOT NULL, description VARCHAR(255) NOT NULL COMMENT '问题描述', state TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1处理中 2已完成', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_repair_state (state), KEY idx_repair_student (student_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报修表'; CREATE TABLE visitor ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, guest_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NULL, visit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, leave_time DATETIME NULL, remark VARCHAR(100) NULL, KEY idx_visitor_student (student_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='访客登记表'; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '建议存哈希,不要存明文', real_name VARCHAR(30) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';

这里几个细节值得说明。bed_count和occupied都设为 INT 并在应用层保证occupied <= bed_count,比用触发器直观得多;state字段用默认值 0,避免每次插入都手动传状态。外键fk_student_dorm在宿舍被删时能挡住引用,防止出现“学生住在不存在的宿舍”这种脏数据。索引我只在真正要查询的列上建:宿舍按楼栋和房间号查、报修按状态查、访客按学号查,单表数据量在几千到几万条时这些索引足够用,不要为每个字段都加索引,那是把写入性能白扔了。

2.2 宿舍分配与调宿:事务和行锁解决“床位超卖”

系统里最容易出错的不是查询,而是宿舍分配。一个标准的入住流程是:管理员查到有空床的宿舍,选一个床位,把学生插进 student 表,再把宿舍的 occupied 加 1。这个流程如果拆成“先查再更新”,两个管理员同时操作时就会出现同一间宿舍多住一个人的情况,也就是业务上的“超卖”。

这个问题并不是高并发才有的。哪怕只有两个操作员,只要中间隔了网络往返,就会出现同一时刻都读到 occupied=3、bed_count=4,然后各自住进一个人,最终 occupied 变成 5。解决思路是让“占用一个床位”变成一个原子操作:直接在 UPDATE 里加条件,只有当occupied < bed_count时才允许加一。

// RoomDao.java 核心方法 public boolean tryOccupyBed(int dormId) throws SQLException { String sql = "UPDATE dormitory SET occupied = occupied + 1 " + "WHERE id = ? AND occupied < bed_count"; try (Connection conn = JdbcUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, dormId); int rows = ps.executeUpdate(); return rows == 1; // 返回 true 说明这张床位真的占上了 } }

这段代码的关键在 WHERE 条件。occupied < bed_count把校验和更新放进了同一条 UPDATE,数据库会在行锁范围内判断条件,相当于用 MySQL 的行锁保证了不会超卖。返回的影响行数rows == 1就是信号:如果为 0,说明宿舍已经满了,业务层要提示操作人员换一间。

调宿则反过来,要把两个宿舍的 occupied 一起调整。旧宿舍减一、新宿舍加一,这两个操作必须在一个事务里完成,否则中途断电就会出现一边变了另一边没变的脏状态。常见做法是给 Service 层方法加上事务控制:

// MoveService.java 调宿逻辑,事务由外部统一管理 @Transactional(rollbackFor = Exception.class) public void moveStudent(int studentId, int oldDormId, int newDormId) { // 旧宿舍床位释放 dormDao.releaseBed(oldDormId); // 新宿舍床位占用,若返回 false 则抛出运行时异常触发回滚 boolean ok = dormDao.tryOccupyBed(newDormId); if (!ok) { throw new BusinessException("新宿舍已满,调宿失败,已回滚"); } // 更新学生的 dorm_id 和 bed_no studentDao.updateDorm(studentId, newDormId, generateBedNo(newDormId)); }

@Transactional在这里保证 releaseBed 和 tryOccupyBed 要么都成功、要么都失败。我在初始版本里没加事务,结果测试时发现调宿后学生表指向了新宿舍,但旧宿舍的 occupied 没减,列表页数据怎么都对不上。从那以后凡是跨表写操作,第一件事就是确认事务边界。

3. 环境与工程骨架:JDK、Tomcat、IDEA、MySQL 的版本搭配和最小配置

3.1 版本搭配与运行原理:Servlet 容器、JSP 编译期和 MySQL 驱动的匹配逻辑

很多人在环境上翻车,是因为拿新版本驱动配老 Tomcat,或者把 Servlet API 的依赖打进了 war 包,导致容器加载类冲突。先记住一套经过验证的组合:JDK 8、Tomcat 8.5 或 9、MySQL 8.0、IDEA 2021 之后任意版本,配套 Servlet 3.1/4.0 规范和 JSP 2.3。这套组合向下兼容性好,网上能查到的资料也最多,等你跑通了再考虑换 JDK 17 和 Tomcat 10 也不迟。

组件推荐版本说明
JDK8课程设计和绝大多数教材的默认环境,兼容性最好
Tomcat8.5.x 或 9.0.x支持 Servlet 3.1/4.0,老项目迁移成本低
MySQL8.0 系列5.7 已停止维护,8.0 的 caching_sha2_password 认证要配合新驱动
IDEA2021 及以上对 JavaWeb 的 Tomcat 集成比较完善
驱动mysql-connector-java 8.0.x驱动全类名是 com.mysql.cj.jdbc.Driver,和旧版不同

JSP 在这里的定位不是模板引擎,而是“Servlet 的另一种写法”:第一次被访问时,Tomcat 会把 JSP 翻译成 Servlet 源码,再编译成 class。所以 JSP 里能写的隐式对象 request、response、session,本质上都是 Servlet 方法里的局部变量。这个原理决定了后面排查问题的方向——JSP 报错时,看 Tomcat 的 work 目录下对应生成的 Java 文件,往往比看 JSP 源码更能定位问题。

MySQL 8.0 的驱动类和连接 URL 是新手最容易卡住的地方。旧版的com.mysql.jdbc.Driver在新驱动里已经不存在了,而且 8.0 默认使用caching_sha2_password认证插件,连接 URL 里少了allowPublicKeyRetrieval=true和serverTimezone参数就会直接报错。所以我把 JDBC URL 固定写成下面这个模板,后面所有 DAO 都复用。

String url = "jdbc:mysql://localhost:3306/dormitory?useSSL=false" + "&allowPublicKeyRetrieval=true" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=UTF-8";

useSSL=false是因为本地开发强制 SSL 握手毫无必要,还容易引发证书报错;allowPublicKeyRetrieval=true解决 8.0 认证插件密钥获取被拒的问题;serverTimezone不设置的话,日期类型映射会因为夏令时偏差报错;characterEncoding=UTF-8配合建库时的 utf8mb4,让中文链路从头到尾是一致的编码。

3.2 在 IDEA 里运行 JavaWeb 项目:Tomcat 配置和 mysql 驱动的三步走

我把一套能在 IDEA 里直接跑起来的最小工程配置拆成三步,照着做就能看到登录页。

第一步,创建 Maven Web 项目。在 IDEA 的 New Project 里选择 Maven Archetype 的maven-archetype-webapp,这一步会生成标准的 webapp 目录结构:src/main/java、src/main/resources、src/main/webapp。没有 src/main/java 目录就手动新建,这是后面写 Servlet 和 DAO 的地方。

第二步,在pom.xml里声明依赖。Servlet 和 JSP API 由 Tomcat 提供,所以 scope 用provided,避免打进 war 包;MySQL 驱动需要打进包,用compile。JSTL 是用来在 JSP 里循环渲染列表的,后面展示层要用。

<dependencies> <!-- Servlet API,Tomcat 自带,provided 防止和容器冲突 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 8 驱动,compile 会进入最终 war --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>

第三步,配置 Tomcat 运行。在 IDEA 的 Run/Debug Configurations 里新增 Tomcat Server Local,Deployment 选项卡把项目的war exploded加上,Application context 填/dormitory。然后启动,浏览器访问http://localhost:8080/dormitory/如果能打开默认 index,环境就通了。

这一步最常见的问题是”明明配置了,访问还是 404“。90% 的原因是 Deployment 里根本没加 Artifact,或者 Application context 填了/访问路径没带项目名。还有一种是依赖的 jar 没有进入最终产物,Tomcat 启动时报ClassNotFoundException: com.mysql.cj.jdbc.Driver,这时要检查 Maven 依赖是否正常下载,以及 war exploded 里 WEB-INF/lib 下是否有 mysql-connector-java 的 jar。

4. 分层实现:JDBC 封装、Servlet 控制层与 JSP 展示层的协作写法

4.1 数据库连接工具类与 DAO:把 JDBC 样板代码收敛到一处

Servlet、JSP、DAO 各管一段,这条分层的线必须清晰:DAO 只管 SQL 和结果集映射,Servlet 只管接收参数和决定跳转,JSP 只管用 JSTL 把数据渲染出来。我见过很多同学把 SQL 直接写在 Servlet 里,页面一多就到处是重复代码,改一个字段名要全项目搜索。先把 JdbcUtils 和 DAO 写扎实,后面无论加多少功能都轻松。

public class JdbcUtils { private static final String URL = "jdbc:mysql://localhost:3306/dormitory" + "?useSSL=false&allowPublicKeyRetrieval=true" + "&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "你自己的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

工具类里Class.forName在静态块中执行,保证驱动只注册一次。密码放在常量里只适合本地开发,课程设计阶段够了,但一定要知道这不是生产做法;真实项目里密码应该来自配置文件或环境变量。getConnection每次调用都建立一个物理连接,这个方案性能一般,但对宿舍管理系统这种并发很低的场景足够,先跑通再优化,不要一开始就上连接池。

DAO 的写法核心是 PreparedStatement 加表现则:任何用户输入都走setString、setInt这类方法,绝不直接拼进 SQL。下面这个方法是查询空闲宿舍列表,返回一个宿舍对象集合,供 JSP 页面展示。

public List<Dormitory> listAvailableRooms() throws SQLException { String sql = "SELECT id, building, room_no, bed_count, occupied " + "FROM dormitory WHERE occupied < bed_count ORDER BY building, room_no"; List<Dormitory> rooms = new ArrayList<>(); try (Connection conn = JdbcUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dormitory room = new Dormitory(); room.setId(rs.getInt("id")); room.setBuilding(rs.getString("building")); room.setRoomNo(rs.getString("room_no")); room.setBedCount(rs.getInt("bed_count")); room.setOccupied(rs.getInt("occupied")); rooms.add(room); } } return rooms; }

ORDER BY building, room_no是宿舍列表最常见的排序需求,楼栋名如果带数字,要注意字符串排序会按字典序排,10号楼会排在2号楼前面。真遇到这种情况,要么楼栋名补零,要么在 SQL 里用CAST转成数字排序。我早期没注意,列表里楼栋顺序乱成一团,后来统一在存储层避免了这个坑。

4.2 Servlet 控制层:登录会话与入住登记的链路拆分

Servlet 在系统里做两件事:从 request 里取参数、调用 Service/DAO、把结果放进 request 或 session、最后 forward 或 redirect 到 JSP。以登录功能为例,处理逻辑可以分成校验、会话、跳转三个环节。

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); AdminDao dao = new AdminDao(); Admin admin = dao.findByUsernameAndPassword(username, password); if (admin != null) { req.getSession().setAttribute("loginUser", admin); resp.sendRedirect(req.getContextPath() + "/dormitory/list"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

这里两个细节新手容易混。第一,doPost里必须第一件事设置req.setCharacterEncoding("UTF-8"),否则表单提交的中文在进入 Servlet 时就乱码了,后面再补救都白费。第二,登录成功用sendRedirect、失败用forward,两者的区别是重定向会让浏览器重新发起一次请求,地址栏会变化,这能防止用户刷新页面时再次提交表单;而 forward 只是服务端内部转发,request 里的error属性在 JSP 里还能读到。

入住登记比登录复杂,因为它涉及多个表。Servlet 只负责把学号、姓名、宿舍 ID 取出来,然后交给一个 Service 方法处理,Service 内部完成“占床位 + 插入学生”的事务。如果事务失败,Service 会抛出异常,Servlet 捕获后把错误消息放进 request,再 forward 回登记页。

@WebServlet("/checkin") public class CheckinServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); String studentNo = req.getParameter("studentNo"); String name = req.getParameter("name"); int dormId = Integer.parseInt(req.getParameter("dormId")); CheckinService service = new CheckinService(); try { service.checkin(studentNo, name, dormId); resp.sendRedirect(req.getContextPath() + "/dormitory/list"); } catch (BusinessException e) { // 会把异常信息显示到登记页 req.setAttribute("error", e.getMessage()); req.getRequestDispatcher("/checkin.jsp").forward(req, resp); } } }

Servlet 里不写业务逻辑的另一个原因是编译问题:业务逻辑放在 Service 里可以在单元测试里直接调用,而 Servlet 依赖容器提供的 request/response 对象,测试起来非常别扭。哪怕你只是为了交作业,把逻辑拆出来之后调试也省心很多。

4.3 JSP 展示层:JSTL 循环渲染与页面自动刷新的两个场景

JSP 页面负责把 DAO 查出来的 Java 对象展示成 HTML。我建议所有循环都用 JSTL 的c:forEach,而不是在 JSP 里写<% for (...) %>,后者会让页面里嵌一堆 Java 代码,可读性差,编译报错也难排查。宿舍列表页的核心片段如下。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h3>宿舍列表</h3> <table border="1"> <tr> <th>楼栋</th> <th>房间号</th> <th>床位</th> <th>已住</th> <th>状态</th> </tr> <c:forEach items="${roomList}" var="room"> <tr> <td>${room.building}</td> <td>${room.roomNo}</td> <td>${room.bedCount}</td> <td>${room.occupied}</td> <td> <c:choose> <c:when test="${room.occupied < room.bedCount}">有空床</c:when> <c:otherwise>已满</c:otherwise> </c:choose> </td> </tr> </c:forEach> </table> </body> </html>

${roomList}这个名字必须和 Servlet 里req.setAttribute("roomList", list)完全一致,JSTL 的c:forEach会自动调用room对象的 getter 方法。所以 JavaBean 里的属性名roomNo映射的是getRoomNo(),命名千万别写成roomno,否则页面上取不到值。这个报错不会以红色形式出现,只是显示空白,最坑。

关于“JSP 页面加载后自动刷新一次”这个诉求,常见场景是报修列表页:宿管盯着页面等新报修进来,不想手动刷新。有两种做法,一种是在<head>里加<meta http-equiv="refresh" content="30">,这是整页定时刷新,最简单但对整个页面有效;另一种是页面加载完成后用 JavaScript 只刷新局部数据,需要额外写 AJAX 接口。宿舍管理系统这种数据量,meta refresh完全够用,不要为了一个轮询效果引入前端框架。

5. 常见问题与避坑排查:乱码、驱动、404、并发分配床位

5.1 中文乱码三连:数据库、请求、响应三层分别处理

现象:页面显示中文全是问号,或者表单提交后在数据库里变成别的字符。排查顺序是从数据库往浏览器一层层看。

原因:数据库连接 URL 没带characterEncoding=UTF-8,建库时用了默认的 latin1,或者 Servlet 里没设置 request/response 编码,三层中任何一层断了都会乱码。Tomcat 8 以上对 URL 参数的 URIEncoding 默认是 UTF-8,所以地址栏传参通常没问题,问题集中在表单 POST 和数据库连接。

解决:建库时显式指定DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci,连接 URL 拼上characterEncoding=UTF-8,Servlet 的doPost第一行写req.setCharacterEncoding("UTF-8")。如果还有问题,检查 JSP 的contentType是否声明了charset=UTF-8,这几处统一后整个链路就对了。

5.2 MySQL 8 驱动类和 SSL 连接错误

现象:项目一启动调用 DAO 时报ClassNotFoundException: com.mysql.jdbc.Driver,或运行时抛SSL connection error、Public Key Retrieval is not allowed。

原因:驱动类写在代码里用的是旧版类名com.mysql.jdbc.Driver,新驱动 8.0 里已换成了com.mysql.cj.jdbc.Driver。SSL 错误和密钥检索问题则是因为 8.0 默认启用caching_sha2_password认证,本地连接时客户端需要额外的安全参数。

解决:统一改成com.mysql.cj.jdbc.Driver,连接 URL 加上useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai。如果你用的 Maven 依赖版本和 MySQL 服务端版本跨度很大,把 mysql-connector-java 升到 8.0.3x 系列,旧版驱动对 8.0 服务端的兼容性不好。

5.3 Tomcat 启动成功但 JSP 访问 404 或 500

现象:IDEA 里 Tomcat 日志显示启动成功,但浏览器访问项目路径就 404,或者访问某个 JSP 直接 500,后台报java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet。

原因:404 基本都是 IDEA 的 Deployment 配置漏了 Artifact,或者 Application context 和实际访问路径不一致。500 则是 war 包里打进了javax.servlet-api,与 Tomcat 自带的 Servlet API 冲突,导致类加载混乱。

解决:检查 Run Configuration 里的 Deployment,确认有项目的war exploded,路径和浏览器访问路径一致。把javax.servlet-api的 scope 改成provided,重新 Build 再启动。看后台日志时优先找Caused by,别被前面一长串堆栈吓住,根因往往在最后一两行。

5.4 并发入住导致床位超卖

现象:两个管理员同时给不同学生登记同一间只剩一个床位的宿舍,数据库最终出现occupied超出bed_count。

原因:业务层用的是“先 SELECT 查空床,再 UPDATE 加一”,两个请求都查到了还空着一个床位,然后先后执行 UPDATE,超卖就发生了。这是典型的读后写竞争。

解决:改成 2.2 里的原子更新UPDATE dormitory SET occupied = occupied + 1 WHERE id = ? AND occupied < bed_count,用 update 影响行数判断是否成功。如果你真的想上锁,也可以用SELECT ... FOR UPDATE把宿舍行锁住,但原子更新在性能上更优,也更好懂。

5.5 数据“丢了”和重启后状态对不上

现象:IDEA 里点了重启,登录用户还在,但新增的学生数据没了;或者宿舍调宿成功后,列表里一段时间后又变回去。

原因:把登录用户这类状态存在了 HttpSession 里,重启后 session 确实还在(Tomcat 会序列化),但业务数据如果放在静态 Map 或 JSP 的 application 域里,重启就会清空。更隐蔽的是,调宿时 UPDATE 没有提交,事务一直挂着,重启后自动回滚了。

解决:业务数据必须全部落到 MySQL,session、application 域只放会话级状态和零时的页面提示。所有写操作走事务,并且确认提交成功,不要依赖“数据库会自动保存”。我用过一句话提醒自己:内存里的数据不属于系统,重启即是终点。

6. 进阶技巧:分页筛选、报表导出与部署验证

6.1 动态条件查询与分页:用where 1=1拼 SQL 时注意参数顺序

宿舍管理做到后面一定会加筛选:按楼栋查、按状态查、按性别查。常见做法是动态拼 SQL,但这里有一个坑——拼接条件的顺序必须和PreparedStatement参数的顺序一致。我习惯把参数按固定顺序收集到 List 里,再逐个设置,而不是边拼边设。

public List<Student> searchStudents(String building, Integer state, int page, int size) throws SQLException { StringBuilder sql = new StringBuilder(); List<Object> params = new ArrayList<>(); sql.append("SELECT s.* FROM student s LEFT JOIN dormitory d ON s.dorm_id = d.id WHERE 1=1"); if (building != null && !building.isEmpty()) { sql.append(" AND d.building = ?"); params.add(building); } if (state != null) { sql.append(" AND d.state = ?"); params.add(state); } sql.append(" ORDER BY s.id LIMIT ?, ?"); params.add((page - 1) * size); params.add(size); try (Connection conn = JdbcUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } // 执行查询并映射结果 } }

where 1=1在这里不是偷懒,而是为了让后续AND片段拼接不判断是不是第一条。LIMIT 的两个参数(page - 1) * size和size遵循 MySQL 的偏移量规则:第一页是LIMIT 0, 10,第二页是LIMIT 10, 10。注意setObject按索引从 1 开始,参数顺序错了,筛选结果就会张冠李戴,这是动态 SQL 里最典型也最隐蔽的错误。

6.2 CSV 导出与部署验证:一台机器上跑通完整流程

报修记录导出成 Excel 是宿管常提的需求。Excel 的 xlsx 格式要引 POI 依赖,对课程设计来说偏重,简单方案是导出 CSV 文件,Excel 和 WPS 都能打开。关键点是要把响应头的 Content-Type 设为text/csv,并且给中文加上 UTF-8 BOM,否则打开全是乱码。

resp.setContentType("text/csv;charset=UTF-8"); resp.setCharacterEncoding("UTF-8"); PrintWriter out = resp.getWriter(); out.write("\uFEFF"); // UTF-8 BOM,Excel 中文乱码的后悔药 out.println("报修单号,学号,描述,状态,提交时间"); for (Repair r : list) { out.println(r.getId() + "," + r.getStudentNo() + "," + r.getDescription() + "," + r.getState() + "," + r.getCreateTime()); } out.flush();

部署验证阶段,我一般会打包成 war 放到 Tomcat 的 webapps 目录下,而不是只在 IDEA 里跑。打包前把 scope 是provided的依赖核对一遍,然后执行mvn clean package,把生成的target/dormitory.war丢进 webapps,启动 Tomcat,访问http://localhost:8080/dormitory/。验证照着业务主线走一遍:管理员登录 → 新建宿舍 → 学生入住 → 报修 → 调宿 → 导出报表,每一步都确认数据写进了 MySQL,再回头看页面显示。

这套系统我前前后后带过不少同学做课程设计,最常见的翻车是把状态放在 JSP 的 session 里,一重启全丢,最后老老实实改回查数据库。技术本身不复杂,难的是每一步都按层走、不加戏,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 6:30:04

AI网关实战:多模型统一接入、Token管理与MCP工具调用

1. 从一次线上事故说起&#xff1a;为什么直连大模型迟早要出问题去年冬天&#xff0c;我负责的一个智能客服系统在凌晨两点突然大面积超时。排查到天亮才发现&#xff0c;不是模型服务挂了&#xff0c;而是我们同时在三个业务线里硬编码了三套不同的模型调用逻辑——A业务线用…

作者头像 李华
网站建设 2026/10/7 6:29:25

AI Native研发范式:从智能体开发到评测闭环的落地手册

这两年我带团队做了好几个智能体项目&#xff0c;最深的感受是&#xff1a;AI Native不是一个适合贴在PPT上的概念&#xff0c;而是一套能把模型、数据、人和工具真正捏在一起的研发方式。这篇手册是团队从“用AI辅助写代码”过渡到“按AI Native方式组织整个研发过程”之后沉淀…

作者头像 李华
网站建设 2026/10/7 6:29:13

AI测试效率提升实战:25个可复用Skill体系与Agent工作流设计

1. 这套 Skill 体系到底解决了什么问题先说说我为什么攒了这么一套东西。日常做 AI 测试和 Agent 开发&#xff0c;最头疼的不是模型能力不够&#xff0c;而是每次遇到新任务都要从头写提示词、重新调流程、重新验证输出格式。一个测试用例生成任务&#xff0c;今天用这个模板&…

作者头像 李华
网站建设 2026/10/7 6:28:59

SVM电网负荷预测实战:SVR原理、特征工程与sklearn代码详解

简介&#xff1a;面向电力系统负荷预测场景&#xff0c;这份MATLAB资源基于支持向量机&#xff08;SVM&#xff09;与支持向量回归&#xff08;SVR&#xff09;实现电网负荷预测&#xff0c;代码完整、附有数据与注释&#xff0c;适合本科及以上学历的研究者、电力从业者进行算…

作者头像 李华
网站建设 2026/10/7 6:28:43

SpringAI 智能审核上 K8s:容器化到高可用部署全解

这套 SpringAI 智能审核项目&#xff0c;写到现在已经是第十八掌。前面我们已经折腾过模型接入、提示词调优、Redis 集群缓存&#xff0c;也解决过并发压测下各种玄学问题&#xff0c;但说实话&#xff0c;代码能跑和能稳定跑完全是两回事。这一掌取名“神龙摆尾”&#xff0c;…

作者头像 李华
网站建设 2026/10/7 6:28:24

从零实现自定义指令:软硬件协同设计全流程解析

一次搞懂指令集设计&#xff1a;从零实现自定义指令做开发的人&#xff0c;尤其是接触过芯片设计、嵌入式工具链或者虚拟化方案的&#xff0c;多少都会碰到“自定义指令”这个词。我第一次认真研究它&#xff0c;不是因为好奇&#xff0c;而是被一个实际需求逼的&#xff1a;跑…

作者头像 李华