做酒店预订系统这个选题的人很多,但真正能把Spring Boot加Vue这套前后端分离玩明白的少。网上各种开源项目代码一大把,但多数缺文档、缺数据库脚本,或者前后端逻辑混乱,买回来也跑不起来。如果你正在找毕设项目,或者想拿一个完整的全栈项目练手,基于Spring Boot加Vue的酒店预订系统可以算是一个性价比很高的选择。它覆盖面广,从前端页面交互到后端权限控制、订单状态流转、数据库表设计都有涉及,难度适中,不至于像电商秒杀那种高并发互联网级系统一样让人打退堂鼓,但也绝不是简单的CRUD堆砌。我这次就结合自己开发和部署这套系统时的实际经历,把整体拆一遍,包括设计思路、关键代码实现、数据库表结构、踩坑记录,都会聊到。
这套系统我实际开发过一版,前后端分离,后端用Spring Boot 2.7,前端用Vue 3加Element Plus,数据库用的MySQL 8.0。跑下来功能完整,包含用户注册登录、房间展示与搜索、预订下单、订单管理、后台管理。如果你是Java后端或者前端方向的学生,拿这套系统作为实战项目,面试时也能讲清楚不少点。下面我从头开始拆解。
1. 项目整体设计与需求拆解
1.1 项目定位与功能范围
酒店预订系统本质上属于轻量级交易系统。用户进入网站,查看酒店房间信息,选好日期后提交订单,管理员在后台维护房间、处理订单。它不涉及支付系统和复杂库存管理,所以不会像电商系统那样需要极高并发。但是它的核心业务逻辑,尤其是“房间-日期-订单”的关联,比普通增删改查更有意思,这也是面试官容易追问的地方。
从角色划分来看,系统一般有用户和管理员两种角色。用户端面向C端,重点在房间搜索、预订流程、订单状态查询上;管理端面向内部人员,重点是房源信息管理、订单审核、统计和分析。这样拆开后,前后端分工就很清晰了。
我对这个项目的需求拆解如下:前端需要用户端和管理端两个界面,用户端要有首页、房间列表、房间详情、提交订单、个人中心、订单列表等页面;管理端要有登录、房间管理、订单管理、用户管理、数据统计等页面。后端则需要提供对应的REST API,统一返回结果格式,支持跨域访问,带Token鉴权。
1.2 核心业务流程梳理
预订这个动作是整个项目的命脉。一套完整的预定流程是:用户选择入住日期和离店日期,系统按日期筛选可预订的房间,用户确认房间信息后填写订单,订单创建成功后状态为“待确认”或“已预订”,管理员在后台可以确认订单,用户入住后退房,订单状态改为“已完成”。
这里容易踩的坑是用“仅看房间库存总量”来判断是否可以预订。酒店房间不是汉堡库存,卖完一个算一个。房间是离散资源,同一房间在同一个晚上只能被一张订单占用。所以预订判断的关键粒度是“房间在某个日期是否已被占用”,而不是“还有几个房间类型空余”。
我第一次做的时候把房间类型设计了stock字段,后来发现完全不靠谱,因为一个房型下有多间实际房间,比如“标准间”有5间房,库存减1,用户A预订了2晚,第二天用户B想预订其中1晚就可能冲突。真正稳妥的做法是房间表记录具体房号,日期表记录每个房间每天的占用状态,订单与房间关联。后面数据库设计我会详细讲。
2. 技术选型与架构思路
2.1 为什么是Spring Boot加Vue
后端选择Spring Boot,主要看中它生态成熟、起步快。Spring Boot把Spring家族的配置自动化,依赖管理、内置Tomcat、无侵入式部署,对做项目开发来说是省心事。而且Java方向就业市场长期稳定,用它作为后端技术栈,对毕设或面试都比较安全。相比之下,如果选Python Flask或Node.js,也不是不行,但在很多高校的考核口径里不如Spring Boot“正统”。
前端选Vue,是因为Vue的上手曲线比React平缓。Vue 3的组合式API配合Element Plus组件库,后端开发者也能快速写出能看的后台。酒店预订系统的前端交互不算复杂,Vue的响应式数据绑定和路由机制足够胜任。如果你的项目要求必须用Vue 2,也可以,但新项目我建议直接用Vue 3,生态更健康。
这套项目我采用的是单体后端加单页前端的标准结构,后端只提供API,前端通过axios调用。没有引入Spring Cloud、Redis、消息队列这些重组件,因为一个酒店预订系统用不上,强行引入反而会让系统的复杂度失控,复杂度就是这么回事,功能在什么地方,复杂度就在什么地方,引入不必要的中间件就是给自己挖坑。
2.2 后端技术栈细节
后端依赖选择方面,我用了这几样:
- Spring Boot 2.7.x(稳定,适配JDK 8或JDK 11,部署兼容性好)
- MyBatis-Plus 3.5.x(单表CRUD非常省事,配合条件构造器写动态查询很顺手)
- MySQL 8.0(数据存储,支持JSON字段和更好的日期处理)
- JWT(用户登录后签发Token,无状态认证)
- Lombok(减少实体类getter/setter样板代码,但注意团队开发时需要确认IDE装好插件)
- Hutool(工具库,生成随机编号、日期格式化、加密处理,挺实用)
Spring Boot 3.x也已经发布很久了,但我还是选了2.7,别的不说,JDK版本没有8的限制,部署在老旧服务器上也方便,而且网上资料最多。真出了问题也好搜解决方案。
2.3 前端技术栈细节
前端用Vue 3加Vite构建,组件库Element Plus,路由管理用Vue Router 4,状态管理用Pinia(Vuex在Vue3里也行,但Pinia更轻量)。HTTP库直接用axios,统一封装请求实例,处理好Token注入和响应拦截。
这里需要强调一点,Vue 3的项目结构一定要在开发前定好。pages或views按用户端和管理端分开,router里做权限判断,api目录按模块拆分,components里放公共组件。如果把这层结构搭好,后面写页面会快很多,否则代码会越写越乱。
3. 数据库设计与核心表解析
3.1 表结构设计思路
数据库设计直接决定业务逻辑难易程度。我的表结构一共5张核心表:用户表、房间表、房型表、订单表、订单房间明细表。附属的还有管理员表(或直接在用户表里用角色字段区分,二选一)。我选择不单独建管理员表,统一放用户表,加一个role字段,用0表示普通用户,1表示管理员,这样登录接口可以共用,后台再判断角色即可。
表设计最重要的一个易错点,就是房间和订单的关系。一张订单可以包含多个或一个房间;一个房间在某个时间段只能被一个有效订单占用。我的思路是订单表存储订单基本信息(订单编号、用户ID、总金额、状态、入住日期、离店日期),订单明细表存储订单下每个房间的预订信息(订单ID、房间ID、房间名、价格)。那么判断房间是否可预订时,就要通过订单明细表和订单表联查,找到存在重叠日期的单子,如果有则冲突。
3.2 关键字段说明
用户表字段比较常规:用户ID、用户名、密码(加密存储)、手机号、邮箱、角色、注册时间、状态。密码我用的BCrypt加密,这个后面讲。
房间表最重要的是房间编号、房间名称、所属房型ID、楼层、面积、可住人数、状态(启用/禁用)。很多初学者把房间表设计成“每种房型一个记录”,这是逻辑谬误。一个真实酒店里,标准间有10间,你就得有10条房间记录,而不是1条。房间和房型是“多对一”的关系。
房型表字段包括:房型ID、房型名称、床型(大床/双床)、挂牌价格、原始价格、房间面积、设施服务(可存JSON)、图片URL、描述。价格放在房型表即可,房间不单设价格,因为同一房型的挂牌价是一致的。
订单表字段包括:订单ID、订单编号(业务唯一的随机串)、用户ID、订单总金额、状态、入住日期、离店日期、入住人姓名、联系电话、备注、下单时间、支付状态(如果没做支付,可以省略但建议保留字段)。订单状态我设定为:0待确认、1已确认/预订成功、2已入住、3已完成、4已取消、5已拒绝。这样管理端操作起来清晰。
订单状态是重点中的重点。有些系统就把状态存成“未支付/已支付/已完成”,但酒店预订是线下到店付,状态要能反映“预订-确认-入住-离店”的完整过程。我见过有人用boolean is_pay去控制一切,结果一个订单既可以说“已支付”又可能“已取消”,逻辑成了浆糊。建议明确一条状态流转线,然后在代码里严格控制合法性。
3.3 日期重叠的判断SQL
判断房间是否可预订是整个项目的核心查询之一。我简化一下核心逻辑:对某个房间,查询有效订单(状态不是已取消和已拒绝)中,是否存在入住日期小于传入的离店日期并且离店日期大于传入的入住日期,有兴趣的可以画一条时间轴,这就是标准的区间重叠条件。
对应的MyBatis-Plus写法可以这样表达:
SELECT count(*) FROM t_order o JOIN t_order_room r ON o.id = r.order_id WHERE r.room_id = #{roomId} AND o.status NOT IN (4, 5) AND o.check_in_date < #{checkOut} AND o.check_out_date > #{checkIn}如果查出数量大于0,就证明这个房间在所选日期内已被占用。这里的边界处理很讲究:入住当天和离店当天不算重叠。假设入住是5月1日,离店是5月3日,那5月3日当天房间应该释放给下一位客人,所以条件用<和>而不是<=和>=。这个问题不搞清楚,很容易出现明明退房了,下一个客人却订不了的怪事。
4. 后端核心实现要点
4.1 统一返回结果与全局异常处理
后端接口开发的第一步不是写业务,而是先定义返回格式。我习惯用一个Result类,包含code、message、data三个字段。成功时code=200,业务失败时code=500或自定义状态码,未登录时code=401,无权限时code=403。这样前端axios拦截器只需判断code,不再各自根据后端response结构处理。
全局异常处理也很关键。用@RestControllerAdvice加上@ExceptionHandler,可以把校验异常、业务异常、未知异常统一处理,不至于让用户看到一堆堆栈。我在项目里自定义了一个BusinessException,任何业务不满足时直接抛出,后端代码不用到处try/catch,整体干净很多。
统一返回格式不是花架子,它能减少前后端沟通成本。后端返回时间格式、字段命名都要统一,这里我强烈建议后端返回值直接用驼峰命名(Java默认就是),前端不做额外转换,省得来回改。
4.2 用户登录与JWT权限控制
登录接口的流程是:接收用户名和密码,查用户,比对密码,生成Token,返回前端。密码使用BCrypt加密,BCryptPasswordEncoder在Spring Security里就有,即使你不引入Security也可以直接用这个类。为什么要用BCrypt而不是MD5?因为MD5撞库风险太大,彩虹表一查一个准,BCrypt里的加盐机制让相同密码生成的密文都不同,安全性能明显高一个档次。
Token我用的JWT(JSON Web Token),结构上包含header、payload、signature。生成时把userId和role放进payload,过期时间设为24小时。前端每次请求在Header里带Authorization: Bearer token,后端用拦截器或过滤器解析。拦截器里我排除掉登录、注册、房间列表这些公开接口,其他接口全部校验。
注意一个坑:Token里不建议放敏感信息,例如手机号、密码,因为payload是Base64编码,任何人能解码看内容。但放userId和role没关系,这两者不敏感。校验时从Redis读用户状态这种方式,如果不需要主动踢人下线的功能,可省略。
4.3 订单创建的幂等性与并发控制
订单创建接口是核心中的核心。最怕什么?两个用户在同一秒订同一间房,结果都创建成功了。注意这里说的同一秒并不绝对,是指重叠日期内的并发请求。不加控制的话,两个请求同时通过了“是否可预订”的检查,然后同时插入订单,这个房间就被超卖了。
解决思路有三种:第一种是数据库行锁,先SELECT ... FOR UPDATE锁住房间记录,再检查订单,但这个锁粒度太大,影响性能;第二种是使用乐观锁,在房间表加version字段,更新时检查版本号,但实现起来要处理好更新失败后的重试;第三种是直接在订单创建接口上使用分布式锁或synchronized,但单机部署用synchronized还行,分布式部署就不行了。对于一个教学级项目,我建议在MySQL层面做约束,通过事务加SELECT ... FOR UPDATE锁住房间记录,再用unique索引或具体业务查询来兜底。
实际项目中,更优雅的方式是在订单明细表建立唯一索引(room_id, order_start_date, order_end_date),但这里存在一个问题,日期是区间不是单点,索引无法直接覆盖重叠范围。所以还是得老老实实加事务。我实现时在创建订单的方法上标注@Transactional,查询时用FOR UPDATE锁定选中的房间行,这样第二个并发请求会被阻塞,直到第一个事务提交。这样最简单,也最不容易出错。如果你的系统要求高并发,这些方法都要升级,但对于酒店预订这个量级,足够用了。
4.4 房间搜索与动态SQL
房间列表通常需要按条件筛选:入住日期、离店日期、人数、关键字等。这个接口用MyBatis-Plus的动态查询很方便,不需要自己手动拼接SQL。核心逻辑是先用日期条件查询出“已经占用的房间ID集合”,然后在房间表查询时加上NOT IN条件,排除掉不可用的房间。注意有些同学会把房间状态enabled和可用性混为一谈,这是两回事。房间禁用是管理员主动下架,跟日期占用无关,这两个条件要同时判断。
日期不传时,默认查所有开启状态的房间;日期传了,则排除重叠订单占用的房间。这个逻辑清楚了,前端搜索功能就是一次接口调用,而不是把所有房间全传给前端让前端过滤,这是一个不少初学者爱犯的错误。
5. 前端Vue与交互实现
5.1 登录注册页面与状态管理
前端用户端和管理端登录可以共用一个登录组件,提交时调用同一个登录接口,后端根据角色决定返回后跳转到哪个首页。Token存在localStorage里。Pinia里定义一个userStore,维护token、userInfo、role等状态,初始化时从localStorage读取,这样页面刷新后登录状态还在。
值得注意的是,Pinia中的state不要永远存一份固定的初始值,在loginaction里调接口成功后赋值,再调用一个router.push跳转。我习惯在登录成功后请求一次用户信息接口,拿到昵称和角色。这样比从登录接口里直接返回一大串信息要稳,因为用户信息可能后续会变。
5.2 axios封装与路由守卫
axios封装算是前后端分离项目的标配。我在src/utils/request.js里创建axios实例,设baseURL: '/api',这样开发环境由Vite代理转发,生产环境由Nginx转发。请求拦截器里,如果存在Token,加上Authorization头。响应拦截器里,统一判断res.data.code,如果是401,清除本地Token并跳转登录页。
路由守卫方面,Vue Router的beforeEach函数里,判断目标路由的meta.requiresAuth和meta.requiresAdmin。未登录用户访问需要登录的页面就跳转/login,普通用户访问管理端页面就跳转/403或首页。这里一个小技巧是,不要在守卫里写太多业务判断,把权限校验逻辑抽成一个函数或一个store方法,后面维护起来才清晰。
5.3 房间选择与订单提交链路
用户端最关键流程是“选房-下单”。我的页面设计是,首页推荐房型卡片,点击进去详情页,详情页中有日期选择器。用户选择入住和离店日期后,点击“查询可用房间”,前端调用后端房间搜索接口,得到当前日期下可预订的房间列表,用户选一间,点击“立即预订”。如果未登录,前端先跳转登录页;登录成功后,再回到预订页面,通过URL参数带上房型和日期信息。
这里很多用户会直接把所有日期和房型数据放在全局状态里,我觉得大可不必。URL就够用,还支持刷新页面后状态恢复。订单提交时,把roomId、checkInDate、checkOutDate、contactName、contactPhone、remark传给后端。后端计算出总价返回订单编号,前端跳转到订单详情页展示“订单提交成功,等待确认”。
下单成功后的反馈很关键,一定要在订单详情页写明当前状态,比如“待确认”,并给出预计确认时间。我最开始没设计这个页面,导致用户下单后以为自己预订成功,结果管理员根本没看到,这个体验非常不好。
5.4 管理端页面开发经验
管理端页面开发比用户端简单,主要是数据表格和表单。房间管理页面,左侧房型列表,右侧房间表格。新增房间时选择房型,填写房间号、楼层等信息。订单管理页面用表格展示所有订单,订单列表要支持按状态筛选,管理员可以点击“确认”或“拒绝”按钮,操作前需要弹窗确认。
写管理端时有一个效率提升技巧,Element Plus的表格配合pagination非常顺手,但不要每个页面都复制一遍分页逻辑。我把通用分页逻辑封装成usePagination组合式函数,传入请求函数和参数,返回列表数据和分页对象。这样房间、订单、用户三个列表都复用这一套,改动时只动一处。
6. 系统联调与部署落地
6.1 开发环境联调注意事项
前后端联调是很多团队项目的痛点。后端默认端口8080,前端开发服务器5173(Vite默认端口),跨域问题必须解决。有两种办法,一种是在后端加@CrossOrigin或配置全局CorsFilter,另一种是利用前端开发代理。我推荐后者:在vite.config.js里配置server.proxy,将/api开头的请求代理到http://localhost:8080,这样后端的接口地址统一不带/api前缀,前端代码里的相对路径是/api/user/login,实际上请求被代理转发到了http://localhost:8080/user/login。
联调时常见的问题就是日期格式不一致。前端传2026-05-01,后端接收时如果使用Date类型并且配合@JsonFormat,一定要保证格式一致。我这里统一使用字符串yyyy-MM-dd传输,后端实体里对应的字段也用String类型接收,然后解析成LocalDate再处理。别看这个问题小,它能花掉你大半天时间来排查。
6.2 前端打包与后端部署
前端生产环境打包命令是npm run build,生成dist目录。部署时最常见的方案是使用Nginx托管前端,同时配置反向代理/api到后端。也可以将dist目录里的静态文件拷贝到Spring Boot的resources/static目录下,打包成单一可执行Jar,但这样不推荐,因为每次改前端都要重新打包后端,热更新麻烦,而且资源混合后缓存控制也不方便。
我实际部署时用的Nginx配置简化如下:
server { listen 80; server_name your-domain.com; location / { root /opt/hotel-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意try_files那行很关键,因为Vue路由是history模式,刷新页面时如果Nginx不重新指向index.html,就会出现404。部署后端时直接用java -jar hotel-server.jar,生产环境可以使用nohup后台运行,或者写一个systemd服务文件来管理,后者更方便看日志和自动重启。数据库初始化直接用数据库脚本,MySQL执行时注意字符集设为utf8mb4,否则中文可能出现乱码。
6.3 上线前必做的检查项
部署上线不是代码能跑就行。我每次交付前都会走一遍检查清单:
- 数据库连接是否区分了开发环境和生产环境,建议用
application-prod.yml配置生产环境,运行参数指定--spring.profiles.active=prod - 密码是否都是BCrypt加密后的值,用户表里不能有明文密码
- 前端请求地址是不是通过环境变量区分,不能写死localhost
- Nginx是否配置了Gzip压缩,图片资源是否做了压缩
- 后端日志有没有记录异常栈,生产环境有哪些异常被吞掉了
这些问题不解决,项目看上去是好的,但一旦别人接手或者正式跑起来,就会有一堆暗坑。
7. 常见问题与排查技巧实录
7.1 房间明明存在,搜索却查不出来
这个问题的典型原因是查询房间时加了状态为启用,但房间数据初始化时enabled字段是0或NULL。另一个原因可能是日期参数没传,前端却默认给了某段日期,而后端这段日期范围内的房间确实已全部被占用。排查时可以先用数据库客户端直接执行SQL,确认房间表和订单表里的数据,再看后端的调试日志,这种问题大概率是数据问题而不是代码逻辑问题。
调试SQL日志必须打开,我一般在开发环境设置mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl,来看具体执行的SQL和参数。这样哪怕SQL有误,也能一下定位。
7.2 订单状态乱跳
订单状态乱跳这种情况,大多是后端更新状态时没有判断前置状态。比如“已完成”和“已取消”两个操作同时发起,就可能出现逻辑错误。解决办法是在更新状态的Service层加校验,写一个状态流转Map,定义0只能到1或4,1只能到2或4等。更新时使用带条件的SQL:UPDATE t_order SET status = #{newStatus} WHERE id = #{id} AND status = #{expectStatus},如果更新行数为0,说明状态已变更,直接提示用户刷新后再操作。这是典型乐观锁思路,简单高效。
7.3 跨域请求一直报错
跨域问题一般有两种表现:一种是请求能发出去,但浏览器控制台报CORS错误;另一种是OPTIONS预检请求直接404。第一种情况往往是后端CORS配置不对;第二种是后端没有处理预检请求。如果你用前端代理方案,开发环境不应该出现跨域问题;如果使用生产环境Nginx转发,浏览器请求的地址是同源的,Nginx再把请求转发到后端,这个场景下后端不需要开启CORS。如果后端同时支持多个来源,则要精确配置allowedOriginPatterns,不要用*,因为携带凭证时不允许通配符。
7.4 MyBatis-Plus分页查不出总数
分页失效基本是插件没配置。MyBatis-Plus 3.5之后,分页插件需要手动配置一个MybatisPlusInterceptor,在配置类里添加PaginationInnerInterceptor,没有这个插件,分页查询会返回全部数据。再一个容易忽略的是,分页对象Page必须作为第一个参数传入mapper方法,否则插件不识别。这个是新手最容易遇到的坑,报错还不会很显眼,要留心。
7.5 部署后静态资源404
如果后端直接挂前端dist目录,需要改Spring Boot静态资源路径,默认的classpath:/static/可以识别resources/static下的文件。但如果你把dist直接放在项目根目录下,路径就不同了,需要在配置类里重写addResourceHandlers。大部分时候我建议用Nginx处理静态资源,绕过Spring Boot,省心。
8. 写在最后的个人体会
开发这套系统最大的收获不在于代码量,而在于想清楚了“房间和订单之间到底应该如何建模”。前端Vue每个页面都写一遍页面跳转,后面就会意识到路由统一管理的重要性。后端每个接口都返回不同格式,联调时就会被前端同事指着鼻子骂。很多问题只有自己写完一个完整项目才明白为什么那些标准规范是有道理的。
我再分享一个小技巧:很多人在开发完酒店预订系统之后,就把它当成结束,其实不是。你完全可以在现有基础上扩展几个很加分的小功能,比如基于日期价格的日历报价、房间图片轮播、订单消息提醒、基于ECharts的月度营收统计。这些功能都不算复杂,但对项目的完整度和面试谈资提升非常明显。我当初就是在搜索接口上加了个价格排序,从按默认顺序展示,升级成可以按价格、面积排序,面试时就能顺势聊到数据库索引和排序策略,效果不错。
这套系统如果每天真实订单量不大,根本不需要复杂优化。但如果你是把它当成学习项目,那就趁这个时机把JWT、事务隔离、日期冲突算法这些底层东西弄透彻,这些不会白学。酒店预订,说起来小,其实把多少人拦住的就是这点简单的业务逻辑,做起来你就会懂。