☰
Spring Boot+Vue酒店管理系统实战:从零搭建到部署
2026/10/10 3:21:35 网站建设 项目流程

从零开始撸个酒店管理系统(附源码实战)

我一直觉得,酒店管理系统是后端开发者练手最划算的项目之一。它没有电商那种秒杀级的流量压力,但预定、入住、退房、换房、账单这些业务逻辑足够复杂,权限、状态流转、并发防重这些坑一个不少。这套系统学明白了,往后接任何带“订单状态”的业务,心里都有底。这篇文章我把整套从零搭建的完整过程、关键代码、踩坑记录都整理出来,按照这套思路跑一遍,你也能拿到一份可以直接拿去改、拿去交作业、拿去面试讲的项目源码。

这次实战决定采用前后端分离结构,后端用 Spring Boot 3 + MyBatis + MySQL,前端用 Vue 3 + Element Plus,权限用 JWT,代码全部放在模拟项目X的仓库里。为什么要选这套技术组合,而不去用 PHP 或者 Python Django,我在后面技术选型部分会详细说清楚,这里先给结论:这套组合是目前中小型管理类系统最主流、招聘市场最认、线上踩坑资料最多的组合,拿来练手和项目实战,性价比最高。

我个人给这个项目的定位是:你不需要有很强的分布式经验,只要会 Java 基础、懂 SQL 增删改查、了解 Vue 组件化,就能跟着把这套系统完整跑起来。如果你是刚学完框架想找项目练手的学生、准备课程设计的开发者、或者想转后端但缺项目经验的求职者,这篇内容非常适合。如果你已经带过多个高并发项目,那抱歉,这套业务复杂度可能不太够看,你可以把这篇文章当成一套基础框架,把 Redis 缓存、MQ 异步、分库分表逐步加进去再玩一遍。

1. 项目整体设计与技术选型

1.1 为什么首选前后端分离而不是 JSP 传统架构

很多人在做酒店管理系统课程设计的时候,习惯性选 JSP + Servlet,理由是“老师讲的都是这个”。但我要泼盆冷水:现在一线业务系统几乎没有新项目用 JSP 了。前后端分离带来的最大好处是——后端 API 可以被小程序、App、自助终端多端复用。

我在做这套系统时直接砍掉了服务端页面渲染,后端只暴露 JSON 接口,配合 JWT 做无状态登录。这样以后想加一个微信公众号订房入口,前端单独写一套页面就行,后端的预定、查询、支付接口完全不用动。

从部署角度讲,前端打包成一堆静态文件丢进 Nginx,后端是一个独立 JAR 包,两个进程互不干扰,排错时也不会因为 Tomcat 里跑着 JSP 和 API 混在一起难定位。这个架构决策做下来,后期开发体验非常舒服。

1.2 后端选型:Spring Boot 3 的取舍与理由

后端我选的是 Spring Boot 3 + MyBatis,而不是 JPA/Hibernate。原因很简单:酒店管理系统的查询场景极其复杂,尤其是统计报表部分,需要写多表联查、分组统计、日期范围过滤。JPA 的 Criteria API 写这类复杂查询是灾难现场,控制不住生成的 SQL;而 MyBatis 可以直接把 SQL 写在 XML 里,SQL 就是公司里的通用语言,DBA 也能看懂,出了问题复制到 Navicat 里就能查。

Spring Boot 3 是基于 JDK 17 的,很多人还在用 Spring Boot 2.x + JDK 8,这个问题要说明一下。如果你是企业内部老项目,用 2.x 没毛病;但做新项目、新源码,我建议直接用 3.x,毕竟新特性、新依赖都在往 3.x 迁移。这套系统源码里所有依赖坐标我都用的 3.x 最新稳定版本,你克隆下来直接 Maven 构建基本不会遇到兼容性问题。

有些人会考虑用 PHP 或 Python 写,PHP 不适合做复杂的后端状态逻辑,Python 的 Django 确实能很快搭起来,但如果你以后的职业方向是企业级 Java 开发,那建议一步到位用 Java 生态。项目可以练手,技术栈最好带点职业规划思维。

1.3 数据库选型:MySQL 8 与InnoDB

