Java酒店预订系统源码实战:从环境配置到并发控制

发布时间:2026/9/24 18:23:46
Java酒店预订系统源码实战:从环境配置到并发控制 简介Java酒店预订系统源码是一套面向初中级开发者的Java Web实战项目覆盖在线预订平台从数据库设计到页面交互的主要开发环节压缩包共41个文件以34个java源文件为核心辅以4个properties配置、1个xml配置和2个md说明文档整体仅83KB结构紧凑适合按模块逐一阅读。项目展示了酒店信息、房间类型、用户、订单等表的建表与关联设计并串联起Servlet/JSP请求处理、JDBC数据访问、Spring框架整合、Hibernate与MyBatis持久化映射、前端交互和登录权限控制等关键实现能够帮助读者理解MVC分层以及SSH、SSM等框架在真实项目中的协作方式。源码中还可看到异常处理、事务控制、单元测试和常见部署配置的落地写法适合在现有代码基础上做二次扩展压缩包内另附一个RocketMQ小项目可进一步学习消息中间件的接入场景。目前已有74人学习下载代码量不大但技术面较广尤其适合课设、毕设或Java Web入门后的综合练习项目仅供学习研究使用不建议直接用于商业场景。1. 一个 zip 装的 Java 酒店预订系统背后是一整条 Java Web 学习线“java酒店预订系统.zip”这类压缩包在课程设计、毕业设计和 Java 求职作品集里出现频率极高。它典型的技术构成是 SSMSpring Spring MVC MyBatis或 Spring Boot JSP MySQL Tomcat业务上覆盖了用户注册登录、房型浏览、在线预订、订单管理和后台维护几乎把 Java Web 开发的主干技术全串了一遍。对正在走 Java 学习路线、准备课程设计答辩或想给简历加一个完整项目的人来说这套东西的价值不在于“酒店”本身而在于它是一份能跑通的、前后端闭环的参考实现。这篇笔记就把拿到 zip 之后从解压、配环境、建库到改代码、压测、写文档的完整路径拆开讲重点说清楚每一步为什么这么做以及最容易翻车的位置在哪。2. 从 zip 到跑通技术栈识别与本地环境准备的顺序问题2.1 先看 pom.xml 还是先配环境判断这是 SSM 还是 Spring Boot 的项目拿到 zip 后第一件事不是急着解压导入 IDE而是先看压缩包里的文件结构判断这个项目的技术栈版本。常见的课程设计源码有两种形态一种是传统的 SSM 项目目录里会有src/main/java、src/main/resources、src/main/webapp/WEB-INF这种分层结构配置文件是applicationContext.xml、spring-mvc.xml、mybatis-config.xml另一种是 Spring Boot 项目根目录下有pom.xml或build.gradle主启动类是带SpringBootApplication注解的 Java 文件配置文件是application.properties或application.yml。# 解压后先看目录结构不要急着打开 IDE unzip hotel-reservation-system.zip -d hotel-project cd hotel-project ls -la find . -maxdepth 2 -type f -name *.xml -o -name *.yml -o -name *.properties | head -20这个命令的作用是快速定位配置文件。有webapp/WEB-INF/web.xml的基本是传统 SSM 或 SSH 项目只有application.yml且没有webapp目录的大概率是 Spring Boot。这一步直接决定后面装哪个版本的 JDK、要不要单独配 Tomcat。很多人在这一步就翻车了拿到一个 Spring Boot 项目却按照 SSM 的老教程去装 Tomcat 并配置server.xml结果项目启动时端口冲突或上下文路径对不上白白消耗一晚上。判断完类型后再打开pom.xml看依赖版本。重点看三处spring-boot-starter-parent的版本号、mysql-connector-java的版本、mybatis-spring-boot-starter或mybatis的版本。这三个版本直接决定你本地该装哪个版本的 JDK 和 MySQL。比如 Spring Boot 2.x 配 JDK 8 是最稳的Spring Boot 3.x 强制要求 JDK 17 以上同时对应的javax.servlet要改成jakarta.servlet这一点在代码里会体现为import javax.servlet.*直接报红。2.2 JDK、Maven、Tomcat 与 MySQL 的版本匹配哪个组合最不容易翻车版本匹配是这类源码跑通的第一道坎。我的建议是如果项目没有明确标注版本优先使用 JDK 8 Maven 3.6.3 Tomcat 8.5 MySQL 5.7 这套组合。原因很实际课程设计类源码大多是几年前写的pom.xml里的依赖版本普遍兼容 JDK 8JDK 8 是 Java 学习路线上最经典的版本市面上绝大部分 Java 基础面试题和八股文也基于这个版本MySQL 5.7 和mysql-connector-java 5.1.x的配合最顺滑不容易出现时区报错。# 在项目根目录执行确认 Maven 能正常解析依赖 mvn -v mvn clean compile -DskipTests如果mvn -v显示的是 JDK 17 而项目是 Spring Boot 2.x会出现两类问题一是编译直接失败报源发行版 8 不存在或无效的目标发行版二是编译能过但启动时报IllegalArgumentException这类反射相关错误。解决方式是修改 IDE 里的 Project Structure 和 Maven 的JAVA_HOME指向 JDK 8。如果系统里只装了 JDK 17 又不想重装可以在pom.xml里临时把java.version改成 17但要注意有些老版本的 Lombok 在 JDK 17 下会失效getter/setter全部报错这时要么升级 Lombok 版本要么卸载 Lombok 改用 IDE 生成。MySQL 版本上有一个高频坑如果本地装的是 MySQL 8.x而项目里的driver-class-name是com.mysql.jdbc.Driver启动时连接池会报Loading class com.mysql.jdbc.Driver. This is deprecated。这不是致命错误但后续可能因为时区问题报The server time zone value的异常。建议把驱动类改成com.mysql.cj.jdbc.Driver并在 JDBC URL 后面加上serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。这几个参数缺一不可allowPublicKeyRetrievaltrue在 MySQL 8 的 caching_sha2_password 认证方式下必须显式开启否则报Public Key Retrieval is not allowed。2.3 导入 IDEA 的两种方式和导入后第一件要做的事导入方式分两种Maven 项目和普通项目。在 IDEA 里选择File - Open找到项目根目录下的pom.xmlIDEA 会自动识别为 Maven 项目并开始下载依赖。如果是传统 SSM 项目且没有pom.xml只有.classpath和.project文件选择 Open 整个目录后右键项目根目录选择Add Framework Support - Maven让 IDEA 生成一份新的pom.xml再把lib目录下的 jar 手动加入 Module 依赖。!-- 手动生成 pom.xml 时这段依赖配置是课程设计 SSM 项目的最小集 -- dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.2.9.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependencies这段配置里的版本号是经过验证的稳定组合。Spring 5.2.x 与 JDK 8 完全兼容MyBatis 3.5.6 支持Mapper注解和 XML 映射两种方式MySQL 驱动 5.1.49 兼容 MySQL 5.7 和 8.x。导入后第一件要做的事不是运行而是执行mvn clean compile先把编译错误清零。这一步能过滤掉 80% 的环境问题缺依赖、JDK 版本不对、Lombok 失效、编码错误等都会在编译阶段暴露。3. 数据模型与核心表设计酒店预订系统到底在管哪些数据3.1 房间、房型、订单、用户四张主表的关系与字段设计酒店预订系统的数据模型是典型的“用户-商品-订单”结构只不过这里的商品是房间。四张核心表分别是用户表、房型表、房间表、订单表。用户表管账号和身份房型表管“标准间、大床房、套房”这类分类信息房间表管具体的物理房间比如 801、802订单表记录用户和房间之间的预订行为。-- 用户表只保留预订业务必需的字段 CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房型表房价、床型、面积、可住人数 CREATE TABLE t_room_type ( id INT NOT NULL AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, bed_type VARCHAR(20) DEFAULT NULL, area INT DEFAULT NULL COMMENT 面积平方米, max_people INT DEFAULT 2, description VARCHAR(500) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意用户表的password字段长度必须大于 32因为 MD5 加密后是 32 位十六进制字符串如果字段长度设成 20 会导致插入时数据截断前端注册接口直接报错。房型表里price用DECIMAL(10,2)而不是FLOAT这是为了避免浮点数精度问题导致结算金额对不上账。房间表和订单表是业务核心。房间表需要有一个room_type_id外键指向房型表还要有status字段标记当前状态。订单表则需要记录用户、房间、入住日期、退房日期、订单金额、下单时间和订单状态。-- 房间表每个房间绑定一个房型 CREATE TABLE t_room ( id INT NOT NULL AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL COMMENT 房间号如 801, room_type_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1已入住 2维修, floor INT DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_room_number (room_number), KEY idx_room_type (room_type_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表一次预订记录 CREATE TABLE t_order ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已确认 2已入住 3已退房 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_room_id (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 DDL 里的关键设计是order_no使用独立业务订单号而非自增主键。自增主键 ID 在分库分表或系统间对账时无法保证全局唯一而且会暴露业务量。常见做法是用时间戳 随机数或数据库序列生成比如yyyyMMddHHmmss 4位随机数。另外订单表不要直接冗余房间号而是存room_id这样需求变更时要改房间号或调整楼层只需要更新房间表一行订单表不需要动。这个设计在课程设计答辩时是加分项能体现对第三范式的理解。3.2 预订状态的流转从提交到入住到退房的状态机设计预订不是一条直线而是一个状态流转过程。最简单可靠的状态机是五态模型待支付 → 已确认 → 已入住 → 已退房待支付状态下用户主动取消则进入已取消。这个状态机的设计直接决定了业务代码的复杂度如果状态乱跳比如从已入住直接到已取消就会出现房间被占了但订单被取消的严重数据问题。-- 状态修改时带上当前状态条件这是防并发状态覆盖的关键 UPDATE t_order SET status 1 WHERE id #{orderId} AND status 0;这条 SQL 是乐观锁思想的简化实现。更新时在 WHERE 条件里带上当前状态如果返回的影响行数为 0说明订单状态已经不是待支付要么已超时要么已被其他请求处理程序就应该放弃这次状态流转而不是盲目覆盖。这种写法在 Java 面试八股文里叫乐观锁在并发场景下比先 SELECT 再 UPDATE 的方式安全得多。状态机里最容易漏掉的是“入住前换房”和“退房时房间状态联动”两个场景。换房意味着订单的room_id要改同时原房间要恢复空闲、新房间要标记占用这必须是两个 SQL 放在同一个事务里执行。退房时订单状态改成已退房的同时房间表的status要改回 0。很多源码在这里只更新了订单表忘记更新房间表结果客房管理页面里房间永远是“已入住”状态前台只能手动改数据库这是最典型的业务逻辑漏洞。3.3 用 SQL 直接跑一遍核心查询验证表设计是否合理表设计得合理不合理别靠肉眼判断直接跑两个业务查询验证。第一个是用户查看可预订的房间列表输入入住日期和退房日期找出在这段时间内没有被预订且不属于待支付状态的房间。-- 查询指定日期区间内可预订的房间 SELECT r.id, r.room_number, rt.type_name, rt.price FROM t_room r JOIN t_room_type rt ON r.room_type_id rt.id WHERE r.status 0 AND r.id NOT IN ( SELECT o.room_id FROM t_order o WHERE o.status IN (1, 2) AND o.check_in_date 2025-08-20 AND o.check_out_date 2025-08-18 );这个查询的过滤逻辑是找“日期区间重叠”的订单。判断两个日期区间是否重叠标准条件是一个区间的开始日小于另一个区间的结束日且结束日大于另一个区间的开始日。这里check_in_date 2025-08-20 AND check_out_date 2025-08-18的含义是已有订单的入住日期早于我要入住的日期且已有订单的退房日期晚于我要入住的日期。注意子查询里status IN (1, 2)把待支付和已取消的订单排除在外待支付订单虽然还没付钱但已经占用了房间的锁定期这一点要按业务需求决定是否纳入。第二个验证查询是统计某一天的入住率或营收这能检验字段类型和索引设计是否合理。跑一遍发现check_in_date和check_out_date上没建索引时数据量过千之后查询会明显卡顿需要补上联合索引。通过这种验证方式能在写业务代码之前就把表结构的坑排除掉比写完接口再发现查询慢要省事得多。4. 核心业务代码走读一次完整预订单操作背后的技术点4.1 从 Controller 到 Mapper 的调用链动态代理在这里做了什么一次预订操作从前端页面发起到数据库落一条记录要经过 Controller、Service、Mapper 三层。以传统 SSM 项目为例前端表单提交到 Spring MVC 的控制器控制器接收参数后调用 Service 层接口Service 层实现类里通过 MyBatis 的 Mapper 接口完成数据库操作。// 预订请求的 Controller 层只做参数接收和响应封装 Controller RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; RequestMapping(value /book, method RequestMethod.POST) ResponseBody public Result book(RequestParam Integer roomId, RequestParam String checkInDate, RequestParam String checkOutDate, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } try { Order order orderService.createOrder(user.getId(), roomId, checkInDate, checkOutDate); return Result.success(order); } catch (BusinessException e) { return Result.error(e.getMessage()); } } }这段代码里的核心是Autowired把OrderService接口注入进来实际注入的是 MyBatis 或 Spring 生成的代理对象。这里就是 Java 面试八股文里常考的动态代理知识点Spring 容器启动时扫描到OrderService接口通过 JDK 动态代理生成一个代理类代理类把方法调用转发给真正的实现类同时在转发前后可以织入事务、日志等增强逻辑。这也是为什么 Service 层通常要定义接口而不是直接用实现类面向接口编程不是教条而是 Spring AOP 代理机制的实际要求。在 Service 实现类里核心逻辑是验证参数、计算价格、生成本地事务。价格必须由后端根据房型的单价和入住天数计算不能信任前端传过来的金额否则用户手动改请求参数就能以一分钱的价格下单。4.2 预订接口的幂等与并发控制怎么处理同一间房的重复预订并发问题是酒店预订系统最值得讲的部分。两个用户在同一秒点击“预订”同一间房如果代码只是简单地先查有没有冲突再插入订单就可能出现两个订单都创建成功的情况。原因是两次查询都看到房间空闲然后都执行了插入这就是经典的“检查后再插入”竞态条件。// Service 层创建订单的核心方法注意 Transactional 的位置 Transactional(rollbackFor Exception.class) public Order createOrder(Integer userId, Integer roomId, String checkInDate, String checkOutDate) { // 1. 参数校验和日期格式转换 // 2. 计算总价 // 3. 插入订单订单状态为待支付 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setRoomId(roomId); order.setCheckInDate(DateUtil.parse(checkInDate)); order.setCheckOutDate(DateUtil.parse(checkOutDate)); order.setTotalPrice(price); order.setStatus(0); orderMapper.insert(order); return order; }这个实现存在并发隐患。createOrder标注了Transactional这个方法内所有数据库操作会包在一个事务里要么全部提交要么全部回滚解决了部分成功的问题但没有解决两个事务同时插入的问题。因为默认的数据库隔离级别是REPEATABLE READ两个事务都查不到对方的未提交数据各自都认为自己拿到了“空闲”的房间。常用做法是在房间表上加一个“锁定字段”或利用唯一索引冲突来兜底。课程设计源码里做不到引入 Redis 分布式锁最简单可靠的办法是在t_order表加一个唯一约束(room_id, check_in_date, check_out_date)的组合唯一索引。这样即使并发插入数据库也会拒绝第二个请求抛出DuplicateKeyException程序捕获后返回“该房间已被预订”即可。这是一个零成本且面试能自圆其说的方案。4.3 JSP 页面与后端的数据交互request 作用域与 Ajax 的取舍传统 SSM 酒店预订系统的前端是 JSP 页面和后端交互有两种方式一种是表单同步提交后端return order-list返回视图名称由视图解析器渲染 JSP另一种是 Ajax 异步请求后端返回 JSON 数据前端用 JavaScript 渲染结果。两种方式各有适用场景同一个项目里混合使用很常见。// 返回视图适合页面跳转类操作 RequestMapping(/myOrders) public String myOrders(HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); ListOrderVO orderList orderService.getOrdersByUserId(user.getId()); model.addAttribute(orderList, orderList); return order-list; }这段代码走的是同步请求模式方法接收 HttpSession 获取登录用户调用 Service 层查询订单列表把结果放进 Model 里最后返回视图名order-list。视图解析器会根据前缀后缀配置找到WEB-INF/views/order-list.jsp渲染时通过 EL 表达式${orderList}和 JSTL 标签库c:forEach循环输出表格行。这种方式的好处是页面刷新即数据最新代码简单坏处是每一次操作都要整页刷新用户体验一般。对于“提交预订”这种高频操作我一般用 Ajax。前端用 jQuery 的$.ajax发送 POST 请求到/order/book后端返回 JSON 结构化数据前端根据返回的code字段决定弹提示还是跳转。这里有一个容易踩的坑ResponseBody返回的 JSON 中日期字段默认序列化格式是时间戳或yyyy-MM-dd两者都可能不符合前端预期。建议在实体类的日期字段上显式指定格式JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date createTime;JsonFormat注解是 Jackson 的Spring Boot 或配置了 Jackson 的 SSM 项目都能识别。timezone GMT8这一项特别重要不写的话服务器时区是 UTC前端显示的createTime会比实际时间少 8 小时这就是“时间对不上”的玄学问题的真正来源。5. 避坑与排查把这套系统从报错堆里捞出来的 15 条实战记录这类课程设计源码有一个共同特点作者在自己电脑上能跑换一台电脑就一堆问题。下面是按报错现象分类整理的 15 条高频踩坑记录每条按“现象 → 原因 → 解决”展开前 5 条对应环境类第 6 到第 10 条对应数据库类最后 5 条对应编码和部署类。5.1 环境与启动类问题现象一启动 Tomcat 时直接报Error during artifact deployment控制台没有具体堆栈。原因通常是依赖包没有全部部署到 WEB-INF/lib 目录下。IDEA 里部署的是 Artifact而不是 Maven 依赖本身。解决方式是检查Project Structure - Artifacts在 Output Layout 里确认Available Elements中项目的依赖已经右键Put into WEB-INF/lib。现象二java.lang.NoClassDefFoundError: javax/servlet/ServletOutputStream。原因是本地 Tomcat 版本和项目编译用的 Servlet API 版本不一致比如项目依赖 Servlet 3.1 而本地装的 Tomcat 是 10.xTomcat 10 把javax包改名为jakarta导致类找不到。解决方式是换 Tomcat 8.5 或 9.x或者在pom.xml里显式引入javax.servlet-api且 scope 设为provided让 Tomcat 运行时用自己的类。现象三启动时端口被占用报Port 8080 was already in use。原因是有其他进程占用了 8080 端口。解决方式是打开命令行执行netstat -ano | findstr 8080查到 PID然后在任务管理器里结束该进程。如果是自己电脑上的其他 Java 程序占用可以改 Tomcat 的server.xml里的端口号但要注意连带修改8009等关联端口。现象四IDEA 里执行mvn clean compile报Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin。原因大多数是 Maven 的 JDK 版本和项目要求的版本不一致。检查File - Settings - Build Tools - Maven - Runner - JRE确保指向的是 JDK 8 或项目要求版本。现象五Lombok 的Data注解生成了 getter/setter但 IDE 里调用时报找不到符号。原因是 IDE 没有启用注解处理器。解决方式是安装 Lombok 插件并在Settings - Build - Compiler - Annotation Processors勾选Enable annotation processingMaven 编译正常是因为 Maven 编译时自动处理了注解。5.2 数据库与 SQL 类问题现象六启动时连接数据库报Access denied for user rootlocalhost。原因是数据库账号密码和配置文件里的不一致。解决方式是打开jdbc.properties或application.yml把username和password改成自己本地的账号密码。这个问题的变体是 MySQL 8 使用caching_sha2_password认证插件而项目里的驱动太老需要升级驱动或创建使用mysql_native_password的专用账号。现象七执行 SQL 时中文乱码插入的中文变成问号。原因有三个层面数据库连接 URL 没有指定characterEncodingutf8数据库表不是 utf8mb4 编码JSP 页面本身的字符集没有设置成 UTF-8。解决方式依次检查这三处。数据库连接 URL 上加上useUnicodetruecharacterEncodingUTF-8并且注意在 XML 配置里符号要转义成amp;这是最常见的转义陷阱。现象八SQL 语句报Unknown column xxx in field list。原因是实体类的属性名和数据库字段名不一致。MyBatis 默认开启驼峰映射但前提是数据库字段用下划线命名且mapUnderscoreToCamelCase配置为 true。如果没开这个配置需要在 XML 的resultMap里显式映射或者在 SQL 里给字段起别名。现象九运行时报Too many connections。原因是连接池的maxActive设置过大而 MySQL 默认的最大连接数是 151。开发环境下连接池配置几十个同时活跃连接一般不会触发但如果项目里多处创建了连接池且没复用就会累计占用连接。解决方式是检查数据源配置确保是单例的并且把maxActive调小到 20 以内。现象十The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这个乱码式的报错是 MySQL 8 的时区问题原因在于 JDBC 连接串没有指定serverTimezone。解决方式是在 JDBC URL 末尾追加serverTimezoneAsia/Shanghai。这个问题在 Windows 中文系统上尤其常见因为系统默认时区名称是中文驱动解析不了。5.3 编码、路径与部署类问题现象十一页面能打开但 CSS/JS 文件全部 404。原因是 Spring MVC 的 DispatcherServlet 把静态资源请求也拦截了。解决方式是在 Spring MVC 配置中放行静态资源mvc:resources mapping/static/** location/static//或者在web.xml里给 CSS、JS、图片等后缀单独配置 servlet-mapping。Spring Boot 项目则默认把static目录映射到根路径一般不会遇到这个问题。现象十二表单提交后跳转的 URL 不带项目上下文路径报 404。原因是 JSP 中的链接写的是绝对路径比如/order/list但项目部署的上下文路径是/hotel实际路径是/hotel/order/list。解决方式是在 JSP 页面顶部用c:set varctx value${pageContext.request.contextPath}/定义上下文路径变量所有链接都写成${ctx}/order/list。现象十三Ajax 请求返回 406 Not Acceptable。原因是 Spring MVC 的ResponseBody返回 Javabean 时如果类里没有默认构造函数或者缺少 Jackson 依赖无法完成 JSON 序列化。解决方式是确认pom.xml里有jackson-databind依赖且被返回的类有空的构造器。现象十四页面正常但登录后刷新就失效session 数据丢失。原因是 Tomcat 的工作目录被清理或者 IDE 热部署导致 Session 序列化失败。开发环境下常见原因是没有把 session 超时时间设置好web.xml里session-configsession-timeout30/session-timeout/session-config配置的 30 分钟不写单位实际是 30 分钟低于这个就被视为过期。另一个原因是浏览器禁用 Cookie导致JSESSIONID无法维持。现象十五部署上线后页面报 500 且控制台提示No qualifying bean of type。在本地 IDE 里跑一切正常部署到服务器后报这个错通常是 Spring 配置文件没有被打包到 classpath 下。检查打包产物target/classes下是否有 Spring 的 XML 配置文件。有些构建配置会把src/main/resources漏掉需要在pom.xml的buildresources里显式声明。6. 让这套课程设计源码在简历里站得住升级与验证的六个关键动作面试官和答辩老师看到一个酒店预订系统最在意的问题逃不出这几个你怎么防止同一间房被重复预订你怎么保证订单和房间状态的数据一致性你的登录密码是明文存储的吗把这几个问题提前解决掉再配合一次压测数据这个项目的含金量能上一个台阶。第一个该改的是密码存储。原始源码里如果直接存明文需要写一个 MD5 加盐工具类注册时对密码加固定盐值做 MD5登录时同样加密后和数据库比对。public class MD5Util { private static final String SALT hotel2025#; public static String encrypt(String rawPassword) { String base SALT rawPassword; return DigestUtils.md5DigestAsHex(base.getBytes(StandardCharsets.UTF_8)); } }这个工具类里用了 Spring 自带的DigestUtils不引额外依赖。加盐的目的是防止彩虹表反向查值固定盐虽然不如随机盐安全但只要不是裸密码在课程设计层面就是明显加分项。第二个动作是给订单表加组合唯一索引并在 Service 层捕获DuplicateKeyException把它翻译成“房间已被预订”的友好提示。这一步可以在压测时看到效果两个并发请求同时进来一个成功一个失败数据不产生脏订单。第三个动作是给房间和订单的状态修改补上乐观锁条件即所有状态变更 SQL 都在 WHERE 里带上原状态值。这一步做完订单状态机的数据一致性就有了基本保证。第四个动作是用 JMeter 做一次并发预订压测。线程数设 20Ramp-Up 设 1 秒循环次数设 1直接请求预订接口观察失败率和响应时间。如果失败率是 0 但订单量翻了 20 倍说明并发控制没有生效回到第二和第三个动作检查问题。这个压测结果可以截图放进项目 README 或简历作品描述里比空口说“我做了一个酒店预订系统”有说服力得多。第五个动作是给代码加一份 README 和 SQL 初始化脚本。README 里写清楚 JDK 版本、MySQL 版本、导入步骤、默认账号密码SQL 脚本要包含建库、建表、插入测试数据三个部分。很多课程设计源码的数据库脚本是拆散的别人拿到手不知道怎么跑写一个完整的init.sql能大幅降低项目的复现成本。第六个动作是给关键的 Java 业务代码补注释重点是类头注释和方法注释。标注“创建订单时用乐观锁防止并发重复预订”“订单状态机流转条件”这类设计意图的注释在答辩时可以直接脱口而出设计思路不需要临时组织语言。做这套系统的过程里我自己的一个习惯是每解决一个报错就在 README 里追加一条问题记录后来这些记录成了我在 Java 后端面试里聊项目细节的素材库。坦白说一个酒店预订系统的功能在真实商业项目里不够看但把它当做一个学习载体去较真并发、事务、状态机这些点是能把“会写 CRUD”升级成“理解业务和数据一致性”的。希望这篇笔记能帮你在拿到那个 zip 之后少走几段弯路把时间花在真正有价值的问题上。本文还有配套的精品资源点击获取