不少朋友在 Spring Boot 项目实战练习时,最喜欢找酒店预订这类带完整业务闭环的系统来练手。这类系统麻雀虽小五脏俱全:前台展示、房间管理、订单流转、支付对接,几乎覆盖了企业级开发日常要用的所有核心环节。这篇博文我就拿一套springboot酒店在线预定系统来做个全面的技术复盘,从项目结构、数据库设计、核心功能实现,到部署打包、常见坑点排查,一次性把这类项目吃透。
这个项目源码编号是 00584,整体技术栈以 Spring Boot 为主,前端采用 Vue 前后端分离模式,非常适合正在学 Spring Boot、准备毕设或者想快速上手企业级项目做法的开发者。就算你已经工作了一段时间,也可以借这个项目梳理一下自己的技术框架,查漏补缺。
1. 项目整体设计与技术栈选型
1.1 为什么选择 Spring Boot 2.7.x 而不是 3.x
打开源码第一眼看的肯定是 pom.xml 里 Spring Boot 的版本。这套项目用的是 2.7.x 版本,很多刚接触的朋友看到这里可能会疑惑:现在最新版本都出到 3.x 了,为什么还用老版本?
这里面的痛点其实特别现实。Spring Boot 3.x 最低要求 JDK 17,而目前绝大多数企业的存量系统、云服务器环境还停留在 JDK 8 上。酒店行业尤其如此,很多中小型酒店的服务器是租的云主机,操作系统是 CentOS 7,装的 JDK 就是 1.8,你如果非要用 Spring Boot 3.x,光是把环境升级一遍就得折腾好几天。
另外,热词里频繁出现"springboot版本太高"、"springboot jdk1.8打包到docker desktop",说明这是大量开发者的真实痛点。Spring Boot 2.7.x 是 2.x 系列的最终版本,既保留了传统开发习惯,又兼容了 JDK 8 和 Jakarta EE 的过渡期,对老项目迁移和新人上手都非常友好。而代码里用的 MyBatis Plus 版本、Redis 客户端版本,也都是经过长期生产验证的稳定版。
这个项目的技术栈选型对照如下:
| 技术组件 | 版本/方案 | 选型理由 |
|---|---|---|
| JDK | 1.8 | 企业存量环境主流版本,云服务器兼容性最好 |
| Spring Boot | 2.7.x | 2.x 最终版,成熟稳定,兼容 JDK 8 |
| MyBatis Plus | 3.5.x | 单表 CRUD 无需写 SQL,开发效率高 |
| Redis | 2.6.x 客户端 | 缓存热点数据、管理端 Session 共享 |
| MySQL | 5.7 / 8.0 | 免费开源,酒店系统并发量完全够用 |
| Vue | 2.x + Element UI | 社区资料丰富,组件库成熟稳定 |
| Maven | 3.6+ | 依赖管理标准方案 |
1.2 前后端分离架构带来的核心优势
这套系统采用的是标准的 Spring Boot + Vue 前后端分离架构。后端只负责提供 RESTful API,前端通过 axios 发送异步请求拿到 JSON 数据渲染页面。
前后端分离最大的好处是开发职责清晰、并行效率高。后端开发只专注于接口、数据库、业务逻辑,不需要关心页面长什么样;前端开发也只管页面交互。两者通过提前约定好的接口文档对接,互相不阻塞。
而且分离架构天然适合多端复用。同一套后端 API,将来如果要出小程序端、手机 H5 端,甚至对接美团、携程的渠道,都能直接复用。热词里频繁出现"springboot vue前后端分离",就是这个架构在就业市场如此受欢迎的原因。
1.3 源码目录结构解读
拿到源码后不要急着双击运行,先把目录结构过一遍。正常的 Maven 项目结构分三层理解:
code复制hotel-booking-system
├── src/main/java/com/example/hotel
│ ├── controller # 控制层,接收前端请求
│ ├── service # 业务逻辑层,核心处理
│ ├── mapper # 数据访问层,MyBatis Plus 接口
│ ├── entity # 实体类,对应数据库表
│ ├── config # 配置类,跨域、拦截器、Redis 配置
│ ├── common # 公共类,统一返回结果、异常处理
│ └── utils # 工具类,日期处理、JWT 工具等
├── src/main/resources
│ ├── application.yml # 核心配置文件
│ ├── mapper # MyBatis XML 文件(复杂 SQL 用)
│ └── static # 静态资源
└── frontend # 前端 Vue 项目目录
很多新手拿到项目第一反应是从 controller 开始读,这是不对的。正确顺序是先看 application.yml,了解有哪些配置项、连的是什么数据库、Redis 地址、端口号,然后从 entity 看实体类了解数据模型,再看 mapper 了解数据操作,接着才是 service 看业务逻辑,最后才是 controller 看接口暴露。这个顺序和代码调用层级是反过来的,顺着数据流走才看得明白。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块设计与数据库建模
2.1 六个核心功能模块的拆解
酒店在线预定系统从功能上可以拆成六大模块:前台用户模块、房间信息模块、在线预订模块、订单管理模块、后台管理模块、数据统计模块。
前台用户模块主要负责用户注册、登录、个人信息维护。登录这块用了 JWT 做无状态认证,用户登录成功后返回一个 Token,前端存在浏览器里,后续请求在请求头里带上这个 Token,后端拦截器校验身份。这种方案比传统 Session 方案更适合前后端分离架构,也无缝支持了 Redis 做分布式系统时的会话共享。
房间信息模块是门户页的核心。用户通过日期选择器和房间类型筛选器查询可预订房间,后台根据条件实时计算房态。房间的每一笔状态变化都会记录在订单系统里,保证查询结果准确。
在线预订模块是整个系统的皇冠。用户选好房型、入住日期、离店日期,系统先锁定房间并生成待支付订单。订单如果在 15 分钟内未支付,系统自动释放房间资源。这里涉及的事务一致性、并发锁处理在后面章节详细讲。
订单管理模块是用户和酒店之间的联系纽带。用户端可以查看自己的预订记录、取消未入住的订单、退房后评价;管理端可以对过期未支付订单做强制取消、查看处理退款、办理入住核销和退房操作。
后台管理模块面向内部管理员,包括房型管理、房间管理、订单维护、用户管理、公告发布。权限部分做了简单的 RBAC 模型,管理员和普通用户的接口做了区分。
数据统计模块通过订单表、房间表的数据聚合,在管理端展示今日预订单量、入住率、营收趋势图。这些数据在酒店实际运营中很重要,能直观反映经营状况。
2.2 数据库表设计与核心字段
数据库设计是这类项目最见功力的部分。这套系统总共 12 张表,核心的三张表是用户表、房间表、预订订单表。这里挑重点的几张来说。
用户表 t_user 的核心字段包括主键 id、用户名 username、密码 password、手机号 phone、真实姓名 real_name、角色 role、创建时间 create_time。密码存储不能存明文,用的是 BCrypt 加盐哈希,即使数据库泄露,攻击者也很难反推出密码。
房间表 t_room 的核心字段包括主键 id、房间号 room_no、房型 room_type_id、床型 bed_type、楼层 floor、门市价 market_price、当前状态 status。重点说下状态字段,status 的取值范围是 AVAILABLE(可订)、RESERVED(已订未入住)、OCCUPIED(入住中)、CLEANING(打扫中)、MAINTENANCE(维修中),这就是酒店行业的"房态管理"。
预订订单表 t_reservation 是最核心的表。字段包括主键 id、订单编号 order_no、用户 ID user_id、房间 ID room_id、入住日期 check_in_date、离店日期 check_out_date、房间数 room_count、订单金额 total_amount、支付状态 pay_status、订单状态 order_status、支付时间 pay_time、创建时间 create_time。
订单状态这里设计了一个状态机,涉及 PENDING(待支付)、CONFIRMED(已确认)、CHECKED_IN(已入住)、CHECKED_OUT(已退房)、CANCELLED(已取消)、EXPIRED(已过期)。状态流转是严格的:只有 PENDING 才能变成 CONFIRMED 或 CANCELLED,只有 CONFIRMED 才能变成 CHECKED_IN,只有 CHECKED_IN 才能变成 CHECKED_OUT。这套状态机设计是后面所有业务判断的地基。
2.3 数据库索引与性能优化思路
订单表数据量增长很快,查询场景多。如果索引设计不好,数据量到几十万条时查询就会明显变慢。
订单表必须建复合索引,user_id + create_time,这样用户查询自己的订单列表走索引,不会全表扫描。状态查询也很高频,order_status + create_time 也建上。房间表上 status 字段要建索引,因为门户页每次都按状态过滤可订房间。room_type_id 字段建普通索引。
优化方面还有两招。一是热点数据缓存在 Redis 里,比如在门户页宣传的推荐房型列表、酒店的紧急公告,这类数据实时性要求不高,缓存 30 分钟没问题。二是 SQL 层面避免 SELECT *,只查询需要的字段,避免回表次数过多。
3. 从零搭建工程并跑通核心预订流程
3.1 项目环境准备与初始化
实际动手之前,确认环境清单里这几项是否就位:JDK 1.8、Maven 3.6+、MySQL 5.7 或 8.0、Redis 5.x 以上版本。如果本地还没装 Redis,Windows 用户可以直接下载 Windows 版本,Linux/Mac 用户用 brew install redis 或 apt install redis-server 安装都很快。
初始化步骤按下面顺序操作:
bash复制# 1. 创建数据库
mysql -u root -p
CREATE DATABASE hotel_booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
# 2. 导入源码提供的 SQL 脚本
mysql -u root -p hotel_booking < sql/hotel_booking.sql
# 3. 修改配置文件 application.yml 中的数据库连接、Redis 配置
这里特别提醒,SQL 文件里的建表语句和初始数据一定要完整执行,里面带了管理员账号、测试房型数据、演示订单数据。如果没有这些数据,前台页面会像一片白地,没有任何渲染效果。
配置文件 application.yml 核心段长这样:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/hotel_booking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password:
database: 0
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.hotel.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case 开启后,数据库字段 room_no 能自动映射到 Java 属性 roomNo,省去一堆手工映射代码。log-impl 建议开发阶段打开,能看到每一条执行的 SQL 和参数,排查问题事半功倍。
启动后端项目:打开 IDEA,导入 Maven 项目,等依赖下载完成后,直接运行主类里的 main 方法,控制台出现 "Started HotelApplication" 就说明后端启动成功。
启动前端项目:进入 frontend 目录,执行 npm install 安装依赖(这一步如果网络慢可以用淘宝镜像),然后 npm run dev 启动开发服务器。浏览器访问 http://localhost:8081 就能看到酒店官网的页面。
3.2 Maven 打包含依赖问题排查
很多同学跑后端项目时卡在 Maven 依赖这一关,原因基本就两类。一类是 Maven 仓库默认源在国外,国内网络拉取缓慢超时;另一类是本地仓库缺了某个特定版本的依赖。解决方案是在 Maven 的 settings.xml 中配置阿里云镜像:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
配置完后,在 IDEA 的 Maven 面板点刷新按钮重新导入。如果某个依赖还是下载不了,去仓库目录删掉对应文件夹,再重新导入。
3.3 登录接口的前后端联调实战
以用户登录为例,走一遍完整的前后端联调流程。后端 UserController 中暴露的登录接口:
java复制@PostMapping("/api/auth/login")
public Result login(@RequestBody LoginDTO loginDTO) {
// 1. 校验参数非空
// 2. 根据用户名查询用户
// 3. BCrypt 校验密码是否匹配
// 4. 生成 JWT Token
// 5. 返回用户信息和 Token
}
前端 api/user.js 中的请求封装大概这样:
javascript复制export function login(data) {
return request({
url: '/api/auth/login',
method: 'post',
data
})
}
联调时最容易出问题的是网络请求跨域。Spring Boot 项目在 config 包里通常有个 CorsConfig 配置类,允许指定来源的前端地址跨域访问后端接口。如果前端控制台报 CORS policy 错误,就去检查这个配置里的 allowedOrigins 是否正确。
另一个常见问题是 JWT 的密钥配置前后端约定不一致,或者 Token 过期时间设置太短。如果登录成功后请求其他接口一直报 401,优先检查后端日志输出的校验异常信息,然后看 Token 是不是已经被前端存进 localStorage 且请求拦截器正确添加了 Authorization 请求头。
3.4 核心预订业务代码的逐步解析
预订功能最核心的逻辑在 ReservationService 中的 createReservation 方法。完整逻辑链包括:校验参数(入住日期不能晚于离店日期、不能是过去日期)、查询对应房间是否存在且状态为可订、计算订单金额、创建状态为 PENDING 的订单、修改房间状态为 RESERVED、删除该房间的 Redis 缓存。
这里最关键的是"校验房间状态+创建订单+修改房间状态"这三步必须是一个原子操作,不然高并发场景下会发生超卖问题。用代码表示大概是:
java复制@Transactional(rollbackFor = Exception.class)
public Reservation createReservation(ReservationRequest request) {
// 加锁防止并发
Room room = roomMapper.selectByIdForUpdate(request.getRoomId());
if (room == null || !"AVAILABLE".equals(room.getStatus())) {
throw new BusinessException("房间不可预订");
}
// 计算金额
BigDecimal totalAmount = calculateAmount(room, request);
// 创建订单
Reservation reservation = new Reservation();
// ... 设置字段
// 更新房态
room.setStatus("RESERVED");
roomMapper.updateById(room);
return reservation;
}
selectByIdForUpdate 是悲观锁方案,查询时直接锁定数据库行,直到事务提交或回滚才释放。对酒店这类并发量不高的业务场景,悲观锁是最稳妥的做法,虽然性能不是最优,但绝对不出错。
3.5 超时未支付自动取消订单的实现
订单创建后如果用户一直不支付,会占用房间资源。系统用了一个 Spring 的定时任务来解决这个问题:@Scheduled 注解标注的方法每 30 秒扫描一次订单表,把创建时间超过 15 分钟且状态仍为 PENDING 的订单全部更新为 EXPIRED,并释放对应房间状态。
这里有一个容易踩坑的地方:定时任务必须在启动类上加 @EnableScheduling 注解,否则定时方法根本不执行。很多新人忘记这一步,测试时发现订单怎么都不会过期,排查半天查不到原因。
伪代码逻辑:
java复制@Scheduled(fixedDelay = 30000)
public void handleExpiredOrders() {
List<Reservation> expiredOrders = reservationMapper.selectExpiredPendingOrders(LocalDateTime.now().minusMinutes(15));
for (Reservation order : expiredOrders) {
// 更新订单状态为 EXPIRED
// 释放房间状态为 AVAILABLE
}
}
3.6 前端页面渲染如何调到后端数据
前端 Vue 项目的核心逻辑在 src/api 和 src/views 两个目录。api 目录按业务模块封装了后端接口调用,views 目录放页面组件。
以门户首页展示可预订房间为例,页面挂载时调用 getAvailableRooms(params),后端返回该时间段内所有可订房间的 JSON 数组,前端用 v-for 指令将数据渲染成卡片列表。
后端返回的统一数据格式很关键。这套项目自定义了 Result 类,结构固定为 { code, message, data },code=200 表示成功,code=500 表示业务异常,前端通过判断 code 来决定是正常渲染还是弹出错误提示。如果返回的数据是 null,前端要用默认值兜底,避免页面出现 undefined 异常。
4. 项目部署与常见问题排查实录
4.1 打包出可部署的 jar 包
本地开发调试没问题后,就该考虑打包部署了。maven 打包命令:
bash复制mvn clean package -DskipTests
打包完成后,target 目录下会生成 hotel-booking-0.0.1-SNAPSHOT.jar。这个 jar 是 Spring Boot 的内嵌容器可执行 jar,里面自带 Tomcat 服务器。尝试用 java -jar hotel-booking-0.0.1-SNAPSHOT.jar 启动时,如果连接数据库失败,优先检查 application.yml 中的数据库密码,以及 MySQL 是否允许远程连接。
4.2 JDK 1.8 项目打包到 Docker Desktop 的实践
最近 springboot jdk1.8打包到docker desktop 被搜得很频繁,这里直接给出一套可行方案。先在项目根目录创建 Dockerfile:
dockerfile复制FROM openjdk:8-jdk-alpine
MAINTAINER yourname
COPY target/hotel-booking-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
注意基础镜像的选择。如果 Dockerfile 基础镜像用的是 openjdk:8-jdk-alpine,部分高版本 Docker Desktop 的 OpenJDK 镜像已经停止更新,拉不到这个老旧标签,可以换成 eclipse-temurin:8-jre-alpine,这个是 Adoptium 社区维护的 JDK 8 镜像,安全更新一直在跟进,实测下来在 Docker Desktop 上拉取成功率高很多。
构建镜像的命令:
bash复制docker build -t hotel-booking:0.0.1 .
启动容器的命令,注意把 MySQL 和 Redis 的地址改成宿主机 IP,容器内访问 localhost 指向的是容器自己:
bash复制docker run -d -p 8080:8080 --name hotel-booking hotel-booking:0.0.1
启动后发现容器内报连接数据库超时,是因为 MySQL 的 bind-address 默认绑定了 127.0.0.1,改配置文件让它监听所有地址,再授权给指定用户允许任意 IP 访问。
4.3 MyBatis Plus 字段映射不生效的排查
这是一个极其常见的坑。数据库字段如果是 create_time,实体类属性是 createTime,理论上开启了驼峰映射后自动转换。但如果自己手写 XML 里的 SQL,写了 SELECT * FROM t_reservation,MyBatis Plus 的自动映射是没问题的。但一旦在 XML 中写 SELECT create_time FROM ... 这种带了别名或者多表联查的 SQL,返回值如果直接映射到实体,驼峰映射可能失效,导致实体属性全是 null。
正确做法是在 XML 中给字段起别名,或者在 application.yml 配置 map-underscore-to-camel-case: true,并且所有 XML 查询使用 resultType 而不是 resultMap 的时候,要确保 MyBatis 版本支持自动驼峰转换。实际排查步骤:先在日志里看打印的 SQL 和参数,确认查询有没有执行;再看返回结果集里的字段名和实体属性能不能对得上;最后看是否开启了下划线转驼峰配置。
4.4 Jackson 日期格式化:后台管理页时间显示不对
开发一个酒店预订系统,日期处理是重灾区。后端返回的日期格式,如果 application.yml 里没配 jackson.date-format,默认序列化出来是 "2024-01-01T12:00:00.000+0800" 这种 ISO 格式,前端直接展示出来非常难看,而且时间戳往往带了时区偏移,看起来会比北京时间慢 8 个小时。
解决方案有两个地方必须同时设置。后端在 application.yml 里配置 spring.jackson.date-format 和 spring.jackson.time-zone。前端在 axios 响应拦截器里,对返回的日期字符串做一次转换,显示成 YYYY-MM-DD HH:mm 的格式。两处配合,页面展示的时间才是准确的北京时间。
4.5 Spring Boot 循环依赖问题
热词里"springboot 循环依赖"排得相当靠前。简单解释:当 ServiceA 构造器注入 ServiceB,ServiceB 又构造器注入 ServiceA,Spring 容器在创建这两个 Bean 时会卡死,直接抛 BeanCurrentlyInCreationException。这个问题在酒店项目里确实容易出现,比如 ReservationService 需要调用 RoomService,而 RoomService 又反向调用了 ReservationService 来更新房间状态。
循环依赖有三种解法。方案一最推荐:重构代码把公共逻辑抽到第三个 Service 中,打破循环。方案二:用 @Lazy 注解放在其中一个注入点上,延迟创建代理对象。方案三:把构造器注入改成 @Autowired 字段注入或 setter 注入。但要注意从 Spring Boot 2.6 开始,Spring 官方默认禁止了循环依赖,如果项目升级到 2.6+,字段注入也能报错。所以最稳妥的办法永远是方案一,重构出清晰的调用层级。
4.6 Spring Boot 自动装配原理,面试高频题
学这个项目不可绕开的原理知识点就是自动装配。@SpringBootApplication 注解是由三个注解组合而成:@SpringBootConfiguration 相当于 @Configuration,@EnableAutoConfiguration 是自动装配的开关,@ComponentScan 扫描根路径下的 @Component。
@EnableAutoConfiguration 的内部核心是 @Import(AutoConfigurationImportSelector.class)。这个类在启动时会扫描 Spring Boot 的自动配置文件 /META-INF/spring.factories,读取所有 xxxAutoConfiguration 配置类的全限定名,然后按条件注解 @ConditionalOnClass、@ConditionalOnProperty 等进行过滤。
比如 RedisAutoConfiguration 类上有 @ConditionalOnClass(RedisOperations.class),当项目里引入了 spring-boot-starter-data-redis,RedisOperations 这个类在 classpath 中存在,这个自动配置就生效。如果没有引入对应的 starter,这个类加载不到,配置就跳过。这就是为什么 Spring Boot 能"开箱即用",实际是条件注解做了一层智能开关。搞清楚整个链路后,面试官再问自动装配原理基本能稳稳接住。
5. 这套系统后续还能怎么扩展
项目如果只是照着跑通一遍,学到的东西有限。真正让能力上一个台阶的做法,是带着"这里有点笨"的眼光去看,想想哪些地方还能做得更好。
第一个扩展方向是引入分布式锁。目前订单创建用的是数据库悲观锁,在单机部署下没毛病。但如果预订量上来了,要横向扩展部署多个实例,就需要引入 Redis 分布式锁,用 SETNX 命令作为房间维度的锁,才能保证多个实例间的并发安全。
第二个扩展方向是增加网关层。目前所有接口直接暴露,鉴权逻辑写在每个模块里。后续如果接入小程序端或者酒店内部管理系统,可以在前面加一层 Spring Cloud Gateway,统一做认证、限流、灰度发布,后端服务就能做真正的微服务拆分。
第三个扩展方向是支付回调的幂等处理。目前订单支付是模拟成功的,没有对接真实的微信支付或支付宝。真实对接时,支付回调可能因为网络问题重复推送。回调处理逻辑里必须对订单号做唯一约束,不然会出现订单状态被覆盖的错误。
第四个扩展方向是数据分析和报表。目前统计报表是简单的 SQL 聚合,每天跑一次全量。后续可以引入定时任务在凌晨把昨天的订单数据计算好,存到统计表里,或者引入 Canal 监听 MySQL binlog,把订单数据实时同步到统计库,报表查询性能会好很多。
这些扩展方向按优先生排序,优先做分布式锁和网关层,因为这两个直接关系到系统能否支撑更大的并发量,也是面试时能加分的亮点。
6. 个人实操体会总结
把源码完整跑通并二次开发一遍,我对 Spring Boot 项目开发的理解比单纯看文档要深得多。文档里的技术是一颗颗零散的珠子,项目是穿珠子的线,只有真正跑通一个完整项目,你才知道事务、缓存、状态机、异常处理这些概念是怎么配合协作的。
我给准备拿这套项目练手的朋友三点建议。第一,不要急着改代码,先花一晚上把项目跑起来,把每个页面点一遍,知道这个系统有哪些功能,数据是怎么流转的。第二,重点看订单模块,把它的状态流转图画出来,这是整个项目的灵魂,理解了它,其他模块都是增删改查。第三,自己动手加一个小功能,比如给房间加一个"特价房"标签、给订单增加评论功能,通过加功能去触碰既有代码结构,远比只看不改学得多。
如果在跑项目过程中卡住了,优先看控制台日志,再检查配置文件,最后才去翻代码。大部分问题都是环境问题或配置问题,真正的逻辑 bug 反而不多。希望这篇复盘能帮你把这个项目真正吃透,也欢迎交流实战过程中的各种问题。