数据库是 MySQL 8.0,存储引擎统一用 InnoDB。为什么不用 MyISAM?因为酒店管理系统涉及预定和订单,事务是刚需。一个预定操作要同时扣减房间可售数量、生成订单记录、记录操作日志,三件事必须同生共死,InnoDB 的事务和行级锁才能真正保证数据不出乱子。

字符集这里有个经验:库、表、字段全部显式指定 utf8mb4,而不是只写 utf8。MySQL 的 utf8 实际只能存 3 个字节的字符,一些生僻字或 emoji 符号存进去直接报错,utf8mb4 才是真正的完整 UTF-8。这个坑我踩过,入住登记时客人姓名带个生僻字导致订单插入失败,排查了半天。

1.4 模块划分与核心功能范围

酒店管理系统看上去简单,但功能边界很容易画不清楚。我做这套源码时把范围圈定在以下六个核心模块,既保证业务闭环,又不会因为贪多嚼不烂:

  • 客房管理:房型、房号、楼层、床位数量、价格、状态维护。
  • 客户管理:散客登记、会员信息、联系方式、历史入住记录。
  • 预定管理:线上预定、取消预定、预定转入住、预定超时释放。
  • 入住管理:办理入住、房间分配、换房、续住、退房结算。
  • 账单管理:订单金额计算、押金管理、优惠减免、账单明细打印。
  • 系统管理:员工账号、角色权限、操作日志、基础参数配置。

每个模块都保留了扩展点,比如房型可以加“钟点房”“长租房”的计费策略,预定可以接入第三方渠道。你做课设或者面试项目时,在扩展点上稍作说明,非常加分。

2. 数据库设计:先把表关系理明白

2.1 六张核心表的职责与关系

我见过很多新手做酒店系统,第一版就把“订单表”设计成万能表,客房信息、客户信息、价格、优惠全塞进去。表面上看查询少了几次 JOIN,实际上业务状态根本理不清,退房、换房、续住的时候你会发现一个字段根本表达不了复杂逻辑。

我的设计原则是:一个业务角色一张表,状态变化用字段表达,金额计算用独立子表记录明细。这版设计里总共有 8 张表,其中最核心的 6 张是这样划分的:

  • room:客房信息表,只负责“房间有什么、多少钱、什么状态”。
  • customer:客户表,只负责“客人是谁、什么联系方式、是否会员”。
  • reservation:预定单表,记录预定信息、房型、预定日期、入住离店日期、状态。
  • checkin:入住单表,记录实际入住信息,一个预定单可以对应多个入住单(比如预定了 3 间房,分 3 次办理入住)。
  • bill:账单表,记录入住产生的费用明细、押金、实收金额。
  • user:员工账号表,包含登录名、密码密文、角色编码。

表之间的关联不要用逻辑外键满天飞的方式。比如入住单关联预定单,用reservation_id字段做普通索引即可,不需要建物理外键。物理外键在高并发写入时有性能损耗,而且业务补偿逻辑困难,实际开发中基本都是靠应用层约束逻辑,底层只建索引。

2.2 客房状态机的字段设计

客房状态是整个系统最核心的状态流转,我拆得非常细,一共七个状态:

  • 1-可售:房间空闲且干净,可以预定。
  • 2-已预定:已被预定但客人还没入住。
  • 3-入住中:客人已办理入住。
  • 4-脏房:客人已退房,但保洁还没打扫。
  • 5-维修中:房间设施故障,暂停销售。
  • 6-预留:内部预留或锁房。
  • 7-离线:系统离线状态下由前台手工锁房。

状态全部用 TINYINT 整数存储,不要用字符串。第一是查询效率,第二是方便在代码里定义常量枚举。状态流转在 Service 层统一控制,不能允许随意 UPDATE。比如从“入住中”跳到“可售”,必须经过“脏房”状态,这个规则写在退房逻辑里,防止前人退房后人直接入住的情况出现。

2.3 房型与房价策略设计

房价策略如果写死在代码里,后期改价格就要重新发版,太蠢。我把房价表抽出来,字段包含:房型ID、平日价、周末价、节假日价、押金标准。计费时根据入住日期是否是周末或者节假日自动匹配价格。

