SpringBoot景区民宿预约系统:数据库设计与高并发库存扣减实战
简介本资源面向计算机专业学生与有项目实战需求的学习者提供一套基于Spring Boot框架的景区民宿预约系统完整实现方案涵盖源码、数据库与配套论文可用于毕业设计选题或课程实践参考。压缩包共883个文件约28.93MB以Java后端代码、Vue与JavaScript前端脚本、HTML与CSS页面样式为主另含SQL建库脚本、XML配置、图片与字体等静态资源并附bat启动脚本与说明文档便于快速部署运行。系统围绕用户管理、民宿信息管理、预约与订单管理等模块展开采用MVC分层结构结合MySQL存储数据、Spring Security实现安全控制并运用响应式设计适配多端浏览。目前已有65人学习下载。读者可据此掌握从数据库表设计到前后端联调的完整流程理解各实体表之间的关联关系与业务逻辑为后续开发积累可复用的工程经验。1. 景区民宿预约系统从旺季满房到订单对不上问题出在哪做景区民宿的老板最怕旺季。五一、十一前两周电话、微信、OTA 平台同时来单前台拿个本子记记漏一笔就是到店无房的客诉。我见过最离谱的一家山脚下十二间房黄金周七天卖了九十三单实际只能接待八十四单多出来的九单全靠临时协调村民家借宿才没炸。这不是态度问题是工具问题。基于 springboot 框架开发的景区民宿预约系统要解决的就是把房态、订单、入住人、支付状态锁在一张表里让每一次点击都有数据库事务兜底。它适合两类人一是手里有景区房源、想自己掌控订单数据的中小经营者二是拿这个题目做课程设计或毕业设计的同学因为业务边界清晰、表结构不复杂、springboot 生态成熟属于能跑通又能讲清楚的选题。源码、数据库脚本和论文三件套本质是把「能演示」和「能答辩」两个目标合并了。2. 先把表结构定死景区民宿预约系统的数据库怎么设计2.1 五张核心表撑起整个预约链路民宿预约的业务链路其实很短用户看房 → 选日期 → 下单 → 支付 → 入住 → 退房。但短链路最容易在设计阶段偷懒等到写查询才发现缺字段。我一般会先把这五张表定下来后面所有接口都围着它们转。表名作用关键字段注意点user用户信息id, openid, phone, real_name手机号做唯一索引别只靠前端校验room房源信息id, title, price, stock, statusstock 是当日可售库存不是总房间数room_calendar每日房态room_id, date, stock, price联合唯一索引 (room_id, date)order订单主表id, order_no, user_id, room_id, check_in, check_out, total_amount, statusorder_no 用业务编号别用自增 id 暴露给前端order_guest入住人order_id, name, id_card一单多人和主表一对多这里最容易被忽略的是room_calendar。很多同学只建了room表用stock字段扣减结果遇到跨日期订单就翻车客人订 3 号到 5 号你扣的是哪天的库存正确做法是按天拆库存下单时对check_in到check_out之间每一天做扣减退房时再逐天回滚。这个设计决定了后面并发扣库存能不能做对。2.2 建表 SQL 与索引落地-- 房源日历表按天管理库存和价格 CREATE TABLE room_calendar ( id BIGINT NOT NULL AUTO_INCREMENT, room_id BIGINT NOT NULL COMMENT 房源ID, calendar_date DATE NOT NULL COMMENT 日期, stock INT NOT NULL DEFAULT 0 COMMENT 当日可售库存, price DECIMAL(10,2) NOT NULL COMMENT 当日价格, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_room_date (room_id, calendar_date), KEY idx_date (calendar_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源每日房态; -- 订单表业务编号唯一状态机驱动 CREATE TABLE order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL, room_id BIGINT NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, total_amount 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 (user_id), KEY idx_room_date (room_id, check_in) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;uk_room_date这个联合唯一索引是防超卖的第一道闸门它保证同一房源同一天只有一条房态记录避免并发插入产生重复行。version字段留给乐观锁后面扣库存会用到。订单表的status用数字枚举而不是字符串查询效率更高但要在代码里用常量类维护别在 SQL 里写魔法数字。提示check_in和check_out用 DATE 而不是 DATETIME民宿按天计费时间部分只会带来时区麻烦。2.3 库存扣减的两种写法与选择扣库存是预约系统的命门。常见做法有两种悲观锁SELECT ... FOR UPDATE和乐观锁版本号。前者在事务里锁行简单但并发高时排队明显后者靠version字段 CAS 更新失败重试。我一般对民宿这种并发量单房源日订单通常个位数用乐观锁就够了代码里重试三次基本不会失败。-- 乐观锁扣减影响行数为 0 说明被其他事务改过需要重试 UPDATE room_calendar SET stock stock - 1, version version 1 WHERE room_id #{roomId} AND calendar_date #{date} AND stock 0 AND version #{version};参数说明stock 0是兜底防止库存扣成负数version #{version}是乐观锁条件查询时先读出当前 version。如果返回影响行数为 0不要直接抛异常给用户而是在 service 层循环重试最多三次仍失败才提示「当前日期已满」。这个细节在论文里写清楚答辩时是加分项。3. springboot 项目骨架怎么搭从 Maven 依赖到接口分层3.1 依赖选型与版本控制springboot 版本太高是热搜里常被吐槽的点我一般锁在 2.7.x 或 3.2.x 这两个长期维护线上。2.7.x 对 JDK 8 友好3.2.x 需要 JDK 17课程设计环境用哪个取决于学校机房。Maven 依赖不用堆太多核心就这几块dependencies !-- Web 层REST 接口 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus单表 CRUD 不用手写 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 参数校验NotNull Future 等注解 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesMyBatis-Plus 的版本要和 springboot 主版本匹配3.5.5 对应 springboot 2.7 和 3.x 都能用。别引入spring-boot-starter-data-jpa又引 MyBatis两套 ORM 混用会让事务和缓存行为变得难以预测这是血泪经验。3.2 配置文件与数据库连接server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/scenic_homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai必须加否则 DATE 字段可能差一天这个坑在跨时区服务器上尤其明显。map-underscore-to-camel-case让room_id自动映射到roomId省掉大量Results注解。逻辑删除配置配合表里的deleted字段订单取消用逻辑删除而不是物理删除方便对账。3.3 下单接口的完整实现下单是核心接口要在一个事务里完成校验日期 → 扣每日库存 → 写订单 → 写入住人。任何一步失败全部回滚。Service public class OrderService { Autowired private RoomCalendarMapper calendarMapper; Autowired private OrderMapper orderMapper; Transactional(rollbackFor Exception.class) public String createOrder(OrderCreateDTO dto) { // 1. 校验入住日期必须晚于今天 if (!dto.getCheckIn().isAfter(LocalDate.now())) { throw new BizException(入住日期不合法); } // 2. 逐天扣减库存checkOut 当天不占库存 LocalDate cursor dto.getCheckIn(); while (cursor.isBefore(dto.getCheckOut())) { int affected calendarMapper.deductStock(dto.getRoomId(), cursor); if (affected 0) { throw new BizException(cursor 已满房); } cursor cursor.plusDays(1); } // 3. 生成订单号并落库 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setRoomId(dto.getRoomId()); order.setCheckIn(dto.getCheckIn()); order.setCheckOut(dto.getCheckOut()); order.setStatus(0); orderMapper.insert(order); return order.getOrderNo(); } }逻辑说明while (cursor.isBefore(dto.getCheckOut()))这个边界是关键退房当天不占库存所以循环到checkOut前一天为止。deductStock返回影响行数为 0 说明当天库存不足或版本冲突直接抛异常触发回滚。Transactional(rollbackFor Exception.class)保证受检异常也回滚默认只回滚 RuntimeException这个细节不写清楚库存扣了订单没生成就是灾难。参数说明OrderCreateDTO里checkIn和checkOut用Future和NotNull校验roomId用Positive。订单号生成用「日期 房源 id 随机数」保证可读且唯一别用 UUID对账时人眼没法看。4. 避坑与排查景区民宿预约系统上线前必须过的五道坎4.1 跨日期订单库存扣减漏天现象客人订 3 号到 5 号只扣了 3 号库存4 号还能被其他人订走。原因循环边界写成cursor.isBefore(checkOut.plusDays(1))或者直接用checkOut做条件多扣或少扣一天。解决明确「退房当天不占库存」这条业务规则循环条件固定为cursor.isBefore(checkOut)并在单元测试里覆盖「同一天入住退房」「跨月」「跨年」三种边界。4.2 并发下单导致超卖现象两个请求同时读到 stock1都判断可订都扣减成功实际卖出两间。原因查询和更新分离没有锁或版本控制。解决用 2.3 节的乐观锁 SQLstock 0 AND version #{version}两个条件缺一不可。压测时用 JMeter 开 50 个线程打同一房源同一天观察是否出现负库存。4.3 订单状态机乱跳现象已取消的订单还能被支付回调改成已支付或者已入住订单被重复取消。原因状态流转没有校验任何接口都能改 status。解决在 service 层写一个状态流转表只允许0→1、1→2、2→3、0→4、1→4这几种迁移其他一律拒绝。支付回调里先查当前状态不是待支付就直接返回成功但不改数据防止重复回调。4.4 日期时区导致查询差一天现象前端传2025-05-01数据库存成2025-04-30。原因JDBC 连接串没配serverTimezone或者服务器时区和数据库时区不一致。解决连接串固定serverTimezoneAsia/Shanghai实体类日期字段用LocalDate而不是java.util.DateJackson 配置time-zone: GMT8。排查时直接SELECT calendar_date FROM room_calendar看原始值别只看接口返回。4.5 论文里的 ER 图和代码对不上现象论文画了七张表代码里只有五张答辩被问「订单日志表在哪」。原因先写代码后补论文或者论文抄了模板没改。解决以数据库脚本为准反向画 ER 图用 Navicat 或 dbx 数据库工具导出表结构字段名、类型、注释一一对应。论文里的「数据库设计」章节直接贴建表 SQL 的关键片段比纯文字描述可信得多。5. 从能跑到能答辩三个让系统站住脚的进阶技巧第一个技巧是给房态加缓存预热。景区民宿的查询有明显的读多写少特征旺季时用户反复刷新房态页。我一般用 springboot 整合 Redis把room_calendar按room_id 月份缓存下单扣库存时先删缓存再更新数据库避免脏读。缓存 key 设计成calendar:{roomId}:{yyyyMM}过期时间设 10 分钟既扛住刷新又不会和数据库差太久。这一步在论文里可以写成「性能优化」章节有数据支撑压测 QPS 从 200 提到 1500 左右。第二个技巧是订单超时自动取消。待支付订单占着库存不放是民宿系统最容易被忽略的漏洞。用 springboot 的Scheduled每分钟扫一次status0 AND create_time now - 15min的订单批量改成已取消并回滚每日库存。回滚时同样要逐天加回别只加一天。这个定时任务要加分布式锁否则多实例部署时会重复回滚库存越滚越多。第三个技巧是论文里的测试数据要真实。很多同学的论文测试章节就写「功能正常」答辩老师一看就知道没跑过。我一般会造三类数据正常单日订单、跨黄金周七天订单、并发冲突订单每类给出请求参数、数据库前后对比、接口返回。用表格呈现比大段文字有说服力。测试场景请求参数预期结果实际结果单日预订roomId1, 5.1~5.2扣 5.1 库存订单待支付一致跨周预订roomId1, 5.1~5.7扣 5.1~5.6 共六天一致并发预订50 线程抢 5.1 最后一间仅 1 单成功49 单提示满房一致最后说个习惯我做完这类系统一定会把数据库脚本、接口文档、压测报告三个文件放在同一个目录命名带日期。答辩前一周不再改代码只对着这三份文件过流程。论文里的每一张截图都能在代码里找到对应的那一行。希望帮到你。本文还有配套的精品资源点击获取