ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue酒店预订系统:从数据建模到并发库存的完整实践

SpringBoot+Vue酒店预订系统:从数据建模到并发库存的完整实践 1. 酒店预订系统的业务边界先想清楚做哪些事做这类管理系统最容易犯的错就是一上来就建表、敲代码。酒店预订系统看着简单无非是用户选房间、下单、管理员管房间和订单但真上手以后你会发现业务边界不划清楚后面每个环节都要返工。我这个基于SpringBoot Vue的酒店预订项目在动手写第一行代码之前花了整整一个晚上整理业务流程这一步的价值在后续开发中被反复验证了。先说这个系统划定的核心角色一共三类角色核心诉求普通用户游客/注册用户浏览酒店与房型、按日期查询可订房间、下单预订、查看/取消自己的订单酒店管理员维护酒店信息、维护房型与库存、查看订单列表、处理订单状态系统管理员用户管理、基础数据管理、统计报表围绕这三个角色我把功能拆成两条主线用户端注册登录 → 浏览酒店列表 → 酒店详情 → 房型列表 → 选入住日期 → 提交订单 → 我的订单 → 取消订单。管理端登录 → 酒店管理增删改查 → 房型管理增删改查 → 订单管理查看/确认/取消 → 用户管理。这里要特别说一个取舍支付环节建议做成到店支付/线下支付的占位状态而不是真的对接支付宝或微信。原因很简单——大多数课程设计和毕设场景不需要真实支付通道真实支付涉及商户资质、证书、回调验签很容易把项目的复杂度从管理系统拉高到金融系统的级别。订单状态里保留待支付/已支付字段即可方便后续扩展但不引入SDK。如果你之后想做真实的支付演示在这个字段的基础上再接微信/支付宝也不会伤筋动骨。另外一个容易被忽略的点是房型的库存语义。酒店系统里的库存不是普通电商的件数而是某一天可预订的房间数量。同一个房型比如大床房有5间7月1日被订了3间那7月1日就还剩2间可订7月2日被订了1间那7月2日还剩4间。这个按日期维度拆解库存的逻辑是整张数据表的灵魂后面第三节我会专门展开。提示如果你只是照着网上的代码抄很多酒店预订系统的库存其实是假的——要么只有总数没有日期维度要么订了之后根本不减库存。但是做项目的人得自己心里有数这块做不做得对直接决定你答辩或面试的时候能不能把系统讲得滴水不漏。2. 技术栈选型SpringBoot Vue组合到底是省心还是折腾这个项目取名为基于SpringBoot Vue的酒店预订系统不是拍脑袋定的。当前前后端分离的全栈项目里SpringBoot Vue确实是最稳的答案但稳是有前提的版本和配套工具没选对再好的组合也会变成灾难现场。2.1 后端选型SpringBoot 2.7 MyBatis PlusSpringBoot 2.7.x 是目前教程资源最多、兼容性最稳的版本。如果你不是对SpringBoot 3有明确需求比如必须用Jakarta EE新特性我建议别选3.x。原因很实际3.x要求JDK 17而很多学校机房、云服务器、老教程还停留在JDK 8到时候环境适配会浪费大量时间。我身边就有同学因为追新用了SpringBoot 3.2结果Maven仓库里某个依赖版本不兼容卡了整整两天。做项目稳定跑起来永远比版本新更重要。持久层我选 MyBatis Plus 而不是 JPA也不是原生 MyBatis理由比较务实单表 CRUD 直接用内置方法搞定省掉大量重复的 XML 映射文件分页插件用起来顺手管理端列表页必须要有分页不然数据多了页面直接卡死相比 JPA国内社区资料更丰富面试时也更好解释为什么用这个。2.2 前端选型Vue 2.7 Element UI前端这块我要泼一点冷水如果是为了稳妥跑通项目Vue 2.7 Element UI 比 Vue 3 Element Plus 更省心。不是说 Vue 3 不好而是市面上毕设教程、组件库资料、老项目源码Vue 2 生态的存量太大。你随便搜一个Vue酒店管理系统大概率是 Vue 2 的代码参考设计的难度天然更低。Vue 2.7 是官方最后支持 Vue 2 的版本它把 Composition API 也兼容进来了所以既可以用传统 options API也能用 setup 语法算是过渡期的甜点位。Element UI 的组件在酒店管理系统里完全够用表格、表单、日期选择器、分页、对话框这些管够。2.3 数据库与中间件的选配MySQL 8.0这个不用解释。配置上我建议字符集统一utf8mb4因为酒店名称、地址里可能出现特殊符号排序规则选utf8mb4_general_ci就够。缓存如果要做Redis 在 SpringBoot 里整合很顺但这个项目初期可以不加不然环境要求又多一项。原因很简单每次给别人部署时多一个中间件就多一个出问题的点。先跑通主流程Redis 作为后期优化的选项比作为前置条件更合理。提示整个项目最核心的依赖版本我用的这套组合是 Spring Boot 2.7.6 MyBatis Plus 3.5.3 JJWT 0.9.1 Hutool 5.8.x。这些版本我实测互相兼容不会有令人抓狂的 jar 包冲突。第一次跑项目的时候不要在尝鲜最新版上花时间先把流程跑通比什么都重要。3. 数据库设计订单冲突和库存问题的根子在表结构酒店预订系统最怕的事是什么同一间房、同一个日期段被两个用户同时订走。这个问题在代码层面很难完美解决必须靠数据库的根本性约束。我的表结构设计围绕唯一性和状态可追溯展开这也是整个项目含金量最高的部分。3.1 六张核心表的设计我建了6张核心表外加一些辅助表表名作用关键字段user用户表id, username, password, nickname, role, phone, status, avatarhotel酒店表id, name, city, address, stars, description, coverroom_type房型表id, hotel_id, room_name, price, area, max_people, bed_type, total_roomsroom_inventory房态库存表关键id, room_type_id, date, total_count, booked_count, versionorders订单表id, order_no, user_id, hotel_id, room_type_id, check_in_date, check_out_date, total_price, status, create_timesys_config系统配置表可选参数键值对比如是否开放注册很多网上的代码没有 room_inventory 这张表订单直接关联 room_type然后假模假式地减一个总库存。这种做法在演示视频里看不出问题但只要两个用户同时提交数据就错了。我把库存拆到房型 × 日期维度每一行代表该房型在某个具体日期还剩几间可订这是稳妥的做法。3.2 唯一约束与乐观锁防止重复预订的三道防线这是整个数据库设计的核心我说三件事订单表防重复预订给订单表建联合唯一索引uk_user_room_date (user_id, room_type_id, check_in_date, check_out_date)。意思是同一个用户同一房型同一入住离店日期只能有一条有效订单。第一次订成功后重复提交直接数据库报错应用层捕获后提示您已预订过该房型。这道防线把同一用户重复下单彻底堵死。库存日期的唯一性room_inventory表建联合唯一索引uk_room_date (room_type_id, date)同一房型同一天只有一条库存记录。更新时用乐观锁版本号解决超卖问题。订单状态机status 字段我定义成几个整型常量0待支付、1已确认、2已入住、3已退房、4已取消。不要把已取消设计成删数据保留记录才能追溯管理。管理端要根据状态做不同类型的操作比如已确认的订单才能点办理入住。关于并发处理的完整方案我在第4节专门展开因为这里涉及一个非常经典的超卖场景属于后端业务逻辑最容易踩坑的环节。3.3 初始化数据的小技巧数据库脚本里我预置了3座城市的5家酒店、每个酒店3到5个房型以及未来30天的库存记录。这里有个很实用的经验库存表不要手工一条条插入而是写一个脚本循环生成把从当前日期 1到当前日期 30的每一天针对每个房型都插入一条total_count 房间数的记录。我第一次做的时候笨手笨脚在 SQL 里复制粘贴结果日期错位排查了半天才发现是粘贴漏了一行。注意MySQL 的日期函数、循环可以用存储过程实现但更推荐你把初始化逻辑写进项目启动后的一个DataInitializerSpring Boot 的 CommandLineRunner这样数据库脚本只留表结构数据自动生成。别人部署的时候少一步手工操作你交付的数据库部分也更专业。4. 后端核心实现JWT鉴权、拦截器与订单状态流转后端代码我按经典分层来controller → service → mapper实体类用 MyBatis Plus 的注解标注表映射。写代码本身没什么玄学但有几个点很值得单独拎出来讲因为它们直接决定系统的安全性和数据正确性。4.1 登录与鉴权JWT 不只是发个token密码存储我用的是 BCrypt 加密Spring Security 的 BCryptPasswordEncoder或者 Hutool 里的 BCrypt 工具千万不要明文存密码——这是项目最基本的底线。如果数据库泄露明文密码等于把所有用户的账号安全都交出去了。JWT 我用了 JJWT 库签发的时候把 userId、username、role 存进 claims。这里有一个很多人容易忽略的坑JWT 一旦签发服务端无法主动让它失效。所以用户修改密码后旧 token 还能用这个问题在纯 JWT 方案里无解。我的处理方式是在 user 表加一个token_version字段用户修改密码时把它加 1JWT 里存这个版本号每次请求校验时对比数据库不一致就拒绝。这个方案轻量、有效比引入 Redis 黑名单简单得多很适合中小项目。拦截器用 Spring Boot 的 HandlerInterceptor校验逻辑就三步取 Header 里的 Authorization → 去掉 Bearer 前缀 → JWT 解析 → userId 查询用户状态 → 放行。管理端接口额外校验 role 字段这里的逻辑我在第5节路由守卫里还会再提一次。4.2 创建订单的完整业务链库存扣减的顺序不能乱创建订单时事务方法的执行顺序极其重要。我按下面的链条写1. 校验用户登录态从 token 拿 userId 2. 校验酒店、房型存在且状态正常 3. 校验入住日期晚于今天、离店日期晚于入住日期 4. 计算入住天数 check_out - check_in按天 5. 循环查询 [check_in, check_out) 每一天的房态库存 6. 如果每一天的可用数都 1继续否则抛出该日期段已被订完 7. 对每一天的库存行执行乐观锁更新UPDATE ... SET booked_count booked_count 1 WHERE version ? 8. 生成订单号、计算总价、插入订单表 9. 返回订单号给前端注意第5步日期区间是左闭右开的7月1日入住、7月3日离店实际占用的是7月1日和7月2日两晚7月3日那天的库存不动。这个算清楚住几晚的逻辑每隔一段时间就会出现一个写错的人你如果做项目务必自己拿日历推演一遍或者直接写单元测试验证。4.3 乐观锁更新超卖问题的解法与重试机制我在第5步做了查库存第7步做更新库存这中间必然有时间差。两个用户同时读到有房然后同时执行更新如果不用乐观锁就会把booked_count从 0 改成 1 两次——明明只剩一间却卖出两单。乐观锁的 SQL 语义是UPDATE room_inventory SET booked_count booked_count 1, version version 1 WHERE room_type_id ? AND date ? AND version ?如果影响行数为1说明抢到了影响行数为0说明 version 不匹配别人已经改过了。这时候要么重试要么直接返回无房。我把这个逻辑写成一个带重试的小组件最多重试3次3次都失败就返回友好提示。实测单机环境下这个方案足够稳。提示如果你做的不是课程设计而是真正高并发的线上系统乐观锁重试会有性能瓶颈那时候得上 Redis 分布式锁或者把库存预热到 Redis 做原子扣减。但作为这套项目数据库乐观锁已经是最稳妥、最好讲解的方案了。答辩的时候把这个为什么选乐观锁而不是悲观锁讲清楚是加分项。4.4 订单编号生成的讲究订单号我用了时间戳 随机数组合yyyyMMddHHmmss 6位随机数。如果嫌冲突概率高可以再加一个雪花算法生成器。这里有一个实际教训不要在事务里用System.currentTimeMillis()做唯一主键的组成部分并发高一点就可能撞号。简单的随机数虽然不能保证绝对唯一但加上数据库的唯一索引兜底业务上完全够用。5. Vue前端路由守卫、Axios封装与页面组织前端这块在别人眼里好像只是套页面但真正决定一个项目能不能顺利演示、答辩时能不能流畅操作前端骨架的合理性非常关键。我见过太多后端逻辑没问题、前端一操作就报错的项目问题大多出在请求封装和路由控制上。5.1 前端工程结构与依赖用 Vue CLI 创建项目vue create hotel-frontend依赖我控制在最小集避免装一堆用不上的包vue 2.7.xvue-router 3.xaxioselement-ui 2.15.x路由我分成两组用户端路由和管理端路由。用户端的页面有首页酒店列表、酒店详情含房型、订单提交、订单列表、登录/注册页管理端独立嵌套在/admin前缀下包含酒店管理、房型管理、订单管理、用户管理。页面组织上我习惯把通用组件比如酒店卡片、房型卡片、订单状态标签抽出来放components/页面组件放views/这样每个页面代码量不大维护起来也清爽。5.2 Axios 拦截器统一处理 token 和错误码axios 封装是我每次都要强调的一个点。很多初学者每个页面单独发请求token 失效了也不知道报错信息一堆乱码。我的做法是定义一个request.jsimport axios from axios import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带 token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理业务错误与登录失效 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { localStorage.removeItem(token) localStorage.removeItem(role) router.push(/login) return Promise.reject(new Error(登录已过期)) } else { return Promise.reject(new Error(res.msg || 请求失败)) } }, error { return Promise.reject(error) } ) export default service这样每个页面里只需要关心业务数据不用重复写错误处理。日期格式化、天数计算这类通用逻辑我也抽成了utils/date.js里的工具函数避免在订单提交页里堆一大坨代码。5.3 路由守卫管理端页面必须防裸进管理端的路由统一加了meta: { requiresAuth: true, role: admin }然后在全局前置守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role admin localStorage.getItem(role) ! admin) { next(/) return } next() })这一步很基础但很多参考源码根本没有。没有路由守卫的结果就是用户直接在地址栏输入/admin/hotel就能进到管理端页面的框架虽然拿不到真实数据后端有拦截器但前端体验和安全性都会显得很不专业。前后端双重校验是我做这个项目始终坚持的原则。5.4 日期选择与联调的坑前端做日期范围选择时Element UI 的el-date-picker在value-format没设置时返回的是时间戳或 Date 对象提交给后端前必须格式化。我在第一次联调的时候踩了个大坑——前端显示正常的日期一提交到后端全变成了2025-07-01T00:00:00.000Z这种带时区的 ISO 格式数据库里存进去直接偏移了8小时。后来统一用value-formatyyyy-MM-dd后端接收时用统一的JsonFormat注解问题才彻底消失。提示前后端分离项目跨域是必然要处理的。我在后端写了一个全局 CORS 配置类开发阶段允许所有来源如果正式部署用 Nginx 做/api反向代理那就不需要 CORS 了。开发阶段图省事可以直接用 CORS正式部署请走 Nginx。6. 让项目跑起来环境配置、数据库脚本与文档配套标题里写了源码数据库文档这套交付物不是拿来凑数的而是一个项目能不能被别人快速复现的关键。我按别人拿到项目 → 十分钟跑起来的目标来整理这也是整个项目完成度的重要体现。6.1 环境要求与启动步骤组件版本要求说明JDK1.8 或 11SpringBoot 2.7 兼容Maven3.6用 IDEA 自带的也行MySQL8.05.7 也可以注意字符集Node.js14建议 16Vue CLI 兼容启动步骤我写在 README 里按下面的顺序来执行db/hotel.sql初始化数据库修改后端application.yml里的数据库用户名密码IDEA 里启动HotelApplication.java端口默认 8080前端目录执行npm install然后npm run serve端口默认 8081浏览器访问http://localhost:8081。有一个特别容易折腾人的点npm install 时如果报依赖版本错误大概率是 Node 版本和 Vue CLI 的版本不匹配。建议 Node 用 16.x如果用的是 Node 18有些老项目的依赖会编译报错。这时候不要傻傻地去升级代码装个 nvm 切换 Node 版本更快。6.2 数据库脚本的排版规范很多参考源码的 SQL 脚本是一坨导出的Navicat 导出的文件动辄几千行夹杂着外键检查、字符集声明别人导入时眼花缭乱。我建议手动整理 SQL 脚本按清晰的顺序写-- 创建数据库 CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4; USE hotel_db; -- 用户表 DROP TABLE IF EXISTS user; CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, role tinyint DEFAULT 0 COMMENT 0用户 1管理员, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;每个表写清楚注释字段注释也写上。这种干净脚本比 Navicat 直接导出更能给看项目的人留下好印象你自己后续复盘代码也省心。更重要的是用DROP TABLE IF EXISTS保证脚本可以重复执行这一点在反复部署时非常有用。6.3 文档里最该写的内容我见过的很多项目文档就是简介 截图 代码贴一遍几乎没有参考价值。我写文档的时候固定了几个章节你可以直接参考项目概述一句话讲清楚系统做什么、给谁用技术栈说明后端、前端、数据库、版本号以及为什么选这些功能清单用户端/管理端一张表列完整谁登录能看到什么数据库设计说明每张表的作用、关键字段、状态字段的含义部署运行指南从零到跑起来的每一步包括截图核心业务与关键技术库存并发控制、JWT鉴权、日期区间计算这三个点最值得写测试账号admin/admin123管理员、user/user123普通用户方便别人直接进门体验。写完这些这个项目才真正称得上源码数据库文档三件套而不只是一堆能跑但不能看的代码。7. 三天联调的核心Bug复盘日期边界、库存不一致与时间戳陷阱最后这部分是我在联调过程中真实遇到的三个Bug每一个都值得你提前预防。这些问题用正常开发流程很难一次发现但一旦踩到排查时间都是按小时计的。7.1 日期边界Bug跨月与跨年金用户选择7月30日入住、8月2日离店日期循环如果从check_in到check_out用普通的 for 循环逐天加1月份切换时会变成7月31日、7月32日、7月33日——轻则查不到数据重则 MySQL 直接把非法日期当 0 值报错。正确做法是通过LocalDate.plusDays(1)逐天增加而不是自己对getDayOfMonth()加1。写日期计算的代码务必用 Java 8 的LocalDate而不是过时的Date和Calendar这点在跨月、跨年场景下特别重要。7.2 库存表没有初始化而导致的假满房第一次跑通时数据库里房型有数据但room_inventory是空的导致前端选任何日期都提示无房。排查了很久发现不是查询逻辑错而是根本没有库存记录。这个问题再次印证前面说的要么准备完整的初始化数据脚本要么写启动时自动生成数据的初始化器一定要把演示数据齐备当作交付标准的一部分。我当时把这个问题写成了一键初始化的CommandLineRunner后面每次重置数据都特别省事。7.3 时间戳时区的8小时陷阱这个是联调时的经典问题。MySQL 驱动连接串里如果没有serverTimezoneAsia/Shanghai并且前端传的时间还是带T和Z的 ISO 格式存进数据库的时间很容易差8小时。订单的入住日期一旦差了一天整个库存和订单就对不上——用户以为自己订的是7月2日实际数据库里是7月1日。解决方案要在三处同时下手前端用value-formatyyyy-MM-dd统一格式化后端application.yml里设置spring.jackson.date-format和time-zone: GMT8数据库连接串加serverTimezoneGMT%2B8。三处对齐日期就不会飘。我个人做这类全栈项目最大的体会是系统的技术难度并不高真正费时间的是业务边界的梳理、数据一致性的设计以及联调时那些差一点就看不出来的数据细节。如果你正在做酒店预订系统或者准备拿它作为毕设、课程项目不要急着写代码先把角色、流程、表和状态字段画清楚。数据模型稳了后面的代码只是把设计翻译成实现而已。
返回列表