节假日判断不能写死日期,我单独做了holiday_config表,运营可以提前配置节假日区间。价格计算的核心逻辑放在服务端,按天遍历生成每日房价明细,这样账单打印时能看到每一天的价格组成,客人问起来也解释得清楚。

后面如果有需要,可以把这张表升级为“价格策略引擎”,支持早订优惠、连住折扣、会员折扣等多重叠加规则,代码里预留了priceRule扩展字段。

2.4 完整建表 SQL 要点

核心建表语句我贴一段有代表性的,完整版本在源码的schema.sql里。注意几个关键点:

  • 金额字段统一用DECIMAL(10,2),禁止用 FLOAT/DOUBLE,否则会出现 0.1+0.2 不等于 0.3 的精度问题。
  • 时间字段用DATETIME而不是TIMESTAMP,避免 2038 年问题,也避免时区转换带来的不必要的坑。
  • 所有表要有create_time、update_time,MyBatis-Plus 的自动填充功能可以统一处理,省得每个插入都手写。

核心 SQL 结构如下:

CREATE TABLE `room` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `room_number` VARCHAR(10) NOT NULL COMMENT '房间号,如 1208', `room_type_id` BIGINT NOT NULL COMMENT '房型ID', `floor` INT NOT NULL COMMENT '所在楼层', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '房间状态:1-可售 2-已预定 3-入住中 4-脏房 5-维修中', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_room_number` (`room_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客房信息表';

设计数据库时,我强烈建议所有主键都用自增 BIGINT,不要用 UUID。不是说 UUID 不能用,而是它的随机性会导致 InnoDB 聚簇索引页分裂严重,写入性能直线下降。以后要拆库拆表,雪花ID有的是办法迁移,但自增主键的简单可靠是最稳妥的起点。

3. 后端 API 从零搭建实录

3.1 项目初始化与依赖配置

创建一个 Spring Boot 3 项目最省事的方案是用官方 Initializr 生成骨架,然后手动引入自己需要的依赖。我在源码里把pom.xml整理得比较干净,核心依赖如下:

  • Spring Boot Starter Web:提供 REST API 能力。
  • MyBatis Spring Boot Starter:数据访问层。
  • MySQL Connector/J:驱动。
  • Lombok:减少实体类样板代码。
  • JWT 相关工具库:做无状态登录。
  • Spring Boot Starter Validation:请求参数校验。
  • Hutool(可选):工具类集合,处理日期、加密非常方便。

pom.xml里的主要依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>

这里要特别注意:如果是 Spring Boot 3.x,MyBatis 官方 starter 版本必须用 3.0 以上,不能用 2.x,否则启动会报ClassNotFoundException,这属于版本兼容性问题,查一下官方文档就能解决,但新手在这里卡住的不在少数。

3.2 三层架构:Controller 只做翻译官

后端我严格遵守 Controller-Service-Mapper 三层结构。很多新手喜欢在 Controller 里写业务逻辑,图省事。短项目看不出问题,一旦业务复杂,Controller 里堆 100 行业务代码,既无法复用也没法做单元测试。

我的分层职责非常明确:

  • Controller 层:只接收参数、调 Service、返回统一结果对象。
  • Service 层:写所有业务规则和状态流转,是核心逻辑的地方。
  • Mapper 层:只负责 SQL 执行,不掺杂业务。

比如“办理入住”接口,Controller 里就是简单的接收请求体和返回响应体。真正的校验逻辑(房间是否可售、预定单是否有效、入住人信息是否齐全)全部在 Service 层。

3.3 统一返回体与全局异常处理

接口返回格式如果不统一,前端对接起来会非常痛苦。我定义了一个Result<T>泛型类,所有接口都返回这个结构:

{ "code": 200, "message": "success", "data": {} }

业务错误码我单独放在ErrorCode枚举里,比如房间不可售返回 2001、预定时间不合法返回 2002、退房时账单未结清返回 2003。光有错误码还不够,必须配合全局异常处理器。我在项目里用@RestControllerAdvice统一拦截三类异常:

  • 业务异常:自定义的检查逻辑,手动抛出。
  • 参数校验异常:@Valid注解校验不通过时自动触发。
  • 系统异常:未知异常的兜底。

