简介酒店客房管理系统源码是一套完整的前后端分离项目后端采用Spring Boot框架前端使用Vue框架数据库使用MySQL存储。系统分为管理后台与用户网页端支持管理员、会员两类角色功能包括会员管理、清洁工管理、客房管理、用户预约、入住办理和系统基础模块适合Java与Web方向的学习者参考也可用于课程设计或毕业设计。资源包共包含一千七百八十八个文件压缩后约三十九点三六兆大小。文件中以Java后端源码、Vue前端组件、JavaScript脚本、HTML与CSS页面为主同时含有SQL数据库脚本、XML配置、图片素材及部分备份文件方便从后端到前端进行整体阅读和二次开发。目前已有九十二人学习下载。依靠这套源码可以理解前后端交互方式、角色权限控制和预约入住流程备份文件也有助于对比调试和排错无论是学习技术栈整合还是参考业务模块划分都很有帮助是一份实用完整的项目参考资料。1. 酒店客房管理系统能解决什么前台三班倒的历史遗留难题我拆这个项目时脑子里一直有个画面某酒店前台白班同事刚把手写台账合上晚班接班就发现三间房的入住状态没同步客人拿着预订记录来办入住前台翻了两分钟 Excel 才确认房间还在。这种场景在中小型酒店太常见了——客房信息散落在纸质单、Excel 和口头交代里订单和房间状态永远对不上。这套基于 Java SpringBoot Vue.js MySQL 的酒店客房管理系统就是冲着这个痛点来的把房间状态、预订订单、入住退房、客户信息全部收进一个前后端分离的 Web 系统里前台在浏览器里点几下就能完成全部操作。如果你是刚接触 SpringBoot Vue 全栈开发的学生或者想把内部管理工具从 Excel 升级成 Web 系统的开发者这个项目的代码量、分层方式和接口设计都适合先跑通一遍再改造成自己的版本。下面按我拆项目的顺序来讲先看选型和结构再拆核心链路然后跑通最后说改造。2. 技术选型与目录结构SpringBoot Vue.js 前后端分离的一次完整落地2.1 技术选型为什么是这套组合酒店客房管理系统的使用场景很明确一个酒店前台三五台电脑偶尔经理在办公室查房态没有高并发但业务状态多、操作频次高。这个场景下SpringBoot Vue.js MySQL 几乎是最省事的选择。后端用 SpringBootJava的理由很实际生态成熟、上手快、部署简单。SpringBoot 自带内嵌 Tomcatmvn package打出一个 jar 包丢服务器上java -jar就能跑不依赖单独的 Web 容器。对这种 CRUD 占比极高的管理系统SpringBoot 的 Starter 机制能省掉大把配置——spring-boot-starter-web给 Web 能力mybatis-spring-boot-starter接数据库访问写业务代码的时间占比远高于配环境的时间。前端选 Vue.js核心原因是组件化。酒店管理界面看起来不多但房间看板、订单列表、入住表单这些页面有大量重复渲染和状态联动。Vue 的双向绑定让表单处理非常顺手组件复用又能把房间卡片、状态标签这类高频 UI 收敛成局部组件改一处样式全局生效。数据库选 MySQL 是这类中小型系统的默认答案InnoDB 的事务和行锁处理订单扣减、房间状态切换这种并发更新足够可靠。ORM 层我建议保留项目里的 MyBatis而不是换成 JPA。原因是酒店管理的查询条件变化太频繁了——今天到店但明天离店的订单三层所有可售大床房这类动态 SQL 用 MyBatis 的 XML 写直观得多出问题还能直接把 SQL 捞出来在客户端执行验证不用猜框架生成的是什么。2.2 后端分包与前端页面按图索骥找到要改的文件拿到源码先别急着跑把目录结构过一遍后面改什么都好定位。这个项目后端是经典的 controller / service / mapper 三层前端按页面维度组织两边通过/api前缀的 REST 接口对接。hotel-admin/ ├── backend/ │ ├── pom.xml │ └── src/main/java/com/hotel/admin/ │ ├── AdminApplication.java # SpringBoot 启动类 │ ├── controller/ # 接口层只收参数、返回结果 │ │ ├── RoomController.java │ │ ├── OrderController.java │ │ └── CheckInController.java │ ├── service/ # 业务层事务边界和状态校验都在这里 │ │ ├── RoomService.java │ │ ├── OrderService.java │ │ └── CheckInService.java │ ├── mapper/ # MyBatis 数据访问接口 │ │ ├── RoomMapper.java │ │ ├── OrderMapper.java │ │ └── StayRecordMapper.java │ ├── entity/ # 与表结构对应的实体 │ │ ├── Room.java │ │ ├── Order.java │ │ └── Customer.java │ ├── common/ # 统一返回结构、全局异常处理 │ │ ├── Result.java │ │ └── GlobalExceptionHandler.java │ └── resources/ │ ├── application.yml # 数据源、端口、JSON 序列化配置 │ └── mapper/ # MyBatis XML 文件 │ ├── RoomMapper.xml │ └── OrderMapper.xml ├── frontend/ │ ├── package.json │ ├── vue.config.js # 端口和开发代理配置 │ └── src/ │ ├── main.js # Vue 入口注册路由和 UI 组件库 │ ├── router/index.js # 页面路由 │ ├── api/ # axios 请求统一封装 │ │ ├── room.js │ │ └── order.js │ └── views/ │ ├── RoomBoard.vue # 房间状态看板前台主界面 │ ├── OrderList.vue # 订单列表 │ └── CheckIn.vue # 入住登记页 └── sql/ └── init.sql # 建库建表 测试数据这个结构最值得注意的一点是前后端映射关系后端controller和前端views一一对应。改房间看板前端动RoomBoard.vue后端对应RoomController.java和RoomMapper.xml顺着接口路径就能摸完整条链路不需要全局搜索。common/Result.java是所有接口的统一返回壳格式固定为{ code, message, data }前端 axios 拦截器里判断code ! 200就直接弹错误提示每个页面不用重复写错误处理逻辑。这个习惯建议你保留后续加接口时能少写很多样板代码。2.3 关键配置文件pom.xml 与 application.yml 里的门道pom.xml里的依赖不多但有几个值得留意dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencymysql-connector-j的 scope 是runtime意思是编译时不需要运行时才加载驱动这是数据库驱动依赖的常规写法。MyBatis 的 Starter 版本和 SpringBoot 父版本有兼容矩阵如果启动报NoSuchMethodError之类的错先查这两个版本的搭配再查代码。application.yml是后端启动前必须改的文件我在第 4 章会详细说每个参数的作用。这里先记住一个原则凡是跟环境相关的配置——数据库账号密码、端口、时区——都收敛在这个文件里代码里不要写死任何环境信息否则换台机器部署就是一场灾难。3. 核心模块拆解从客房预订到入住登记的数据流链路3.1 房间状态机四个状态撑起整个业务流酒店管理系统的核心不是订单表而是房间状态。预订、入住、退房、维修这些业务动作最终都反映为房间状态的变化。这个项目里房间状态是字符串常量一共四个值AVAILABLE 可售 BOOKED 已预订 CHECKED_IN 已入住 REPAIR 维修中流转规则很直接可售房间被预订后变BOOKED客人到店办了入住变CHECKED_IN退房后回到AVAILABLEREPAIR状态的房间不可预订不可入住。这个状态约束是业务正确性的最后一道防线必须由后端 Service 层强校验不能只靠前端按钮控制。对应的实体类长这样public class Room { private Long id; private String roomNo; // 房号如 301 private String type; // 房型大床房/双床房/套房 private BigDecimal price; // 门市价 private String status; // AVAILABLE / BOOKED / CHECKED_IN / REPAIR private String floor; // 楼层 private String remark; // 备注 // getter / setter 省略 }字段说明status是全表最关键的字段Service 层反复读取和校验它price必须用BigDecimal而不是double这是金额字段的底线——二进制浮点数在比较和累加时有精度误差房价涉及结算和报表统计用BigDecimal能避免显示 99.99 实际是 99.99000001这种玄学差异。type和floor是看板分组和筛选的条件预留了按楼层看房态的扩展空间。3.2 入住登记接口从 Controller 到 Mapper 的完整调用链入住登记是整个系统最典型的业务操作一次请求要同时影响订单、房间、入住记录三张表。完整拆一遍这个项目的分层协作方式就清楚了。先看 Controller它只负责接收参数、调用服务、返回结果RestController RequestMapping(/api/checkin) public class CheckInController { Autowired private CheckInService checkInService; PostMapping public Result checkIn(RequestBody CheckInRequest request) { checkInService.checkIn(request.getOrderId()); return Result.success(); } }CheckInRequest是前端传入的 JSON 请求体这里只取了orderId——入住一定对应一条已预订的订单由订单找到房间而不是直接传房间号。这样设计是为了防止出现订单 A 的房间被订单 B 办了入住的数据错乱。业务逻辑全部在 Service 层加上Transactional保证多表更新的原子性Service public class CheckInService { Autowired private RoomMapper roomMapper; Autowired private OrderMapper orderMapper; Autowired private StayRecordMapper stayRecordMapper; Transactional(rollbackFor Exception.class) public void checkIn(Long orderId) { // 1. 查订单状态必须是已预订 Order order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } if (!BOOKED.equals(order.getStatus())) { throw new BusinessException(订单状态不是已预订无法办理入住); } // 2. 查房间状态必须是已预订 Room room roomMapper.selectById(order.getRoomId()); if (!BOOKED.equals(room.getStatus())) { throw new BusinessException(房间当前不可入住); } // 3. 更新订单状态和房间状态 orderMapper.updateStatus(orderId, CHECKED_IN); roomMapper.updateStatus(room.getId(), CHECKED_IN); // 4. 写入入住记录留审计痕迹 StayRecord record new StayRecord(); record.setOrderId(orderId); record.setRoomId(room.getId()); record.setCheckInTime(new Date()); stayRecordMapper.insert(record); } }四个步骤对应四层逻辑第一步和第二步是先查后改订单和房间两个状态同时满足才继续防止脏数据被写进去。第三步连着两个updateStatus必须同时成功任一步失败就整体回滚。第四步写入入住记录后续退房、换房、开发票都基于这条记录相当于给每次入住留了账簿。Transactional(rollbackFor Exception.class)这里有个细节rollbackFor必须显式指定为Exception.class否则 Spring 默认只对RuntimeException回滚。如果自定义的BusinessException继承的是Exception不加这个参数事务就不会回滚我在第 5 章专门讲这个坑。Mapper 层是接口定义注意多参数方法必须加Param注解Mapper public interface RoomMapper { Room selectById(Long id); int updateStatus(Param(id) Long id, Param(status) String status); }对应的 XML 里updateStatus用动态 SQL 只更新传入的字段update idupdateStatus UPDATE room set if teststatus ! nullstatus #{status}/if /set WHERE id #{id} /update#{}是预编译占位符MyBatis 会转成?走 PreparedStatement既能防 SQL 注入又能在status为 null 时不覆盖原值。这里绝对不能写成${}那是字符串拼接SQL 注入风险极大这是写 MyBatis 的底线。3.3 前端房间看板Vue 组件如何刷新状态前端最核心的页面是房间看板RoomBoard.vue整层楼的房间铺成格子每个格子显示房号、房型和状态标签。前台一天里 90% 的时间盯的就是这个页面。template div classroom-board el-row :gutter10 el-col v-forroom in roomList :keyroom.id :span4 el-card :classroom-card room- room.status click.nativehandleRoomClick(room) div classroom-no{{ room.roomNo }}/div div classroom-type{{ room.type }}/div el-tag :typestatusTagType(room.status) {{ statusText(room.status) }} /el-tag /el-card /el-col /el-row /div /template script import { listRooms } from /api/room export default { data() { return { roomList: [], timer: null } }, created() { this.fetchRooms() // 30 秒轮询房态保证多台前台电脑看到的是一致的 this.timer setInterval(this.fetchRooms, 30000) }, methods: { async fetchRooms() { const res await listRooms({ status: }) if (res.code 200) { this.roomList res.data } }, statusText(status) { const map { AVAILABLE: 可售, BOOKED: 已预订, CHECKED_IN: 已入住, REPAIR: 维修中 } return map[status] || status }, handleRoomClick(room) { // 按状态跳转到对应操作可售去下单已预订去入住 if (room.status AVAILABLE) { this.$router.push({ path: /order/create, query: { roomId: room.id } }) } else if (room.status BOOKED) { this.$router.push({ path: /checkin, query: { orderId: room.currentOrderId } }) } } }, beforeDestroy() { clearInterval(this.timer) } } /script几个关键点roomList来自listRooms接口实际请求是GET /api/room/list后端按状态过滤返回 JSON 数组。看板用 30 秒轮询而不是 WebSocket 推送这是复杂度取舍——酒店前台操作频率低30 秒刷一次房态足够没必要为实时引入 WebSocket 的心跳和重连逻辑。想做秒级刷新改setInterval的时间就行后端接口不用动。statusTagType把状态映射成 UI 标签颜色可售绿、已预订橙、已入住蓝、维修中灰前台扫一眼颜色就知道哪些房能卖。点击格子按状态路由到不同操作页符合前台的肌肉记忆——看到绿格子点进去就是下单不用去订单列表翻。注意timer在beforeDestroy里必须清除否则组件销毁后定时器还在跑接口一直请求这是 Vue 组件开发最常见的内存泄漏点页面切走再切回来就多一个定时器。4. 本地跑通全流程建库、启后端、起前端的三步联调4.1 初始化数据库五张表的关系与建库脚本源码里自带sql/init.sql这是整个项目的地基。执行前先建一个专用库字符集必须用 utf8mb4mysql -uroot -p # 密码验证通过后执行 CREATE DATABASE hotel_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hotel_admin; SOURCE /path/to/sql/init.sql;utf8mb4不要图省事换成utf8MySQL 的utf8最多存 3 字节遇到生僻字或 Emoji 会直接报错或落库成问号。酒店客户姓名里有生僻字的概率不低这一步省不得。init.sql 建了五张表关系如下表名作用关键字段关联关系room房间信息id, room_no, type, price, status订单表通过 room_id 关联orders预订订单id, room_id, customer_name, check_in_date, check_out_date, statusroom_id 指向 room.idcustomer客户信息id, name, phone, id_cardorders 冗余了姓名电话减少联表stay_record入住记录id, order_id, room_id, check_in_time, check_out_timeorder_id 指向 orders.idsys_user系统登录用户id, username, password, role独立不参与业务流转orders表冗余存了customer_name和customer_phone而不是 joincustomer表这是刻意的取舍——订单列表是查询频率最高的页面不 join 能少一次表关联客户名和电话变更频率极低冗余存储的代价可以忽略。init.sql 里还埋了测试数据三层楼共 21 间房每层 7 间四个状态都有覆盖保证你第一次启动就能看到完整的房态看板效果不用自己再补数据。4.2 启动 SpringBoot 后端配置数据源与自检接口后端是标准 Maven 工程启动前先改application.yml的数据源指向本地 MySQLserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hotel_admin?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hotel.admin.entity参数逐个说serverTimezoneAsia/Shanghai必须加MySQL 8 以上的驱动默认按 UTC 处理时间不加的话查出来的日期比北京时间早 8 小时。useUnicodetruecharacterEncodingutf8保证中文参数和返回值不乱码。mapper-locations指定 XML 文件位置type-aliases-package让 XML 里写resultTypeRoom时能自动找到实体类省去写全限定类名。然后启动cd backend mvn spring-boot:run日志出现Tomcat started on port(s): 8080就说明起来了。上线部署时改成mvn clean package然后java -jar target/hotel-admin-0.0.1-SNAPSHOT.jar一样的效果。启动后用 curl 自检接口curl http://localhost:8080/api/room/list?statusAVAILABLE返回 JSON 里code是 200 且data里有房间数组说明数据库连接和 MyBatis 映射都正常。这一步是联调前必须做的自检能把大量前端白屏其实是后端没起来的排查时间省掉。4.3 启动 Vue 前端代理配置与最小验证闭环前端是 Vue CLI 工程先装依赖再起开发服务器cd frontend npm install npm run serve默认端口是 8080和后端冲突Vue CLI 会提示换端口输入y自动改到 8081。浏览器访问http://localhost:8081打开登录页。这里有个关键配置前端开发服务器必须把/api请求代理到后端 8080否则浏览器直接请求跨域端口会被同源策略拦下。改vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }changeOrigin: true表示代理时改写请求头里的 Host让后端认为请求来自本机。配置之后前端代码里axios.get(/api/room/list)实际被转发到http://localhost:8080/api/room/list前后端联调就通了。注意代理配置改了要重启npm run serve它不走热更新这是最容易踩的空跑。跑通之后做最小验证闭环打开房间看板确认 21 个房间的格子颜色和状态文字正确点一个可售房间下单回看板确认格子变成已预订橙色再点这个房间办理入住刷新后确认变已入住蓝色。这三步全过说明前端到后端再到数据库的链路、状态机流转、页面刷新逻辑都没问题。提示开发阶段前后端分两个端口跑靠代理转发生产部署时用 Nginx 把/api反向代理到后端 SpringBoot 服务前端打包后的静态文件由 Nginx 直接托管同一端口对外省掉跨域问题。5. 避坑指南联调期最容易翻车的五个场景5.1 跨域报错接口明明能通页面却一片空白现象后端接口用 curl 测完全正常前端页面请求时浏览器控制台报No Access-Control-Allow-Origin header is present页面数据加载不出来。原因前端在 8081后端在 8080浏览器同源策略拦截了跨端口请求。多数情况是vue.config.js里的代理路径前缀和 axios 实际请求的路径不一致或者代理文件改了没重启。解决先确认前端请求路径以/api开头再确认代理配置的 key 也是/api两者完全一致才命中。改完配置必须重启npm run serve。生产环境用 Nginx 反向代理统一端口比在后端放开 CORS 更安全——放开跨域等于允许任意网站调你的接口。5.2 日期序列化时间变成一串数字现象前端拿到checkInTime显示成1720000000000这种毫秒时间戳或者格式是2024-07-01T10:00:00.00000:00跟设计稿完全不一样。原因后端返回的java.util.Date被 Jackson 默认序列化成时间戳以及时区没对齐序列化出来是 UTC 时间。解决在application.yml统一配置格式实体字段上用JsonFormat兜底spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/ShanghaiJsonFormat(pattern yyyy-MM-dd, timezone GMT8) private Date checkInDate;注意date-format只对java.util.Date生效对 Java 8 的LocalDateTime/LocalDate不生效需要额外注册 JavaTimeModule。这个项目用的是Date按上面配就行。如果你二次开发用了LocalDateTime记得单独处理。5.3 事务不生效订单和房间各改各的现象接口内部抛了异常日志里有报错但数据库里订单状态变了、房间状态没变或者反过来数据成了半成品。原因Transactional只对 Spring 管理的 Bean 外部调用生效。两个最经典的翻车点一是同一个类里 A 方法直接调 B 方法B 上的Transactional完全失效因为走的是this调用而不是代理对象二是rollbackFor没写自定义异常继承自Exception而不是RuntimeException默认不回滚。解决事务方法入口必须从外部调用代码里不要同类自调用需要自调用就拆成两个 Bean 或注入自己。rollbackFor Exception.class写死不留默认值。验证方法很暴力但有效在事务方法里故意抛一个自定义异常跑完看数据有没有变化没变说明事务生效。5.4 状态枚举不一致前端显示与数据库存储脱节现象房间看板某个格子状态显示空白或乱码点操作按钮后提示非法状态。原因前后端对状态值的叫法不统一。前端某处用了中文当 value后端存的是英文枚举或者后端两个地方分别写了CHECKED_IN和CHECK_IN拼写不一致查出来的值前端不认识。解决数据库永远存英文字符串前端只做显示层映射。把状态常量收敛到一个类里统一管理public class RoomStatus { public static final String AVAILABLE AVAILABLE; public static final String BOOKED BOOKED; public static final String CHECKED_IN CHECKED_IN; public static final String REPAIR REPAIR; }前端同样建一个状态映射常量文件两边各守一份但以数据库存储的值为唯一真相。加新状态时先改数据库可接受的取值范围再改后端常量最后动前端映射顺序不能反。5.5 MySQL 时区偏移入住时间差 8 小时现象前端显示入住时间是2024-06-30 16:00:00实际北京时间应该是2024-07-01 00:00:00所有时间记录都偏了 8 小时。原因JDBC 连接串里的serverTimezone没配或配成了UTC。MySQL 服务端时区和驱动时区不一致驱动在读写DATETIME时做了额外偏移。解决连接串固定写serverTimezoneAsia/Shanghai同时检查 MySQL 服务端时区SELECT global.time_zone;如果返回SYSTEM建议在 MySQL 配置文件里显式设置default-time-zone 08:00并重启服务。连接串和服务端两处一致时间就不会再漂。这个坑最隐蔽的地方在于不查具体时间记录根本发现不了因为所有日期看起来都差不多对。6. 二次开发思路给系统加一个会员积分模块跑通之后最有价值的练习是加一个真实需求。我推荐加会员积分它不改变现有订单和房间的核心流程但要动表结构、接口、前端页面能完整走一遍开发闭环。先设计数据模型。积分挂在客户名下每次完成入住获得积分下次预订可抵扣。建一张新表CREATE TABLE member_point ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT NOT NULL, total_point INT DEFAULT 0, used_point INT DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );然后改造退房接口。在退房事务里追加积分累计逻辑按订单金额 1 元积 1 分Transactional(rollbackFor Exception.class) public void checkout(Long stayRecordId) { // 原有退房逻辑房间回可售、订单置完成、写入退房时间 StayRecord record stayRecordMapper.selectById(stayRecordId); roomMapper.updateStatus(record.getRoomId(), RoomStatus.AVAILABLE); orderMapper.updateStatus(record.getOrderId(), FINISHED); stayRecordMapper.updateCheckOutTime(stayRecordId, new Date()); // 新增累计积分 Order order orderMapper.selectById(record.getOrderId()); int points order.getAmount().intValue(); memberPointMapper.addPoint(order.getCustomerId(), points); }积分抵扣放在下单接口前端下单页加使用积分抵扣选项后端收到usePoint参数后校验积分余额、计算抵扣金额、扣减积分再生成订单。这样一套改造下来你实际动了五层——SQL 表、Mapper、Service、Controller、Vue 页面覆盖了这个项目的全部套路。验证也分两步走用测试客户开房、入住、退房查member_point表确认积分增加同一客户再下新订单传usePoint100确认订单金额减了、积分余额也扣了。两个链路跑通你对这个项目的掌控力就从能跑变成了能改。说句实在话我每次拿到别人写的管理类系统第一件事不是通读代码而是先给自己加一个小需求——加的过程中框架的约束、数据模型的问题、接口设计的别扭之处全部现形。从那以后我每次接手新项目都强制先走一遍这个流程跑通、加需求、再回头读代码。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站