ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue酒店预约管理系统:前后端分离与并发防重实战

SpringBoot+Vue酒店预约管理系统:前后端分离与并发防重实战 简介面向需要完成课程设计或毕业设计的JavaWeb学习者这份基于SpringBootVue的酒店预约管理系统提供了一套前后端分离的完整实现方案。后端采用SpringBoot与MyBatis-Plus前端基于Vue与ElementPlus涵盖酒店管理预约系统的核心业务模块可用于快速理解主流框架整合流程并直接改造复用。资源包共3个文件压缩后总大小约63.93MB其中含一个SQL数据库脚本与两个zip压缩包分别对应后台系统源码与前端工程便于分别导入和后端联调。已有2496人学习下载适合具备一定Java和Vue基础、希望参考真实项目结构的中级学习者。通过下载可获得完整项目源代码、数据库初始化脚本可辅助完成环境搭建、功能二次开发与部署上线对掌握SpringBootVue全栈开发有明显参考价值。1. 先从一次“周末满房”说起springbootvue 酒店预约管理系统在解决什么springbootvue 酒店预约管理系统说到底是给酒店解决一个最现实的问题同一间房不能被同一晚订出去两次。纸质房态表加电话确认的传统做法在节假日订单涌进来时完全撑不住超卖、错房、退订不及时每一条都在前台被放大。这套系统把流程拆成前后端分离的两半SpringBoot 管房态、预约单、超时释放Vue 管选房、下单、订单列表。接口只吐数据页面只管交互两端独立部署各自能换技术栈。适合两类人一类是拿它当课程设计或毕业设计需要一套能跑通、能答辩、能讲清设计理由的骨架另一类是刚接触前后端分离项目想弄明白 axios 转发、vue 路由参数、打包部署这些坎的从业者。下面前四章按后端、前端、联调、兜底递进第五章落在并发防重这个最容易出事的点上。2. 后端先行用 SpringBoot 把“房态和预约单”拆成可落地的接口2.1 先定数据模型房间表、预约表、订单状态怎么设计所有预约系统的混乱根源都是“房态”和“预约单”没有分开。房态是某一时刻房间的即时印象空闲、已订、清洁中预约单是业务事实谁、什么时候、订了多久。这两者必须各有一张表否则改个房态就把订单历史弄丢了。常规做法是至少三张表房间表、预约单表、锁房表防止同一晚重复预订。客户不多的小酒店可以先把客户姓名电话直接塞进预约单不必单独建客户表表关键字段说明tb_roomid、room_no、room_type、price、floor、statusstatus0 空闲、1 已订、2 清洁中tb_reservationid、room_id、customer_name、customer_phone、check_in_date、check_out_date、status、create_timestatus0 待支付、1 已确认、2 已入住、3 已退房、4 已取消tb_room_lockid、room_id、lock_date、reservation_idlock_date 表示“某一天”配合唯一索引兜住并发预约单里存 check_in_date 和 check_out_date 两个日期退房当天默认不占用房间所以实际占用天数是check_out_date - check_in_date对应的每一天。这个口径要在接口层统一前端展示和后端校验共用同一套逻辑不然就会出现“明明锁了 3 天日历上只显示 2 天”的偏差。2.2 创建项目的标准路径IDEA 建 SpringBoot版本别踩坑在 IDEA 里创建 SpringBoot 项目路径是 New Project → Spring Initializr填好 Group 和 Artifact勾选 Spring Web、MySQL Driver、MyBatis-Plus。这里最容易被卡住的不是建项目本身而是“springboot版本太高”导致的连锁问题Spring Boot 3.x 强制要求 JDK 17并且把包名从javax.*换成了jakarta.*如果本机还是 JDK 8要么把 Boot 降到 2.7.x要么装 JDK 17 并满处改 import。学习型项目建议直接选 2.7.x 系列生态里的教程、报错帖和面试题基本都围绕这条版本线踩坑成本最低。依赖不要贪多预约管理系统最核心的就这几项dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId !-- 选与 Spring Boot 2.7 兼容的 3.5.x 版本 -- version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies提示mybatis-plus-boot-starter 优先用 3.5.x 而不是 3.4.x因为 3.5 以后对 Spring Boot 2.7 的自动装配兼容性更好分页插件 PaginationInnerInterceptor 的写法也更简单。这里顺便说清一个经常被问到、也被很多人忽略的点SpringBoot 为什么加了 starter 就能自动配好数据源核心在SpringBootApplication里的EnableAutoConfiguration它通过META-INF/spring.factories2.7 及以前或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports3.0 之后加载候选配置类再根据类路径上有没有对应的类决定是否生效。“自动装配”不是黑魔法而是条件化加载你引入了mybatis-plus-boot-starterMybatisPlusAutoConfiguration条件满足才会去读spring.datasource配置。出了问题先查“哪个配置类没生效”比照着配置抄一百遍管用。2.3 预约核心接口Controller 和日期校验怎么写接口设计的核心矛盾是不要让前端把业务规则算完再传给后端而是后端必须自己校验“这个房间这几天到底能不能订”。所以预约接口里最重要的就是 check 逻辑RestController RequestMapping(/api/reservation) public class ReservationController { private final ReservationService reservationService; public ReservationController(ReservationService reservationService) { this.reservationService reservationService; } PostMapping public Result create(RequestBody ReservationDTO dto) { // dto 里带 roomId、customerName、customerPhone、checkInDate、checkOutDate // 1. 校验入住日期必须在退房日期之前 // 2. 校验入住日期不能早于今天 // 3. 按晚拆分日期逐晚写入 tb_room_lock任一晚冲突则整单失败 // 4. 写 tb_reservation状态置为 0待支付 return reservationService.create(dto); } }Override Transactional(rollbackFor Exception.class) public Result create(ReservationDTO dto) { LocalDate checkIn LocalDate.parse(dto.getCheckInDate()); LocalDate checkOut LocalDate.parse(dto.getCheckOutDate()); if (!checkOut.isAfter(checkIn)) { return Result.error(退房日期必须晚于入住日期); } ListLocalDate nights new ArrayList(); for (LocalDate d checkIn; d.isBefore(checkOut); d d.plusDays(1)) { nights.add(d); } // 逐晚尝试插入锁记录靠唯一索引 uk_room_date 兜住并发 for (LocalDate night : nights) { try { roomLockMapper.insert(new RoomLock(dto.getRoomId(), night, null)); } catch (DuplicateKeyException e) { throw new BizException(该房间在 night 已被预约); } } return Result.ok(reservationMapper.insert(buildReservation(dto))); }这段代码的关键有三处。第一Transactional保证锁记录和预约单要么都成功要么都回滚绝不能出现“锁住了但订单没建成”的半截数据。第二按晚写tb_room_lock把“多天连续占用”拆成“每一天的原子占用”数据库唯一索引才能真正兜住跨天场景。第三catch (DuplicateKeyException)是最硬的防线即使两个请求同时通过查询校验最终插入时也只有一个能成功。2.4 接口清单和统一返回结构前后端分离项目里接口约定比接口实现更容易让联调崩掉。常见做法是后端统一返回{ code, msg, data }前端根据code判断业务成功与否而不是靠 HTTP 200/500 猜。预约管理系统的接口一般就这几条方法路径参数说明GET/api/room/listpage、size、roomType房间分页列表管理端使用GET/api/room/availablestartDate、endDate、roomType查可订房源POST/api/reservationJSONroomId、name、phone、checkInDate、checkOutDate创建预约PUT/api/reservation/{id}/statusJSONstatus、remark确认入住、退房、取消GET/api/reservation/listpage、size、phone按手机号或分页查预约单/api/room/available的查询逻辑是先按时间条件查tb_room_lock里已占用的房间 ID再从tb_room里排除这些 ID同时过滤掉 status2清洁中的房间。这个过程必须放在数据库里用 NOT IN 或 LEFT JOIN 完成不能把房间全量查出来在 Java 里 filter——几十间房时无所谓几百间时接口耗时和连接占用会明显上升。3. 前端落地Vue 项目的初始化、路由和下单页对接3.1 从零初始化Node 环境、安装依赖和常见报错处理前端建议直接用官方脚手架vue/cli新项目用 Vue 3。安装依赖这个环节报错率远高于写代码最常见的是 npm 网络超时先把镜像切到国内源再装node -v npm install -g vue/cli vue create hotel-front cd hotel-front npm install axios element-plus vue-router4 dayjsvue/cli是脚手架本体vue create交互式生成项目骨架选默认的 Vue 3 预设axios负责发 HTTP 请求element-plus提供表格、表单、日期选择器这类现成组件vue-router4对应 Vue 3老项目里遗留的vue-router3只能配 Vue 2混用会直接白屏dayjs用来处理日期区间计算比原生 Date 省事。装完后先跑npm run serve确认能出默认页再去写业务代码。不要一上来就把依赖全部装齐缺什么装什么依赖树的报错会好排查很多。3.2 四个页面的路由设计vue 路由参数怎么传才不丢预约系统前端一般就四个页面首页房型展示、房间详情、下单页、我的订单。路由设置如下// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /room/:id, name: RoomDetail, component: () import(/views/RoomDetail.vue) }, { path: /booking, name: Booking, component: () import(/views/Booking.vue) }, { path: /orders, name: Orders, component: () import(/views/Orders.vue) }, ] export default createRouter({ history: createWebHistory(), routes, })这里有一个 vue 路由参数的经典坑。:id这种是 params 参数跳转时必须用name加paramsrouter.push({ name: RoomDetail, params: { id: row.id } })如果用router.push({ path: /booking, query: { roomId: row.id, price: row.price } })接收方要用route.query.roomId。两种传参的差别直接决定刷新后是否丢参数传参方式跳转写法接收写法刷新后是否保留paramsrouter.push({ name: RoomDetail, params: { id } })route.params.id否queryrouter.push({ path: /booking, query: { roomId } })route.query.roomId是params 适合实体 ID 这种“页面身份”刷新丢了就回到列表重选query 适合“选完房型带过去下单”这种上下文数据刷新后 URL 上还带着下单页能自己恢复状态。下单页从房间详情跳过来建议把 roomId 放在 query 里。3.3 axios 封装和联调转发不做这步接口必跨域Vue 跑在 8081SpringBoot 跑在另一个端口浏览器会直接拦跨域。最省事的方案开发环境在vue.config.js里配置 devServer 转发// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }同时把 axios 封装成统一实例// src/api/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000, }) request.interceptors.response.use( (resp) { const { code, msg } resp.data if (code ! 0) { alert(msg || 请求失败) return Promise.reject(new Error(msg)) } return resp.data.data }, (err) Promise.reject(err) ) export default requestproxy的/api前缀让浏览器以为请求是同源的实际由 devServer 转发给后端baseURL: /api保证所有请求都走转发统一在响应拦截器里解code业务代码里拿到的就是干净的 data不用每个组件都写一遍resp.data.data生产环境没有 devServer就要靠 nginx 转发这个在下一章部署部分讲。3.4 下单页的核心逻辑日期联动和可订房查询下单页是最能体现前端价值的地方用户选完入住、退房日期立刻看到这个房型还有哪几间能订。核心逻辑是监听日期变化后调可用房间接口template el-form el-form-item label入住日期 el-date-picker v-modeldates typedaterange changeloadAvailable / /el-form-item el-form-item label房型 el-select v-modelroomType changeloadAvailable el-option label大床房 value大床房 / el-option label双床房 value双床房 / /el-select /el-form-item el-form-item label可选房间 el-checkbox-group v-modelselectedRoomIds el-checkbox v-forr in rooms :keyr.id :valuer.id {{ r.roomNo }}¥{{ r.price }} /el-checkbox /el-checkbox-group /el-form-item /el-form /template script setup import { ref } from vue import request from /api/request import dayjs from dayjs const dates ref([]) const roomType ref() const rooms ref([]) const selectedRoomIds ref([]) async function loadAvailable() { if (!dates.value || dates.value.length ! 2 || !roomType.value) return // 序列化前用 dayjs 格式化避免 Date 对象被转成英文格式 const [start, end] dates.value.map((d) dayjs(d).format(YYYY-MM-DD)) rooms.value await request.get(/room/available, { params: { startDate: start, endDate: end, roomType: roomType.value } }) } /script两个容易忽略的细节。daterange返回的数组里是两个 Date 对象直接放进params会被序列化成Fri May 10 2024...后端LocalDate.parse会直接报错所以传参前先用 dayjs 格式化。另外el-checkbox的value属性名在 element-plus 不同小版本里有差异报 “Cannot read properties of undefined” 时先查这一处。4. 联调与部署登录态、排查顺序和两个高频翻车点4.1 登录态怎么处理JWT 还是 Session选一个能说清理由的小酒店预约系统的登录不建议上来就上 Spring Security 全家桶学习成本抵消不了收益。常见做法是两种Session 存登录态或者 JWT 无状态令牌。管理端给前台工作人员用、人数少、就一台服务器Session 加拦截器足够以后要做小程序或对接多个端JWT 更省事。JWT 的做法是登录成功后签发 token前端存在 localStorageaxios 请求拦截器里带上request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config })难点不在签发而在过期策略。预约系统订单状态改动不频繁token 有效期设 2 小时、过期后强制重新登录即可不必为了体验去实现 refresh token 双令牌机制。提示无论 Session 还是 JWT前端都要在收到 401 时统一跳回登录页不然会出现“接口全报错但页面还停在订单列表”的假死状态。4.2 联调阶段按这个顺序查能省一半时间前后端联调报错90% 可以用固定顺序排查先看请求有没有发出、再看是不是 404、再看是不是跨域、最后看数据格式对不对。现象先查什么常见根因请求 404F12 Network 里的实际 URL后端 RequestMapping 路径与前端不一致CORS 报错devServer 转发是否生效改了 vue.config.js 没重启或 nginx 没配置转发时间格式异常后端返回的 JSON 日期字符串LocalDateTime 默认 ISO 格式前端没格式化具体到操作看 Network 面板里请求 URL 是不是http://localhost:8081/api/room/list路径不对就改前端拼接如果是 404多半是后端RequestMapping写成了/rooms而前端请求/room如果请求成功但页面显示 NaN 或时间格式怪异查后端返回的日期字段——SpringBoot 序列化LocalDate是2024-05-10LocalDateTime是带 T 的 ISO 格式前端展示中文日期要先dayjs(...).format(YYYY-MM-DD HH:mm)。4.3 springboot 版本太高和 vue 打包后布局异常这两个坑几乎每个做这个系统的都会遇到一次。springboot 版本太高的典型症状建项目时选了 3.x但本地 JDK 是 8项目启动直接报Error: A JNI error has occurred或 jackson 相关类找不到。解决办法就两条路把pom.xml里 parent 版本改成 2.7.x 的最新补丁版或者装 JDK 17 并在 IDEA 里把 Project Structure 和 Maven runner 的 JDK 都切过去。改版本时连带检查 MyBatis-Plus 的依赖坐标mybatis-plus-spring-boot3-starter是给 3.x 用的别混进 2.7.x 项目里。vue 打包后布局异常是另一个高发问题。现象是npm run build之后图片加载不出、路由刷新 404、CSS 错乱。根因大多是两个vue.config.js没配publicPath默认/部署在子路径下资源全部 404createWebHistory需要服务器配合把前端路由重写回index.html。生产环境 nginx 的标准配置server { listen 80; server_name hotel.example.com; root /opt/hotel-front/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html是 Vue history 路由的生命线用户直接访问/orders时 Nginx 找不到这个文件就会回退到index.html由 Vue Router 接管/api单独转发给 SpringBoot这才是真正的前后端分离生产部署形态。5. 收尾技巧springboot 定时任务配合唯一索引守住“同一房间同一时间段”5.1 唯一索引做最后防线预约系统的核心矛盾是并发。两个用户同时看到大床房 301 有空同时点了下单如果只靠“先查后插”两个请求都能通过查询校验结果是同一晚被订了两次。解决办法是在数据库层加硬约束CREATE TABLE tb_room_lock ( id BIGINT AUTO_INCREMENT PRIMARY KEY, room_id BIGINT NOT NULL, lock_date DATE NOT NULL, reservation_id BIGINT NULL, UNIQUE KEY uk_room_date (room_id, lock_date) ) COMMENT 房间按晚锁表;uk_room_date保证了同一个房间同一天只能有一条锁定记录。订单创建时按晚逐条插入任何一晚触发DuplicateKeyException整个事务回滚这就是 2.3 节代码里 try-catch 的意义。业务系统里常有人问“乐观锁版本号和唯一索引哪个好”我的观点是乐观锁解决的是“更新丢失”唯一索引解决的是“约束违背”场景不同。预约这种场景唯一索引更可靠因为它不依赖业务代码有没有写对数据库本身就会拒绝非法数据。5.2 待支付订单超时未付定时任务释放锁锁记录会一直在用户下了单不付钱房间就永远被占着。需要一个定时任务把超时未支付订单取消并删掉对应锁记录Component public class ReservationTimeoutTask { private final ReservationMapper reservationMapper; private final RoomLockMapper roomLockMapper; Scheduled(fixedDelay 300000) Transactional(rollbackFor Exception.class) public void releaseTimeoutReservations() { // 1. 找出创建时间超过 15 分钟且状态为 0待支付的订单 LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListReservation expired reservationMapper.selectList( new LambdaQueryWrapperReservation() .eq(Reservation::getStatus, 0) .lt(Reservation::getCreateTime, deadline)); if (expired.size() 0) { return; } // 2. 订单状态置为 4已取消 Reservation update new Reservation(); update.setStatus(4); reservationMapper.update(update, new LambdaQueryWrapperReservation() .eq(Reservation::getStatus, 0) .lt(Reservation::getCreateTime, deadline)); // 3. 删除这些订单占用的锁记录 for (Reservation r : expired) { roomLockMapper.delete(new LambdaQueryWrapperRoomLock() .eq(RoomLock::getReservationId, r.getId())); } } }启动类上加EnableScheduling才能让Scheduled生效。fixedDelay 300000表示上一次任务执行完再过 5 分钟跑下一次适合这种低频率清理如果改成 cron 表达式设成凌晨两点跑15 分钟超时的订单最长会被占一整天体验太差。定时任务三步必须在一个事务里改状态、删锁、记日志任何一步失败都要回滚。5.3 验证这套兜底确实有效不依赖前端页面直接用 curl 模拟并发。连续发起两次同样的预订请求curl -X POST http://localhost:8080/api/reservation \ -H Content-Type: application/json \ -d {roomId:1,customerName:张三,customerPhone:13800000000,checkInDate:2024-06-01,checkOutDate:2024-06-03}第二次请求应当返回“该房间在 2024-06-01 已被预约”。再进数据库查两张表SELECT * FROM tb_room_lock WHERE room_id 1 AND lock_date BETWEEN 2024-06-01 AND 2024-06-02; SELECT id, status, create_time FROM tb_reservation WHERE room_id 1 ORDER BY create_time DESC;正常情况是锁表只有一条记录对应成功订单另一条走到DuplicateKeyException分支被完整回滚如果锁表出现重复记录说明唯一索引没建上回查SHOW INDEX FROM tb_room_lock。定时任务的验证更简单把订单状态置为待支付并把create_time改成 20 分钟前等下一个调度周期后确认订单变成已取消、锁记录被清掉。把清理逻辑和下单逻辑放在同一个RoomLockMapper上操作时记得锁表删除条件必须带reservation_id只按room_id lock_date删会误伤其他正常订单的锁这是定时任务最容易写出的隐性事故。本文还有配套的精品资源点击获取
返回列表