简介这是一份基于Java的酒店预订系统实战项目源码面向正在学习Java Web开发的学生与初级开发者帮助读者理解从零搭建在线预订平台的完整流程既可辅助课程设计也可作为求职面试前的项目复盘样本。压缩包共41个文件以34个Java源文件为核心辅以4个properties配置文件、1个xml配置文件和2个md说明文档整体仅83KB轻巧便于本地运行调试。源码贯穿MVC设计模式、Servlet与JSP请求响应、JDBC数据库访问、Spring框架整合并涉及Hibernate/MyBatis的持久层映射思路同时包含前端页面布局、安全认证、异常处理等实际工程细节能直观展示分层架构如何落地。目前已有74人学习下载适合希望通过完整案例快速掌握Java Web主流技术栈、并理解配置与代码协作方式的开发者。1. 这份 java酒店预订系统.zip解决的是「从需求到跑通」的课设全流程你手里这份「java酒店预订系统.zip」很可能来自课程设计选题或某个资料分享站。它属于典型的 Java Web 管理系统前台做房型展示、在线预订后台做房型管理和订单维护。适合三拨人正在找 java 课程设计案例源码的学生想用真实业务练 Servlet/JSP 的初级开发以及准备把 CRUD 项目写进简历但缺业务逻辑的求职者。很多同学搜「民宿酒店预订系统源码下载」时拿到过这类包但打开后容易卡在跑不起来。先给一个反直觉的结论这个系统做成什么样不由页面决定而由「订单日期冲突是否查重、房间库存是否扣减、取消订单是否释放」这三件事决定。后面所有内容都围着这三件事转。2. 拆业务与表结构预订系统的核心不是页面是数据关系2.1 最小功能集前台三条路径、后台四张管理页一个合格的课设级酒店预订系统不用做成携程。按我给学生改代码的经验功能集卡在「能讲清楚业务闭环」就够前台用户注册登录后可以按日期和房型搜索可售房间选房后提交订单后台管理员登录后可以维护房型、维护房间、查询订单、处理订单状态。这四条路径对应到代码里就是一组 Servlet/Controller 加一组 JSP/Admin 页面。为什么要这样切而不是只做一个表因为「房型」和「房间」是两类不同粒度的概念。房型是定价和规格比如标准间、大床房房间是真实存在的「203号房」。同一个房型可以对应十间房。课设失败案例里常见问题是把这两个概念揉进一张表最后日期冲突永远查不准。先定角色。用户表里放一个role字段区分前台用户和管理员比建 user 和 admin 两张表实用。管理员的菜单在前端 JSP 里判断 role 显示不用做两套登录。权限这一块是很多课设的短板但修到「后台的 JSP 页面在入口处校验 session 里的 role」就够了不用上框架。2.2 表结构用户、房型、房间、订单的关系根据上面的功能集最少四张表user、room_type、room、orders。关系是room通过type_id关联room_typeorders通过room_id关联room通过user_id关联user。为什么订单不直接存room_type_id因为如果只存房型订单就无法知道顾客具体住的是哪间房。你可以用「同一房型有多间房存在任意一间满足就行」的思路但那样在取消订单后释放哪一间又会变成黑盒子。课设复杂度控制在「按具体房间下单」最合适。这里有一个容易被问倒的设计点订单表要不要冗余房型和价格常见做法是冗余total_price因为订单生成后价格不应该随房型调价变化。房间号和房价也可以冗余进订单表避免后续查询订单要 join 三张表。课设里 join 太多页面响应慢是其次写不对才是问题。字段选型上金额用DECIMAL(10,2)而不是 float/doublefloat 在累加和比较大小上的精度问题在金融属性场景里是必扣分项。日期用DATE时间用DATETIME别用 varchar 存日期否则后面按日期区间查重会踩「字符串比较和日期比较不一致」的坑。2.3 建表 SQL四张表直接跑注释里是选型理由下面这套 SQL 是常见的课设库表结构按上面四张表写出核心字段。注意用utf8mb4不要再用utf8因为 utf8 在 MySQL 里存不了部分生僻字和 emoji。-- 用户表字段越多越容易在注册页面出错先保住登录 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-前台用户 2-管理员, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码字段长度给 100 是因为很多课设直接用MD5(password)32 位十六进制但如果你用SHA-256或加盐长度至少要 64。这里给 100 是不卡你后面换加密方式。phone 允许空注册时可以不强制。-- 房型表一个房型可以对应多间房 CREATE TABLE room_type ( id INT NOT NULL AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, bed_count INT NOT NULL DEFAULT 2 COMMENT 床位数, area DECIMAL(10,2) DEFAULT NULL COMMENT 面积平米, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;price 放在房型不要放在房间。同一标准间各房间价格一致如果后续要按楼层加价再加一个 price 到 room 表覆盖它。bed_count 是给列表页筛选用的不要省。-- 房间表每行是一间具体的房 CREATE TABLE room ( id INT NOT NULL AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL, type_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-可售 1-维修/停用, PRIMARY KEY (id), UNIQUE KEY uk_room_no (room_no), KEY idx_type_id (type_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;room_no 唯一防止前台显示重复。status 字段的作用是让维修中的房间在搜索里不要出现而不是在预订时才排除。idx_type_id 是为了按房型搜索房间时走索引。-- 订单表核心中的核心后面所有并发测试都在这张表上做 CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, 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-已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_room_date (room_id, check_in_date, check_out_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么订单表不用自增id直接给用户看而要单独生成order_no三个原因第一自增 id 会被遍历抓数据暴露经营量第二业务订单号要包含日期或用户信息方便客服查第三如果后续把 orders 做分表自增 id 会冲突order_no 保持业务唯一。课设里生成方式可以直接用yyyyMMddHHmmss 用户id 随机数长度压到 32 以内。idx_room_date这个复合索引的字段顺序不是随意的。SQL 里查房间某个日期段是否有重叠订单room_id是等值条件check_in_date/check_out_date是范围条件所以要把room_id放最前否则索引利用率会打折扣。这个细节在数据量小的课设里感觉不出来但面试官问索引时可以直接拿出来讲。至此四张表建完。注意orders里没有contact_name和contact_phone因为这两个字段应该从 user 表带出来而不是每次下单重复输入。如果你想在项目里体现「字段冗余」可以在订单里冗余姓名手机号但那样要有一套同步策略课设不推荐。提示orders在 MySQL 8 里单独使用order是保留字所以要写成orders或加反引号。如果建表时报语法错误先检查是不是表名撞了保留字。3. 本地跑通JDK/Tomcat/MySQL 配置与启动顺序3.1 拿到 zip 先别导入 IDE按这个顺序读目录先说为什么。一份随课程设计下载下来的 zip 工程最常见的坏情况是路径不完整、缺少 jar 包或缺少 sql 文件。一上来就导入 IDEA报错能把你淹掉。我一般会先把压缩包解压然后用文件管理器过一遍根目录。看这几个东西README.txt/使用说明.doc作者把启动步骤写在里面优先读.sql文件或sql/目录建库脚本决定你能不能把数据库搭起来src/main/java或src/Java 源码webapp或WebRootJSP、js、css、WEB-INFlib/或WEB-INF/lib/第三方 jar 包。如果你打开发现没有 sql 文件只有数据库备份.bak或.frm就比较尴尬。常见做法是看代码里 jdbc 配置连的库名然后自己按实体类建表。别慌后面第 6 章会讲用 MyBatis-Plus 根据实体类生成建表 SQL 的救急办法。3.2 数据源配置与建库jdbc.properties 的四个必查参数跑通这步最常卡在数据库连接。要看懂这份 zip 里用的是什么框架老课设是纯 JDBC配置文件叫jdbc.properties或db.properties新一点的是 Spring Boot MyBatis配置在application.yml。不管哪种本质上都是四个参数driver、url、username、password。# jdbc.properties经典 JDBC/MyBatis 课设配置 jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456第一行com.mysql.jdbc.Driver适配 MySQL 5.7如果本机装的是 MySQL 8驱动类要换成com.mysql.cj.jdbc.Driverurl 必须带serverTimezone否则启动后第一次查询会直接抛The server time zone value is unrecognized。characterEncodingutf8是保证「Java 字符串 - MySQL」不转成乱码的关键和页面编码、请求编码三层要一致。参数说明useSSLfalse是在本机开发时关掉 SSL 握手少一次失败可能serverTimezoneAsia/Shanghai指定时区避免日期差 8 小时allowPublicKeyRetrievaltrue只在 MySQL 8 用 caching_sha2_password 认证时加如果没加会报Public Key Retrieval is not allowed。注意这是 java 环境配置的一部分很多人卡在环境上其实是卡在数据库驱动和 url 上不是 JDK 没装好。然后是建库。用命令行或 Navicat 都行# 在 MySQL 里创建数据库并导入课设带的 sql mysql -uroot -p CREATE DATABASE hotel DEFAULT CHARSET utf8mb4; USE hotel; SOURCE /path/to/hotel.sql;SOURCE后面是绝对路径Windows 下注意斜杠方向建议先cd到 sql 文件目录再执行SOURCE hotel.sql避免路径转义问题。导入后执行SHOW TABLES;看到 user、room_type、room、orders 四张表就说明库建对了。3.3 部署到 Tomcatwar exploded 的选择与验证清单课设用的 Tomcat 很少是真正生产版本多半是本地解压版。常见做法分两种IDEA 里配 Tomcat或者直接把项目打成 war 丢进webapps。我推荐 IDEA 方式调试能看到 stack trace。# IDEA 部署要点 # Run - Edit Configurations - 新建 Tomcat Server/Local # Application server 选你解压的 Tomcat 目录 # Deployment - 选 Artifact 类型为 xxx:war exploded # Application context 填 /hotel # 启动前确认 build 输出里有 classes 和 lib用war exploded而不是war因为前者是展开目录修改 JSP 不用重新打包刷新页面就能看到。Application context如果填/访问首页就是http://localhost:8080/index.jsp填/hotel就是http://localhost:8080/hotel/index.jsp注意后面所有前端跳转路径都要带这个 context否则 404。课设里常见翻车场景是页面写死a href/login结果部署在/hotel下就跳到根路径去了。解决办法是页面里全部用${pageContext.request.contextPath}拼路径。启动验证按这个顺序来启动日志末尾出现Server startup in [xxx] milliseconds没有Exception访问http://localhost:8080/hotel/首页能打开注册一个用户跳转不报 500直接访问管理员后台地址会被踢回登录页。如果卡在前两步多数和 jar 包没进WEB-INF/lib有关具体排查放第 5 章。还有一个兼容性问题Tomcat 9 需要 JDK 8 以上Tomcat 8.5 可以跑 JDK 7/8。如果课设代码是用 JDK 8 编译的你用 JDK 17 跑老代码常见是启动能起来但某些反射操作报IllegalAccess警告页面功能不报错也要留意。遇到「java 启动失败怎么解决」的搜索需求时先看 catalina.log 里 Caused by 那一行驱动类和端口占用是两个最容易误判的点。4. 核心业务实现预订、扣房、取消订单的代码路径4.1 预订接口一次请求不只是 insert而是四步校验这是整个系统最值的部分。很多课设的预订功能就一个 insert结果同一间房被两个用户重复订同一个时间段页面还提示成功。正确的最小流程是校验登录 - 校验日期 - 校验当前订单是否重叠 - 插入订单。代码可以裁剪成下面的样子// OrderService.java 核心方法代码做了裁剪保留主要判断路径 public synchronized boolean createOrder(OrderVO vo) { // 1. 日期合法性入住不能早于今天退房必须晚于入住 if (vo.getCheckInDate().before(todayStart())) { return fail(入住日期不能是过去); } if (!vo.getCheckOutDate().after(vo.getCheckInDate())) { return fail(退房日期必须晚于入住日期); } // 2. 查询同一房间、同一时间段内是否有状态为待支付/已确认的订单 // 注意区间判断用的是 和 而不是 between String sql SELECT COUNT(*) FROM orders WHERE room_id ? AND status IN (0, 1) AND check_in_date ? AND check_out_date ?; int overlap namedJdbcTemplate.queryForObject( sql, new Object[]{vo.getRoomId(), vo.getCheckOutDate(), vo.getCheckInDate()}, Integer.class); if (overlap 0) { return fail(该房间在所选日期内已被预订); } // 3. 生成订单号并插入 OrderPO po new OrderPO(); po.setOrderNo(generateOrderNo(vo.getUserId())); po.setUserId(vo.getUserId()); po.setRoomId(vo.getRoomId()); po.setCheckInDate(vo.getCheckInDate()); po.setCheckOutDate(vo.getCheckOutDate()); po.setTotalPrice(calcPrice(vo)); // 状态默认待支付 int rows orderMapper.insert(po); return rows 1 ? success(po.getId()) : fail(下单失败); }先看第二步 SQL。要判断两个区间[A, B)和[C, D)是否重叠标准条件是A D AND C B。对应到代码里入参入驻区间是[check_in_date, check_out_date)已有订单区间是[check_in_date, check_out_date)所以 SQL 写check_in_date 入参退房日期 AND check_out_date 入参入住日期。如果写成或边界日期会踩「入住当天和退房当天算不算重叠」的坑。注意约定同一间房退房当天可以入住所以check_out_date对比时用而不是。方法加了synchronized课设阶段用同步防止两个线程同时通过检查并插入重叠订单。这不算最优解但能让系统在本地测出并发问题。真正生产级要用数据库唯一约束或事务加锁后面进阶章会补。generateOrderNo生成规则建议yyyyMMddHHmmss userId 三位随机数长度 32 以内同时给订单号建唯一索引防止重复。4.2 日期冲突与余房计算按房间查重还是按房型计数上面代码是「按房间查重」适合把订单挂在 room_id 上的表结构。另一种常见设计是「按房型维护余房数字段」在 room_type 表加available_count每下一单减一。这种设计的坑是余房数很容易和实际订单不一致有人取消订单忘了回加有人下单失败却提前扣了数。如果你非要按房型维度去查可以不用余房数字段而是实时查「可售房间中没有被重叠订单占用的房间数」。SQL 写成-- 查询某房型在指定日期区间内剩余可订房间数 SELECT COUNT(*) FROM room r WHERE r.type_id ? AND r.status 0 AND r.id NOT IN ( SELECT o.room_id FROM orders o WHERE o.status IN (0, 1) AND o.check_in_date ? AND o.check_out_date ? );这段 SQL 的思路是「先找到所有可售房间再排除掉任何订单重叠的房间」。两个?分别对应退房日期和入住日期同样遵循A D AND C B规则。NOT IN子查询在课设数据量几百单下性能没问题但要保证子查询里orders.room_id有索引。这种写法比维护available_count省心唯一的代价是每次查询要扫描该房型下的房间课设规模完全可接受。参数说明r.status 0把维修房排除o.status IN (0,1)只把待支付和已确认算作占用已取消和已完成不参与冲突。有人会把已完成订单排除在外要注意已完成订单的日期已经是过去式本来就不会和未来预订重叠排除与否对结果没有影响但写进去语义更清晰。4.3 取消订单改状态只是第一步关键是「释放」有没有对称取消订单课设里经常被写成update orders set status2 where id?这样写也行但会漏两个问题订单归属校验和取消时机。必须只能取消自己的订单且待支付/已确认订单才允许取消已完成订单不能再改。// 取消订单先查后改校验归属和状态 public void cancelOrder(Integer orderId, Integer userId) { OrderPO order orderMapper.selectById(orderId); if (order null || !order.getUserId().equals(userId)) { throw new BizException(订单不存在或无权操作); } if (order.getStatus() ! 0 order.getStatus() ! 1) { throw new BizException(当前订单状态不可取消); } // 如果表结构里有 available_count这里要把余房数加回来 // 本表结构按 room_id 查重把订单置 2 后4.1 的查询自动放行无需回加 orderMapper.updateStatus(orderId, 2); }status用 int 而不是 boolean因为状态有 4 个用 boolean 就是给自己埋坑。取消操作没有走「删除订单」因为需要保留订单记录对账这也是这个系统的基本业务逻辑酒店订单不能删只能取消。BizException是自定义异常页面拿到后弹提示而不是把整个堆栈打给用户。状态机这块建议在实体里加一个statusText()方法把 0、1、2、3 映射成「待支付 / 已确认 / 已取消 / 已完成」JSP 里% order.getStatusText() %输出不要在页面里写一堆if (status 0)。这样面试问起状态机你能说清楚状态不在任意两态间跳转比如已取消不能变回已确认。5. 避坑与常见问题启动失败、乱码、日期差一天五个高频坑5.1 404 / ClassNotFoundExceptionjar 包没进 WEB-INF/lib现象Tomcat 能启动但访问项目路径 404或者明明写了Class.forName(com.mysql.jdbc.Driver)却在运行时报ClassNotFoundException。原因IDE 里的 lib 目录只是项目级文件夹没有输出到 Web 工程的WEB-INF/lib。Java 编译器能看到 jar运行时容器看不到这是课设包最常见的问题。解决在 IDEA 里打开 Project Structure - Libraries确认依赖的 jar 范围至少是Compile然后到 Artifacts 的 Output Layout 里看WEB-INF/lib下有没有对应 jar。另一个笨办法是直接把 jar 复制到src/main/webapp/WEB-INF/lib/让 IDE 重新构建。建议排查顺序先看WEB-INF/lib目录内容再改 Project Structure不要一上来就重装 Tomcat。5.2 中文变 ??三层编码没对齐现象页面 JSP 里中文正常从表单提交到数据库后变成??或者数据库里中文正常页面读出来乱码。原因这是三层编码不一致JSP 页面编码、请求参数编码、数据库连接编码。常见配置只设了第一层忘了第二层或第三层。解决JSP 顶部写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%Servlet 或 Filter 里设置request.setCharacterEncoding(UTF-8)JDBC url 里带characterEncodingutf8。这三层全部统一后中文问题基本消失。搜索 java 编码相关问题时很多人只改过滤器漏了连接串这是第一个要排查的。5.3 日期差一天java.util.Date 和 java.sql.Date 的黑匣子现象页面上选的入住时间是 2026-07-01数据库存成 2026-06-30或者反过来查询时少了 8 小时。原因常见是把SimpleDateFormat解析出的java.util.Date直接传给 MyBatis 或 PreparedStatement框架转换时走了本地时区或者setDate方法只接受java.sql.Date而你把java.util.Date强转丢掉了时分秒。另外 MySQL 连接串没加serverTimezone也会导致时区偏移。解决代码里分离两个类型。接参用java.util.Date入库前转java.sql.Datejava.util.Date in new SimpleDateFormat(yyyy-MM-dd).parse(param); PreparedStatement ps conn.prepareStatement(sql); ps.setDate(1, new java.sql.Date(in.getTime()));new java.sql.Date(in.getTime())是把 util 日期的时间戳交给 sql 日期不会丢精度。反过来从ResultSet拿出来时用rs.getDate(check_in_date)再转成 util别自己拼字符串。这个坑属于「现象像乱码、根因是时区」排查时可以先把日期原样打到日志里看。5.4 MySQL 8 和 MySQL 5 驱动混用启动失败最容易被误导现象Tomcat 一启动报The server time zone value is unrecognized或者Public Key Retrieval is not allowed很多人以为是 JDK 坏了。原因本机 MySQL 是 8.0但课设代码里写的是老驱动com.mysql.jdbc.Driverurl 也没带时区参数。MySQL 8 的认证插件默认 caching_sha2_password需要额外许可。解决按数据库版本选驱动和 url。我一般直接统一成 MySQL 8 写法因为 8 驱动能向下兼容 MySQL 5.7 协议jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue只用于开发环境生产环境建议换成认证方式或 SSL。如果你是在老服务器上部署也是一样的排查思路先确认 MySQL 版本再看驱动 jar 的版本和 url 参数。5.5 连接池耗尽订几次房就变慢重启又恢复现象点几次预订后系统响应越来越慢控制台报Connection is not available, request timed out重启 Tomcat 后恢复过一会儿又不行。原因DAO 里拿 Connection 后没有关闭。纯 JDBC 代码最容易犯只要try里开连接、没有finally关连接池连接就会被耗尽。解决用 try-with-resources 或 finally 关闭。课设老代码里最常见的是只关了ResultSet漏了PreparedStatement和Connection。统一写法Connection conn null; try { conn dataSource.getConnection(); // 业务代码 } finally { if (ps ! null) { try { ps.close(); } catch (SQLException ignore) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignore) {} } }conn.close()在连接池里不是真实关闭而是把连接还回池子所以关闭顺序要先关 statement 再关 connection否则会让连接处于不可复用状态。加了synchronized的 createOrder 也会放大这个问题一个请求卡在数据库操作其余请求全部排队表现就是「越来越慢」。如果遇到这类启动成功但越跑越慢的问题优先看连接是否泄漏。6. 进阶改动让这个课设项目经得起面试追问与其背 java 八股文里「项目怎么保证数据一致性」不如把这份酒店预订系统的两个短板补上事务和并发校验。下面三个改动是课设升级里性价比最高的。6.1 用 MyBatis-Plus 把 JDBC 换成实体类驱动如果你不想止步于 JSP 原生 JDBC最常见的落地路径是接 Spring Boot MyBatis-Plus。此时建表不用再手写 SQL定义好实体类后开启代码生成器可以直接根据实体类生成创建表的 SQL 语句去掉 2.3 节手工建表的一半工作。// RoomType.java 实体类建表字段和注解一一对应 Data TableName(room_type) public class RoomType { TableId(type IdType.AUTO) private Integer id; private String typeName; private BigDecimal price; private Integer bedCount; }TableName指定表名TableId标记主键。这样改表结构的入口从 SQL 文件转移到了实体类字段改动不容易出现两边不一致。课设里用 MyBatis-Plus 的价值不在减少代码量而在面试时能说明白 ORM 和原生 JDBC 的边界。6.2 订房与扣库存用事务绑在一起4.1 的 createOrder 只做一件事插入订单所以单条 insert 本身是原子的。但如果你按 4.2 那种「先查再插」的逻辑加余房数字段查和减是两条 SQL必须放到一个事务里。Spring 中加Transactional(rollbackFor Exception.class)或者在 JDBC 里手动conn.setAutoCommit(false)加commit/rollback。Transactional(rollbackFor BizException.class) public void createOrderWithStock(OrderVO vo) { roomTypeMapper.decreaseAvailable(vo.getTypeId()); orderMapper.insert(convert(vo)); }decreaseAvailable和insert任何一步抛异常订单不会残留在表里余房数也不会被扣成负数。这是我调试这个系统时最后悔没早做的一件事最初只做 insert取消订单还得手动补数据。6.3 并发验证一个账号、两台设备、同一间房改造做完验证方法很重要。打开两个浏览器无痕窗口用同一个账号同时订同一间房同一天。正确结果是只有一个成功另一个提示「已被预订」。如果两个都成功说明你的查重没生效或事务隔离级别不对。这是我带人改课设时必做的一步。很多项目能跑通但并发一测就翻车。做开发别只求「点着不报错」要主动制造边界条件。记住这条习惯改完并发相关代码先用两个窗口打一下再谈完成。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站