全局异常处理器的代码很简单,但它的价值在于:所有接口千万不能把堆栈信息直接返回给前端。有的新手直接把e.printStackTrace()的内容用 500 状态码返回,前端看不懂,安全上也不该暴露内部信息。正确做法是日志文件里记录完整堆栈,响应只返回“系统繁忙,请稍后重试”。

3.4 JWT 登录与权限拦截器实现

登录逻辑本质上是一个鉴权过程:员工输入账号密码,后端校验通过后签发一个 JWT token,前端每次请求在 Header 里带上Authorization: Bearer xxx。

JWT 我用 HMAC256 签名,密钥放在配置文件里,而不是写死在代码里。Token 里只放用户ID、用户名、角色编码三个字段,不放密码、不放手机号这类敏感信息。过期时间默认设置 8 小时,酒店前台是 24 小时轮班制的,8 小时是个比较合适的折中值。

权限拦截器我是这么实现的:写一个JwtInterceptor,继承HandlerInterceptorAdapter,在preHandle方法里校验 token 是否合法、是否过期,然后从 token 里解析出角色,放入 ThreadLocal 上下文。Admin 角色可以访问所有接口,FrontDesk 角色只能访问客房查询、预定、入住操作,没有报表权限,这样权限控制粒度清晰。

3.5 预定业务核心代码解析

预定是酒店最核心的业务,这块逻辑我详细说一下,也是整套源码里代码量最集中的地方。

预定操作的整体流程是:

  1. 前端传入入住日期、离店日期、房型ID、房间数量。
  2. 后端校验日期合法性、校验房型是否存在。
  3. 查询该时段内该房型的可售房间数量。
  4. 如果可售数量足够,加上行锁扣减。
  5. 生成预定单,状态为“已预定”。

这里有个重要的并发问题:两个客人同时预定同一间房,怎么保证不会重复?最简单有效的方案是对房间表加行锁,使用SELECT ... FOR UPDATE语句锁住房型关联的房间记录,再执行可售判断和状态修改。

MyBatis XML 里这行代码非常关键:

<select id="selectAvailableRoomCount" resultType="int"> SELECT COUNT(*) FROM room WHERE room_type_id = #{roomTypeId} AND status = 1 FOR UPDATE </select>

FOR UPDATE是 InnoDB 提供的悲观锁机制,只有在事务里才生效。使用时要配合@Transactional,保证整个查询和后续更新在同一个事务里。在中小型酒店的业务量下,这个方案已经足够可靠,不需要引入 Redis 分布式锁这种重型武器;如果以后要做预订中心这种高并发系统,再考虑锁方案升级。

4. 前端 Vue 3 + Element Plus 实战

4.1 前端项目结构与路由设计

前端我选的是 Vue 3 组合式 API + Vite + Element Plus。Vue 3 的script setup语法写起来非常顺滑,比起 Options API 省掉很多样板代码。Vite 作为开发服务器和打包工具,启动速度比 Webpack 那是天壤之别,配好后刷新基本不需要等待。

前端目录结构:

  • src/api:所有后端接口的封装。
  • src/router:路由配置,含登录鉴权守卫。
  • src/views:页面视图,按模块分文件夹。
  • src/components:公共组件,比如搜索栏、分页、弹窗。
  • src/store:用 Pinia 管理用户登录态和权限信息。

路由守卫的作用是:进入每个页面之前检查本地有没有 token,没有 token 就强制跳到登录页;有 token 再校验路由的 meta 信息里要求的角色编码,不满足就跳转 403 页面。这套机制让前端页面的权限控制和后端拦截器形成双重保障,单纯绕过前端按钮并不能直接访问后端数据。

4.2 API 封装与 Axios 拦截器

前端请求我统一用 Axios 封装,全局配置了baseURL,方便区分开发环境和生产环境。开发环境把 Vite 的代理配置到/api,代理到本机的后端端口,天然避开跨域问题。

Axios 拦截器是前端的关键点。请求拦截器统一从 Pinia store 里拿 token,加到 Header 上;响应拦截器统一拆解后端的Result结构,code 不等于 200 就弹出 Element Plus 的 Message 提示。特别要处理一个情况:如果后端返回 401 说明 token 过期,这时响应拦截器要做的是清空本地登录态并且跳转到登录页,避免用户卡在页面里反复报错。


