ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java学生宿舍管理系统:从数据库设计到并发避坑的完整实战

Java学生宿舍管理系统:从数据库设计到并发避坑的完整实战 简介基于Java的学生宿舍管理系统是一套面向高校宿舍管理场景的完整毕业设计源码采用SpringBootMySQLBootstrap架构按管理员、宿管、学生三类角色划分权限覆盖学生、楼宇、宿舍、入住、宿管及管理员管理模块支持增删改查操作与Excel数据表格批量导入导出适合Java学习者用于课程设计或毕业设计二次开发。资源包总体积72.37MB共661个文件以js、css、less、scss等前端静态资源java、class等后端代码以及sql数据库脚本为主同时包含doc/docx说明文档和少量mp4操作演示视频文件结构齐全便于直接导入开发环境查看运行效果。目前已有467人学习或下载。从学习和二次开发角度看压缩包内准备了MySQL5与MySQL8两套项目版本可适应不同数据库环境。后端包含登录验证码、文件导入导出等具体实现便于参考多角色权限控制、业务模块分层的编码思路在此基础上可快速改造成适合课程设计或毕业设计答辩的宿舍管理项目。1. 基于Java的学生宿舍管理系统一个看似「老土」的题目为什么仍值得完整做一遍「基于Java的学生宿舍管理系统」是Java课程设计与毕业设计里出现频率最高的题目之一。它不像电商系统那么浮夸也不像图书管理系统那么单薄恰好落在一个舒服的位置一张床位对一个学生、一次入住对应一笔账单能把面向对象建模、JDBC、Servlet 生命周期、事务和会话管理这些 Java 基础一条线串起来。做完它你简历上才算有一个能讲满十分钟的完整项目。这篇文章适合正在赶课程设计的在校生、想补 Java 基础的自学者以及要带新人做内部培训的团队。我会按「脆弱方案 → 能跑方案 → 带亮点的面试方案」的顺序把选型、建表、代码、翻车点一次写透你照着复现即可。2. 技术栈怎么定JSPServletJDBC为什么仍是这个题目的安全答案标题只写了「基于Java」没有指定框架这是个可以钻的空子也是容易纠结的地方。常见做法有三种传统 JSP Servlet JDBC、SSMSpring SpringMVC MyBatis、Spring Boot 加前后端分离。放到课程设计或入门练手的场景里我更推荐第一种。SSM 适合求职向深化Spring Boot 适合你已经有底子之后加速开发但对一个一两周内要交、还要现场答辩的项目JSPServletJDBC 的每一行代码都长在自己写的类里评审问「Servlet 生命周期」「Session 怎么存登录态」「事务怎么开的」你都能直接指着代码回答不用背框架源码。另一点是并发量级。宿舍管理系统的真实规模就是几百个人查寝、报修、看水电单机单库完全够用Session 天然支持。不要一上来就上 Redis、JWT、消息队列那是在给课程设计加风险不是加亮点。下面按最常见的工程结构把环境、目录和分层先立起来。2.1 环境与目录骨架JDK 8 起步war 包长什么样开发环境别追新JDK 8 或 11 都行Tomcat 9Maven 3.6 以上。先把 JDK 装好并验证java -version能输出版本这一步卡住的人最多多半是 JAVA_HOME 配置到了 jre 目录而不是 jdk 目录。装完不用 IDE 也能编译但实际开发建议用 IDEA 的 Community 版导入 Maven 项目后直接把 Tomcat 配进去。Maven 工程骨架长这样dormitory-system/ ├── pom.xml └── src/main/ ├── java/com/dorm/ │ ├── entity/ # 与表对应的实体类 │ ├── dao/ # JDBC 数据访问 │ ├── service/ # 业务逻辑 │ ├── servlet/ # 控制器 │ └── filter/ # 登录与编码过滤器 ├── resources/db.properties └── webapp/ ├── WEB-INF/web.xml ├── jsp/ # 页面 └── static/ # css/jsentity、dao、service、servlet、filter 五个包是这套代码的主干。entity 只放字段和 getter/setterdao 只写 SQLservice 处理业务规则servlet 收参数和跳页面filter 做横切。这样分层的好处是后面每加一个功能比如「调宿」你只需要在 service 加一个方法不用动其他包。pom.xml 用下面这套依赖就够project ... modelVersion4.0.0/modelVersion groupIdcom.dorm/groupId artifactIddormitory-system/artifactId version1.0.0/version packagingwar/packaging dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies build finalNamedorm/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.8.1/version configuration source1.8/source target1.8/target encodingUTF-8/encoding /configuration /plugin /plugins /build /project版本号以你本地 Maven 仓库能拉到的为准上面是这三组坐标里我常用的组合。javax.servlet-api 的 scope 必须是 provided因为 Tomcat 自带了 Servlet 实现打包进 war 反而会冲突。finalName 决定了访问路径这里配成dorm启动后访问http://localhost:8080/dorm/。工程跑起来有两种常见方式一种是 IDEA 里配置 Tomcat把 war exploded 部署上去断点调试方便另一种是mvn clean package打出 war 包丢到 Tomcat 的 webapps 目录适合最后演示前稳定运行。课程设计阶段建议用第一种答辩前切到第二种省得演示到一半 IDE 卡死。两种方式都需要先确认 Tomcat 端口不冲突改conf/server.xml里的Connector port8080即可。2.2 面向对象建模实体、DAO、Service、Servlet 四层怎么分工很多课程设计写成「一个 Servlet 干到底」查询写死、SQL 拼接、页面跳转也写在 doPost 里。这种代码交上去自己都觉得不踏实答辩问「面向对象体现在哪」只能指着实体类的 getter 说封装。要想把封装、继承、多态讲清楚最少要有下面这张分工表层职责典型类entity表和对象的映射只有属性与 getter/setterStudent、DormRoom、RepairOrderdao数据库读写只返回实体或 List不写业务判断StudentDao、RoomDaoservice事务、校验、状态流转业务规则只在这里StudentService、RepairServiceservlet取参数、调 service、转发或重定向到 JSPLoginServlet、CheckInServlet以入住登记为例servlet 收学号和房间号service 先查学生状态、再查房间余量、最后写库并提交事务dao 只执行单条 SQL。规则的变动比如「男生不能住女生楼」只改 service 一处这就是分层带来的第一个收益。包和类名建议按业务名来Student、DormRoom、RepairOrder、WaterElecBill、SysUser 五组对象对应数据库五张核心表。还有一个常见的误用是校验写在 JSP 里。列表页、入住页、调宿页都要判断「房间是否已满」你要是每个页面都写一段 if改一次规则要改三个地方。正确做法是把isRoomAvailable(roomId)这类判断收进 RoomServiceServlet 只负责调用并转发结果。答辩时这就是一个标准的「职责单一」案例比背概念好讲得多。2.3 公共组件先行DBUtil 与 db.properties把地基打好写 DAO 之前先封装一个 DBUtil否则每个 DAO 里都有一遍 DriverManager.getConnection 的样板代码。用 Druid 连接池是最省事的方案它在 db.properties 里配好参数即可driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/dorm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait60000url 里的 useUnicode、characterEncoding、serverTimezone 三个参数不是可选项。前两个不配中文十有八九变问号第三个不配MySQL 8 的驱动会拿服务器默认时区去解析时间很容易报 CST 相关的错。DBUtil 的核心就一段静态初始化public class DBUtil { private static DataSource ds; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties p new Properties(); p.load(in); ds DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }连接池初始化放在静态块里类第一次被加载时就完成失败直接抛 ExceptionInInitializerError避免项目启动后第一个请求才发现连不上库。getConnection 每次从池里取用完由调用方关闭后续做事务时我会给同一个线程绑定同一个 Connection用 ThreadLocal 保存这块到第 4 章展开。公共组件就这些别提前设计一堆工具类业务代码还没写工具类先膨胀那也是另一种过度设计。3. 数据库设计先行五张核心表怎么建才能少写一堆补丁代码写代码前先把表设计想清楚这是宿舍管理系统里性价比最高的一步。常见课程设计只写宿舍、学生、水电、报修四张表但登录账户一般会单独建一张 sys_user把管理员、宿管、学生三种角色都放进去。五张表的关系是sys_user 管登录student 关联 dorm_roomwater_elec_bill 和 repair_order 都挂在 student 下面。设计表时我会盯三件事唯一约束、状态字段、金额类型。唯一约束负责防止数据重复状态字段替代各种 DELETE金额类型保证计算不丢精度。下面分表写。3.1 宿舍房间表楼栋、房号、容量与 occupied 冗余建 dorm_room 的 SQL 可以直接抄字段含义都写在注释里CREATE TABLE dorm_room ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 房间ID, building_no VARCHAR(10) NOT NULL COMMENT 楼栋编号如 3栋, room_no VARCHAR(10) NOT NULL COMMENT 房间号如 301, capacity TINYINT UNSIGNED NOT NULL DEFAULT 4 COMMENT 床位数, occupied TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 已住人数冗余字段, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用 2维修中, remark VARCHAR(200) DEFAULT COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_building_room (building_no, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;capacity 和 occupied 两个字段并存是刻意的冗余。查询「哪些房间有空床」时直接WHERE occupied capacity一条 SQL 就出来不用先查房间再 count 学生。代价是入住和退宿时必须同步更新 occupied这个一致性由 Service 层的事务保证后面讲。UNIQUE KEY 防止同一栋楼重复建同一个房号这是建表时就要兜住的底线。3.2 学生表与登录账户房间床位唯一约束、密码不做明文student 表比很多课程设计多一个 bed_no 字段这很重要。宿舍不只是「住在哪个房间」还精确到几号床否则查寝时「床位是不是空」无法表达CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL COMMENT 1男 2女, phone VARCHAR(20) DEFAULT , room_id INT NOT NULL COMMENT 关联 dorm_room.id, bed_no TINYINT UNSIGNED NOT NULL COMMENT 床位号如 2, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在宿 0已退宿, check_in_date DATE NOT NULL, check_out_date DATE DEFAULT NULL, UNIQUE KEY uk_stu_no (stu_no), UNIQUE KEY uk_room_bed (room_id, bed_no), KEY idx_room (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_room_bed 是防重关键同一个房间的同一个床位号只能出现一次由数据库兜底比在 Java 里先查再插可靠得多。退宿时不要 DELETE 这条记录而是把 status 置为 0、写上 check_out_date历史住客记录留着后面查「某床位住过谁」才有数据这也是第 5 章避坑里会展开的一条。sys_user 表独立出来不要把管理员账号塞进 studentCREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL COMMENT SHA-256加盐后的密文, role TINYINT NOT NULL DEFAULT 1 COMMENT 1管理员 2宿管 3学生, student_id INT DEFAULT NULL COMMENT 学生角色时关联 student.id, status TINYINT DEFAULT 1, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;password 字段长度按 64 留是给散列值准备的。再怎么赶时间也别把明文密码直接写进表课程设计被抽查到这一点很掉价。角色字段让一张表撑起管理员、宿管、学生三种登录入口后续 Filter 和页面跳转都可以按 role 分流。3.3 水电与报修表decimal 金额、联合唯一账单月水电账单按月生成天然是「一个学生一个月一条记录」的形态联合唯一约束正好卡死重复CREATE TABLE water_elec_bill ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, bill_month CHAR(6) NOT NULL COMMENT 如 202506, water_usage DECIMAL(8,2) DEFAULT 0 COMMENT 用水量吨, elec_usage DECIMAL(8,2) DEFAULT 0 COMMENT 用电量度, amount DECIMAL(7,2) NOT NULL COMMENT 应缴金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未缴 1已缴, pay_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_month (student_id, bill_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;amount 用 DECIMAL(7,2)不用 FLOAT 或 DOUBLE。水电费是金额浮点运算的误差在累计对账时会暴露成几分钱的差异DECIMAL 是精确十进制值得多敲几个字符。bill_month 用 CHAR(6) 而不是 DATE 或 INT因为它是纯展示用的月份标识字符串比较和排序都不会出错。报修工单考察状态流转能力是这套系统里最适合准备面试的环节CREATE TABLE repair_order ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT 报修学生, room_id INT NOT NULL, item_type VARCHAR(20) NOT NULL COMMENT 水龙头/灯/空调, description VARCHAR(500) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待受理 1处理中 2已完成 3已评价, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL, handler VARCHAR(30) DEFAULT COMMENT 维修工姓名, KEY idx_student (student_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status 用数字表示状态机0 到 3 的单向流转不允许跳变。finish_time 不在创建时生成等工单走到「已完成」才写入这个逻辑放 Service 层处理。idx_status 是为了支撑「全部待受理工单」这类列表查询加不加索引在几百条数据时感觉不出来但这是建表习惯。3.4 把 V1.0 建表脚本跑起来DDL 与初始化数据建表顺序有讲究先建 sys_user 和 dorm_room再建 student最后建 water_elec_bill 和 repair_order因为 student 引用了前两张表的 id后两张表又引用 student 的 id。把上面五段 SQL 存成 db.sql用命令行执行mysql -u root -p dorm db.sql mysql -u root -p -e SHOW TABLES FROM dorm;第一次跑通后插几条基础数据用于联调INSERT INTO sys_user (username, password, role) VALUES (admin, 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918, 1); INSERT INTO dorm_room (building_no, room_no, capacity, occupied, status) VALUES (3栋, 301, 4, 2, 1), (3栋, 302, 4, 0, 1), (5栋, 501, 6, 6, 1);admin 那一长串是admin做 SHA-256 后的密文联调时可以先用它登录之后再去改密码。房间初始化数据里 5 栋 501 的 occupied 等于 capacity正好用来测「满房后办理入住会被拒绝」这条业务规则。到了这一步数据库部分就绪可以开始写 Java 代码了。4. 从登录到报修闭环三层架构代码按这个顺序写表结构定了代码就按 DBUtil → entity → DAO → Service → Servlet → JSP → Filter 的顺序写先打通登录再扩展业务。每次只加一个功能闭环一个 DAO 方法、一个 Service 方法、一个 Servlet 入口、一张 JSP 页面跑通再写下一个。下面用四个关键节点把整条链串起来。4.1 登录与会话状态Session 存 userIdFilter 守门登录接口是所有页面的入口。用 Session 存登录对象不要只存一个 username 字符串角色信息后面每个页面都要用WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); 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); SysUser user userService.login(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /jsp/admin/home.jsp); } }登录失败用 forward 而不是 redirect这样才能把 msg 带到 JSP 上提示「用户名或密码错误」登录成功用 redirect避免刷新页面时重复提交表单。session 里放的是完整 SysUser 对象后续 JSP 通过${loginUser.role}直接判断身份。有了 Session 登录态就要用 Filter 挡住未登录的请求WebFilter(/*) public class LoginFilter 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(); if (uri.endsWith(/login) || uri.contains(/jsp/login.jsp) || uri.contains(/static/) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /jsp/login.jsp); return; } chain.doFilter(req, resp); } }白名单里放行登录接口、登录页、静态资源三类其他路径一律检查 session。注意 Filter 只做「有没有登录」的判断不做「能不能访问某个功能」的权限判断后者放 Service 层Filter 太多业务会变成失控的拦截器迷宫。4.2 DAO 层写法PreparedStatement 反注入与 ResultSet 映射DAO 层最容易犯的错是字符串拼接 SQL。一条带 OR 11的学号搜索可以直接把整张 student 表拖出来。全部用 PreparedStatement 占位符是这条底线最简单也最规范的实现public Student findByStuNo(String stuNo) { String sql SELECT id, stu_no, name, room_id, bed_no, status FROM student WHERE stu_no ? AND status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setName(rs.getString(name)); s.setRoomId(rs.getInt(room_id)); s.setBedNo(rs.getInt(bed_no)); s.setStatus(rs.getInt(status)); return s; } } } catch (SQLException e) { throw new RuntimeException(查询学生失败, e); } return null; }try-with-resources 保证 Connection、PreparedStatement、ResultSet 三个资源一定被关闭这是 Java 7 之后的标配别再用 finally 里手动 close 的写法。ResultSet 的列名要和 SQL 里的别名一致我习惯在 SELECT 里写清楚要哪些列不用SELECT *这样实体映射的每个字段都是显式的查问题不至于对着表结构猜。提示所有 DAO 方法都返回实体或List实体把 ResultSet 转成对象这件事只发生在 DAO 内部Service 和 Servlet 永远不碰 JDBC 的 API。4.3 入住登记事务边界与床位占用的原子更新入住是这套系统里唯一必须开事务的功能插入学生记录、更新房间 occupied一个失败另一个必须回滚。先给 DBUtil 补上事务三件套用 ThreadLocal 把 Connection 绑到当前线程private static ThreadLocalConnection tl new ThreadLocal(); public static Connection getConnection() throws SQLException { Connection conn tl.get(); if (conn null || conn.isClosed()) { conn ds.getConnection(); tl.set(conn); } return conn; } public static void beginTransaction() throws SQLException { getConnection().setAutoCommit(false); } public static void commit() { Connection conn tl.get(); if (conn ! null) { try { conn.commit(); conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} tl.remove(); } } public static void rollback() { Connection conn tl.get(); if (conn ! null) { try { conn.rollback(); conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} tl.remove(); } }ThreadLocal 的意义在于同一个请求线程里不管 DAO 被调用几次拿到的都是同一个 Connection这样事务才能覆盖跨多个 DAO 的操作。如果你把 Connection 当参数一层层往下传事务也能跑通但每个 Service 方法都会多一个参数代码会越写越丑。入住登记的 Service 方法是这套代码里最能体现「数据一致性」的一段public void checkIn(Student stu, int roomId) { try { DBUtil.beginTransaction(); String lockSql UPDATE dorm_room SET occupied occupied 1 WHERE id ? AND occupied capacity; try (PreparedStatement ps DBUtil.getConnection().prepareStatement(lockSql)) { ps.setInt(1, roomId); if (ps.executeUpdate() 0) { throw new BizException(该房间已满); } } String insertSql INSERT INTO student (stu_no, name, room_id, bed_no, status, check_in_date) VALUES (?, ?, ?, ?, 1, CURDATE()); try (PreparedStatement ps DBUtil.getConnection().prepareStatement(insertSql)) { ps.setString(1, stu.getStuNo()); ps.setString(2, stu.getName()); ps.setInt(3, roomId); ps.setInt(4, stu.getBedNo()); ps.executeUpdate(); } DBUtil.commit(); } catch (Exception e) { DBUtil.rollback(); throw new RuntimeException(入住失败: e.getMessage(), e); } }两个点值得记。第一UPDATE ... WHERE occupied capacity把「检查余量」和「占用床位」合并成一条原子 SQL两个管理员同时给同一房间办入住只有一条能更新成功这是防超卖最轻量的做法。第二事务里第 2 次调用 getConnection 拿到的还是同一个连接因为 ThreadLocal 还绑着它所以 try-with-resources 关掉的是 PreparedStatement不是 Connection。BizException 是自定义业务异常继承 RuntimeException你也可以直接用 IllegalStateException效果一样。4.4 报修工单状态流转把状态机收敛进 Service报修的状态机是 0→1→2→3 单向流转每次操作都要带「当前状态」作为条件防止两个操作员同时处理同一张工单出现覆盖public void transitOrder(int orderId, int fromStatus, int toStatus) { String sql UPDATE repair_order SET status ? WHERE id ? AND status ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, toStatus); ps.setInt(2, orderId); ps.setInt(3, fromStatus); if (ps.executeUpdate() 0) { throw new BizException(工单状态已被他人修改请刷新后重试); } if (toStatus 2) { try (PreparedStatement ps2 conn.prepareStatement( UPDATE repair_order SET finish_time NOW() WHERE id ?)) { ps2.setInt(1, orderId); ps2.executeUpdate(); } } } catch (SQLException e) { throw new RuntimeException(e); } }WHERE status ?是关键更新时带上当前状态改了 0 行说明状态已被别人推进直接报错不给「后写覆盖先写」留机会。这个模式叫乐观锁的轻量版比在表上加版本号省事对课程设计完全够用。finish_time 的写入直接跟状态变更放在一起避免出现状态是已完成、时间却是空的脏数据。状态机的合法迁移可以单独抽一个枚举维护但别把规则撒到 JSP 的 if 判断里。5. 从乱码到超卖宿舍管理系统避坑要记的五个翻车现场这一章是血泪经验。下面五个坑我见过不止一次出现在答辩现场每一条都按「现象 → 原因 → 解决」拆开照着排查比撞运气快得多。5.1 中文乱码三个入口JSP 页面、请求编码、JDBC URL现象插入的学生姓名进了 MySQL 变成??搜索「张伟」查不出来登录时中文用户名直接报错。原因乱码不是单一原因而是三个地方只要有一个没配 UTF-8 就出问题。JSP 页面本身的 pageEncoding 不是 UTF-8POST 请求体没有被 Servlet 按 UTF-8 解码JDBC URL 没带 characterEncoding 参数驱动按系统默认字符集转码。三处是串联关系查起来像玄学实际上是一条链路断了。解决三处一起处理。JSP 文件头写% page pageEncodingUTF-8 %在 Filter 里统一调用request.setCharacterEncoding(UTF-8)不要每个 Servlet 各写一遍JDBC URL 带上useUnicodetruecharacterEncodingutf8同时数据库表建表用utf8mb4。三个入口全部就位后中文乱码基本绝迹。5.2 床位超卖并发入住时 occupied 是怎么多出来的现象演示时找两个浏览器同时给 301 房间第 4 个床位办入住两个请求都提示成功房间里实际住了 5 个人occupied 变成了 5。原因Service 里写成了「先 SELECT 查 occupied 是否小于 capacity再 UPDATE 加一」。两个请求同时通过了 SELECT 判断然后各自执行 UPDATE最后都成功。这就是经典的 check-then-act 竞态Java 代码里看着顺序没问题放到并发场景就翻车。解决把检查和更新合并成一条 SQL第 4.3 节那段UPDATE dorm_room SET occupied occupied 1 WHERE id ? AND occupied capacity就是为这个准备的。执行后返回 0 行说明已满直接抛出「该房间已满」。注意这条 SQL 要跟 INSERT 学生在同一个事务里否则会出现在请求 B 更新房间成功后事务回滚房间占用数却多了一的情况。5.3 退宿与外键为什么删学生会把整个楼删乱现象把「退宿」实现成 DELETE 学生记录先建了ON DELETE CASCADE的学生外键删学生时把 dorm_room 里房间的关联数据一并删掉整栋楼的房间全乱了。原因物理外键加级联删除是把双刃剑。业务上「退宿」是一个状态变化不是数据销毁。学生毕业后学号、入住时间、账单记录都要留着直接删除等于销毁审计数据而水费和报修表还挂在 student_id 上外键约束会阻止删除或者级联删掉一堆不该删的数据。解决不建物理外键或者建了也不开级联删除。学生退宿改成UPDATE student SET status 0, check_out_date CURDATE() WHERE id ?同时单独执行一条UPDATE dorm_room SET occupied occupied - 1 WHERE id ?两句包在同一个事务里。退宿和入住变成对称的两个操作一个加占用一个减占用代码可读性和稳定性都更好。5.4 时间字段的时区与格式Date 还是 LocalDateTime现象数据库里存的是2025-06-01 12:30:00页面上显示成Thu Jun 01 12:30:00 CST 2025或者 JSON 里变成一串1748766600000数字。原因实体里用了java.util.DateJSP 直接toString()或者 JDBC 驱动用getTimestamp时没带时区参数。Date 的 toString 输出的是英文格式前端拿到时间戳数字更没法看。解决新代码统一用java.time.LocalDateTime接数据库的 DATETIME 字段JDBC 8 驱动原生支持。页面和接口输出统一用DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)格式化不要在 JSP 里各写各的fmt:formatDate和字符串截取。MySQL 连接串里的serverTimezoneAsia/Shanghai必须保留否则驱动和服务器时区不一致时LocalDateTime 会平白无故差 8 个小时。5.5 SQL 注入与 XSS课程设计也要守住底线现象学生搜索框输入 OR 11返回了全部学生把scriptalert(1)/script作为姓名存进库打开页面弹窗。原因拼接 SQL 执行了用户输入里的引号和关键字JSP 页面直接用${student.name}输出未经转义的 HTML脚本被浏览器执行。这不是大系统才有的问题课程设计里一样会被演示到而且一旦发生当场就很难收场。解决全部 SQL 走 PreparedStatement 占位符这是底线页面输出用户可控字段用c:out value${student.name}/JSTL 的 out 标签默认会转义尖括号和引号比${}裸输出安全得多。密码别存明文SHA-256 加盐后的散列值存进 sys_user 的 password 字段即使数据库被拖走也不能直接拿到原文。6. 把课程设计讲成面试作品三个加分点和一个并发验证办法项目跑通只是第一步答辩和面试里讲出「我在哪里做了权衡」才是加分项。这章把前面代码里最值得讲的点提炼一下再给一个五分钟能做完的并发验证。6.1 把 ThreadLocal 事务和条件更新整理成面试素材面试官问「Java 怎么保证数据一致性」时你可以直接拿这个项目回答入住功能用了 ThreadLocal 把 Connection 绑到请求线程多个 DAO 共享同一事务床位占用用了条件更新WHERE occupied capacity把「查余量更新」合并成原子操作。这两句话能展开讲十分钟因为背后涉及事务的 ACID、乐观锁思想、线程本地变量的隔离原理全部是你自己写的代码不是背八股文。接着补一句「单机单库场景下这样够用不需要引入分布式事务」反而显得你有边界感。6.2 一个并发入住验证加一份验收清单验证床位不超卖不需要装压测工具开两个 mysql 终端同时执行下面这条 SQLUPDATE dorm_room SET occupied occupied 1 WHERE id 1 AND occupied capacity;两个终端几乎同时回车第二个会先阻塞等待等第一个提交后再执行此时条件已经不满足返回0 rows affected。这就在数据库层面验证了条件更新的原子性。再回到应用里开两个浏览器同时点入住只有一个能成功另一个提示「该房间已满」业务闭环就成立了。验收清单按这个顺序过一遍登录未带 session 会被 Filter 拦回登录页入住满员房间被拒绝且 occupied 不变退宿后房间占用减一、学生记录仍可查同一个月同一学生不能生成第二张水电账单中文姓名和地址在页面、数据库两边都不乱码。五条全部通过这套系统就具备了拿去演示和答辩的底气。我做课程设计时把退宿写成了 DELETE第一次演示直接删掉了房间的关联数据当场翻车。后来改成逻辑删除加条件更新这份代码反而成了我简历上唯一能讲满十分钟的项目。希望帮到你。本文还有配套的精品资源点击获取
返回列表