接口封装示例,所有页面只调用这个方法来拿数据:

export function createReservation(data) { return request({ url: '/api/reservation/create', method: 'post', data }) }

4.3 客房管理页面的核心实现逻辑

客房页面需要加载房型列表、楼层筛选、房间状态。我用 Element Plus 的el-table展示,状态那一列用el-tag标签按颜色区分:绿色可售、橙色已预定、红色入住、灰色脏房、蓝色维修。

页面加载时同时请求/room/list和/roomType/list两个接口,前端拿到数据后本地做一次关联映射,把roomTypeId对应的房型名称填到表格里。这个操作也可以让后端直接 JOIN 返回,但我选择前端关联,让后端接口更通用一点。

操作栏放三个按钮:办理入住、换房、设为维修。不同状态显示不同按钮,类似“状态机驱动按钮显隐”,这是一种常见设计思路。比如房间状态是可售,就显示“办理入住”按钮;如果是入住中,就只显示“换房”和“退房”。判断逻辑用 Vue 的计算属性写在代码里,简洁清晰。

4.4 预定下单页的日期选择与房间校验

下单页是整个前端交互最复杂的页面。日期范围组件我用 Element Plus 的el-date-picker,类型设为daterange。选择日期后,前端立刻调后端接口GET /room/available?roomTypeId=xx&startDate=xx&endDate=xx,实时查询该时段可售房间数、展示每日价格和总价。

价格计算我在前端做了联动:选择房型、日期后,前端先把每日价格算出来展示给用户,用户确认后再把参数发给后端。这只是一个友好交互展示,真正的价格还是要以后端计算为准,前端算的只是预估价。后端在创建预定单时会重新按规则算总价,前端传的价格只做展示,不能直接信,否则容易被非法请求绕过。

5. 核心业务逻辑与数据流转细节

5.1 预定-入住-退房的状态机设计

酒店系统的灵魂,是把预定/入住/退房的状态流转理清楚。我画的业务闭环大致是这样:

  • 预定单状态:待确认 → 已确认 → 已入住 → 已取消 → 已离店
  • 入住单状态:在住 → 已退房(含正常退房、提前退房)
  • 账单状态:未结清 → 已结清 → 已退款

状态的流转我全部放在 Service 的方法里,不允许其他层直接改状态字段。举个例子,退房操作对应的方法逻辑是:

  1. 校验入住单存在且状态为“在住”。
  2. 计算本次入住所有费用(含房费、加床费、赔偿费等)。
  3. 校验押金是否足够抵扣,不够的话走补收流程。
  4. 更新房间状态为“脏房”。
  5. 更新入住单状态为“已退房”。
  6. 更新预订单状态为“已离店”。
  7. 生成账单单据。

这一串操作全部@Transactional包裹,任何一步失败都会整体回滚。有一次我在开发时发现客人退房了、房间状态也改了,但账单没生成,就是因为少标了事务注解导致的。

5.2 房价计算:为什么不能用简单的数量乘单价

很多初版酒店系统的价格计算是总价 = 每晚价格 x 晚数,表面上没问题,但遇到价格策略就垮了。比如周中 300、周末 500,入住时间跨越周五到周一,三天的价格分别是 300、500、500、300,总价应该是 1600 而不是简单的“单价 x 晚数”。

我的实现是:后端根据入住日期和离店日期,遍历每一天,检查当天是工作日、周末还是节假日,用对应价格逐一累加。累加过程生成每日价格明细存入bill_item表,就像是超市小票一样列得清清楚楚。

这个逻辑的代码特别适合放在 Service 的一个独立方法里,因为预定预览和退房结算都要复用。如果一开始就把价格计算和创建预定写在一个方法里,后面做“价格详情展示”就要重构了。

5.3 并发防重:悲观锁还是乐观锁

预定房间时的并发问题,我在前面提了悲观锁方案。在实现层还有另一个选择是乐观锁:房间表加一个version字段,更新时检查版本号是否匹配,不匹配就重试。我在房间库存扣减逻辑里最终用的是悲观锁,理由是小并发下悲观锁的实现更直观、出错率更低。乐观锁适合读多写少且冲突概率低的场景,比如浏览商品、下单扣库存这种,但酒店房间变更频率可控,悲观锁不会成为瓶颈。

这里有个重要提醒:SELECT ... FOR UPDATE只有在事务里才生效,而且必须确保查询走了索引。如果 SQL 的 WHERE 条件没有走索引,InnoDB 会锁全表,那性能就会很难看。所以room_type_id字段我建了联合索引,让行锁能精准落到几行数据上,尽可能锁少量数据。

5.4 MyBatis XML 动态 SQL 实战要点

MyBatis 的 XML 里我用了大量动态 SQL,这是 MyBatis 比 JPA 舒服的核心原因。比如房间列表查询,筛选条件可能是房间号、楼层、状态,可能都有,也可能都没有。

<select id="selectRoomList" resultType="RoomVO"> SELECT r.*, rt.name AS roomTypeName, rt.price FROM room r LEFT JOIN room_type rt ON r.room_type_id = rt.id <where> <if test="roomNumber != null and roomNumber != ''"> AND r.room_number LIKE CONCAT('%', #{roomNumber}, '%') </if> <if test="floor != null"> AND r.floor = #{floor} </if> <if test="status != null"> AND r.status = #{status} </if> </where> ORDER BY r.floor, r.room_number </select>

<where>标签会自动去掉多余的 AND 或 OR,这个特性非常实用。另外LIKE CONCAT的方式要养成习惯,不要直接在 XML 里写'%' '${roomNumber}' '%',${}存在 SQL 注入风险,#{}才是预编译占位符。MyBatis 里${}唯一的合理使用场景是动态排序字段、动态表名,而且场景要通过白名单校验,这个安全习惯建议从练手项目就开始养成。

5.5 接口幂等与重复提交防护

前台服务员在高峰期可能手滑点两次“办理入住”按钮,如果后端没有幂等保护,就会产生两条入住单,一间房被分配给两个客人。

我的方案是在关键接口上做 token 幂等机制。前端在进入表单页时向后端申请一个唯一requestId,提交时带着这个 ID 传到后端。后端在处理前先查缓存里有没有这个 ID,有就直接返回之前的处理结果,没有就处理并写入缓存。这个方案在 Spring Boot 里可以用 AOP 切面实现,代码比较简洁,但确实能防止大多数重复提交问题。

当然,如果要真正做得更好,需要配合 Redis 做缓存和过期控制。没有 Redis 的环境用 ConcurrentHashMap 做 JVM 内缓存也能解决单机场景的问题,我把两种实现都写在源码里了,按需选择即可。

6. 常见问题与排查技巧实录

6.1 MyBatis 参数传递导致 SQL 执行异常

项目做到一半,测试时发现一个诡异问题:传入roomTypeId=1时接口正常,传入roomTypeId=2时报空指针。排查了半天,最后定位在 Mapper 方法没有加@Param注解,XML 里的#{roomTypeId}无法正确绑定。

MyBatis 多参数传递时必须用@Param显式命名参数,或者在 XML 里用param1、param2这种默认位置参数,否则框架不知道参数名是什么。单参数且是实体对象或基本类型时没这个问题,但多参数场景很容易踩坑。建议所有 Mapper 方法统一加@Param,风格一致,排查问题也更快。

6.2 跨域问题与开发环境代理配置

前后端分离开发时,前端跑在 Vite 的 5173 端口,后端跑在 8080 端口,浏览器直接请求就会遇到 CORS 报错。我试过在后端加@CrossOrigin注解、加 CORS 配置类,都能解决,但最干净的方案还是让前端代理转发:Vite 配置里设置proxy,把/api开头的请求转发到后端地址。

这样做的额外好处是生产环境部署时,Nginx 也只需要配置一个/api反向代理到后端服务,前后端联调时的接口地址完全不用改。如果后续后端接口改成网关域名,只需要改代理配置一处就行。

6.3 金额计算出现 0.1+0.2 精度问题

第一次做账单结算时,我把金额字段定义成了DOUBLE,结果测试时发现一个奇怪现象:三笔金额加起来显示 100.999999。用户视角看到这种账单,第一反应就是系统算错了。

解决方案很简单:数据库字段改成DECIMAL(10,2),Java 实体类用BigDecimal。这里要提醒一下,BigDecimal 构造时最好用字符串入参的构造方法,而不是数字类型入参,比如new BigDecimal("19.90"),直接new BigDecimal(19.90)精度问题依旧会出现。

6.4 日期区间查询的边界条件

查询“2024年5月1日至5月3日入住记录”时,如果 SQL 里用check_in_date BETWEEN '2024-05-01' AND '2024-05-03',那 5 月 3 日 00:00 到 23:59 之间的数据全部查不出来。因为日期字符串会被隐式转换成2024-05-03 00:00:00,5 月 3 日当天大部分数据都被漏掉了。

正确写法是右边界加一天然后使用左闭右开区间,或者显式把结束日期转成当天的 23:59:59。这个坑尤其在统计报表模块里会高频出现,排查时第一反应就想到时间边界条件,能省很多时间。

6.5 并发测试出现的房间超卖问题

这个是我后期模拟并发场景时发现的。两个线程同时预定最后一间房,各查一次可售数量都显示有房,结果系统生成了两笔订单,绑定到同一个房间。排查时发现是因为我用的是查询再更新的非原子操作,缺少锁保护。

加上FOR UPDATE锁之后,第二个事务必须等第一个事务提交才能继续查询,这是真正从机制上解决了超卖问题。这类问题的核心教训是:任何“查询后根据结果再更新”的场景,都要考虑并发场景下的原子性问题,不能觉得“业务量小就不会发生”。

7. 部署上线与开发效率实操心得

7.1 本地环境启动的完整步骤

我假设你的电脑装好了 JDK 17、Maven 3.8+、Node 18+、MySQL 8。第一次启动这套项目的完整流程是:

  1. 创建数据库,执行schema.sql初始化表结构和基础数据。
  2. 修改后端application.yml里的数据库账号密码。
  3. Maven 打包mvn clean package -DskipTests,然后java -jar启动后端。
  4. 前端进入目录,执行npm install,然后npm run dev。
  5. 浏览器访问开发地址,默认账号 admin,密码 123456。

如果启动过程报端口冲突、数据库连不上、依赖下载失败,大概率是环境版本问题,先看日志前几行,Spring Boot 的启动日志信息其实很全,比盲目搜索要快得多。

7.2 代码层提升开发效率的三件套

这套项目能快速开发完,依赖三个编码习惯,我把它们整理成了固定的模板:

  • 统一响应体 + 全局异常处理:接口代码只剩业务逻辑,不用每处都写 try-catch 和“拼返回对象”的模板代码。
  • Lombok 简化实体类:实体类只写属性,getter/setter自动生成,代码量减少 40%。
  • 前端表单页统一风格:所有表单页都用 Element Plus 的el-form加rules校验规则,改模板效率极高。

建议你做一套自己的“标准增删改查模板”,后续做任何管理类系统,上手就是复制粘贴改字段,一天出一个模块不是梦。

7.3 后续可以扩展的方向

做完整套系统后,如果不满足于“能跑”,可以继续加这些能力:

  • 接入 Redis 缓存房型和可用房间数,减轻数据库压力。
  • 引入 RabbitMQ 处理“预定成功通知”这类异步操作。
  • 增加统计报表模块,按周、按月输出入住率、平均房价、RevPAR 等经营指标。
  • 部署时用 Docker 把 MySQL、后端、前端、Nginx 编排起来,一键启动。
  • 小程序端复用后端 API,做一个移动端订房入口。

整体来看,这套系统的边界比较清晰,扩展点也保留得比较充分。做课设的话,基础功能已经足够;做简历项目,建议至少加一个统计报表模块并且讲清楚其中的 SQL 优化思路,面试官会比较感兴趣。

最后分享一个我自己的体会:做完这版系统,最大的收获不是“我写了个多少行的项目”,而是完整走了一遍“需求分析 → 表设计 → 接口设计 → 前端对接 → 联调部署”的全流程。做项目不一定是越复杂越好,把一个业务闭环真正跑通、把状态流转和并发隐患想清楚,比写一堆用不上的炫技代码有用得多。这套源码你可以直接拿去部署、二次开发或者改造,如果在跑通的过程中遇到新问题,用上面整理的排查思路基本上都能解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询