拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue酒店客房管理系统:从业务设计到部署上线

做了三年酒店信息化相关项目我越来越明白一件事很多中小型酒店不是缺客源是缺一套能把前台从重复劳动里解放出来的管理工具。前台每天要在纸上记房态、在Excel里查订单、在微信里和保洁确认退房进度高峰期一忙就是各种手忙脚乱。这套基于SpringBootVueMyBatisMySQL的酒店客房管理系统就是冲着这些真实痛点来的——它覆盖预订、入住、退房、房态管理、订单查询和简单的营收统计源码完整可以直接拿去改。如果你正准备做毕业设计、公司内部系统重构或者想入行Java全栈开发这套系统的技术栈正好是当前企业级项目的主流搭配SpringBoot负责后端业务Vue负责前端页面交互MyBatis操作数据库MySQL做数据存储。这篇文章不打算重复那些官方文档里的套话我直接把系统从业务设计到数据库建模、从后端接口到前端页面、从本地启动到部署上线的完整思路拆开讲包括踩过的坑和最后留下的教训。1. 酒店客房管理的业务起点先弄清前台每天都在忙什么做一个系统最忌讳一上来就建表写代码。我接触过不少酒店管理者他们嘴上说要个能查订单的系统实际上真正困扰他们的是房态信息不透明、交接班容易出错、退房后保洁跟进不及时这些事。所以设计这套系统之前我先把前台的日常业务流程从头到尾捋了一遍。1.1 前台每天要处理的信息流一间房从客人入住到离店围绕它转的信息至少有四类房间状态、客户资料、订单流水、价格记录。房间状态空净房、住客房、脏房、维修房每一种状态都对应不同的可售状态。客户资料姓名、手机号、身份证号、入住时间老客户还要记录偏好。订单流水预订渠道电话、OTA、到店、房型、间数、入住和离店日期、实收金额。价格记录门市价、协议价、会员价、节假日上浮价。这些信息在传统手工流程里是分散的。前台一个登记本财务一个Excel表客房部用对讲机同步保洁状态。系统的任务就是把分散的信息统一收口让同一个数据源被不同岗位的人使用。1.2 从预订到离店的业务闭环我把整个业务流程画成了几个状态节点这也是后面数据库设计和后端状态机的基础客人来电或到店咨询前台查询指定日期内可售房型。确认房型和价格后生成预订订单订单状态为已预订。客人到店前台办理入住分配具体房间订单状态变为在住。住店期间可能产生加床、餐饮、洗衣等额外费用统一挂账到订单。客人退房前台结算所有费用更新房态为脏房订单状态变为已退房。保洁打扫完毕在系统中将房态更新为空净房房间重新变为可售。这个闭环看起来简单但任何一个环节出问题都会影响后续环节。比如前台忘了把房间改为脏房保洁打扫完以后直接更新为空净房就会导致客房部不知道哪间需要打扫或者一间脏房被当成空净房卖出去客人推门进去发现床单没换这是最糟糕的体验。1.3 系统需要覆盖的管理边界考虑到中小型酒店的人员配置和预算这套系统没有去做那些大而全的PMS功能比如门锁对接、PMS直连OTA、复杂的财务结算。我把边界定在这几个核心点房态可视化所有房间的状态一目了然。订单全生命周期管理从预订到退房全程可追踪。客户信息建档方便二次营销。基础统计报表入住率、营收、订单量。多角色权限前台、客房、 manager 不同账号不同权限。这个范围对绝大多数中小型酒店来说是够用的。与其追求大而全不如把一个闭环做扎实。这也是我在项目启动阶段和业务方反复确认的第一件事。2. 选型逻辑为什么这套组合在中小型酒店场景里最稳SpringBootVueMyBatisMySQL这一组合放在今天已经不算新潮但论稳定性、学习成本、招人难度它依然是最优解之一。我在选型时对比过其他方案下面说说每个组件在这个项目里到底是干什么的以及为什么要这么选。2.1 SpringBoot解决的是后端开发效率问题如果不用SpringBoot用传统SSH或者纯Servlet写光配置文件就得折腾半天。SpringBoot的自动配置机制让项目可以快速启动内置Tomcat又省去了单独部署Web服务器的麻烦。在这个系统里SpringBoot主要负责接口层提供RESTful API给前端调用。业务层处理订单逻辑、价格计算、房态变更。数据层通过MyBatis访问MySQL。安全控制用拦截器校验登录状态和操作权限。SpringBoot的另一个好处是生态成熟。项目里只要引入spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java就能把骨架搭起来部署的时候直接打成一个jar包拿到服务器上java -jar就能跑。2.2 前端为什么选Vue而不是JSP模板早期酒店系统很多用JSPJSTL做页面每次跳转都要刷新整个页面操作反馈很生硬。前台录一个客户信息填完表单点保存页面整页刷新一次光标位置丢了体验很差。Vue的核心价值在数据驱动DOM页面局部刷新不需要整页跳转这在房态看板和订单录入这种高频交互场景里非常明显。我选Vue还看中两点组件化开发房型卡片、订单表格、弹窗表单都可以拆成独立组件维护成本低。生态完整Vue Router管页面跳转Vuex/Pinia管全局状态Axios管请求。项目用的是Vue 2居多因为一些老项目还在维护但实际上Vue 3Composition API写起来更清爽。如果你自己从零开始建议直接Vue 3但这套系统的核心逻辑两者是通用的。2.3 MyBatis在报表统计和复杂SQL场景中的优势JPA和MyBatis之争在技术社区里从来没停过。对我来说判断标准很简单这个项目里SQL是不是绕不开的一等公民酒店系统的几个核心查询场景比如查询所有在住订单关联客户姓名和房号比如统计某月每天的入住率和平均房价写原生SQL非常直接。MyBatis允许把SQL写在XML里调试时把SQL拿出来在Navicat里跑一遍数据对不对一目了然。这一点在排查问题时能省很多时间。MyBatis还有一个很实用的能力是动态SQL。订单查询页面通常有多个筛选条件日期范围、订单状态、客户姓名、房型用户可能填任意组合。用if标签动态拼接WHERE条件代码简洁还不会漏条件。2.4 MySQL在中小型酒店场景中的定位中小型酒店的订单量再大也达不到互联网电商的并发级别。一间100房的酒店日均订单量撑死二三百单MySQL完全能轻松扛住。选MySQL的原因更多是运维成本低、云厂商支持好、网上资料丰富。当然数据库选型要考虑两个问题并发量如果酒店是连锁集团未来要集中管理数百家门店那需要考虑读写分离或者换PostgreSQL甚至分布式数据库。数据量订单表如果每年增长几十万条需要定期归档历史数据。单店场景下MySQL配合合理的索引和SQL优化性能是完全够用的。3. 数据库建模业务稳定性的地基数据库表结构是整个系统最不能偷懒的部分。前期设计不好后期改起来会牵扯到接口、前端、报表各个层面代价非常大。我根据自己的开发经验把核心表的设计思路拆开讲。3.1 房间模型物理房与房态要解耦房间表看起来简单但有一个隐蔽的坑房间的物理属性楼层、朝向、床型和业务状态空净、住、脏、维修是不同维度的数据。比如101房永远存在但101房当前是脏房是随时变化的。建表时要注意房间基本表存房号、楼层、房型ID、房间面积、朝向、备注。这部分数据基本不会变。房型表存房型名称、床型、可住人数、基础价格、挂牌价。房态记录表存房间ID、状态、变更时间、操作人。这样能追溯房态的变更历史。不要把房态字段直接写在房间表里每次UPDATE完就完事那样出了纠纷根本说不清什么时候由谁改的状态。3.2 订单表如何应对预订、入住、续住、退房订单是整个系统最核心的表。我的设计思路是将订单拆成主表和明细表订单主表订单号、客户ID、房间ID或房型ID、入住日期、离店日期、下单时间、订单状态、订单金额、实付金额、备注。订单明细表订单ID、费用项目房费、加床费、餐饮费、赔偿费、金额、产生时间。为什么要拆成主表和明细表因为酒店订单的费用条目不是固定的房费之外随时可能加一笔洗衣费、加一张加床。如果所有费用都塞在主表里字段会越加越多最后变成一张大宽表。明细表让费用的扩展变得容易统计营收时也直接SUM明细表即可。订单状态我设计成几个常量0-已预订、1-在住、2-已退房、3-已取消、4-未到店(NoShow)。状态流转要在后端代码里控制不能直接改数据库。3.3 客户表与会员信息的存储客户表存的是客人基础信息姓名、手机号、身份证号、生日、住址、备注。这里有一个安全合规点身份证号属于敏感个人信息数据库里建议加密存储前端列表默认脱敏显示只在必要的时候明文展示。虽然增加了开发工作量但长期看是值得的。会员等级可以单独设计一张等级表或者在客户表加一个等级字段。考虑到中小型酒店会员体系比较简单我倾向于直接在客户表加member_level和points字段避免过度设计。如果有积分明细需求再单独加一张积分流水表。3.4 房价策略日期、时段和价格类型三要素酒店价格最大的特点是动态变化工作日和节假日价格不同门市价和协议价不同提前预订和当天入住价格也不同。最灵活的做法是设计一张房价表房型ID生效日期date类型价格类型门市价/协议价/会员价/节假日价单价查询某个房型在某天的价格时用日期匹配加价格类型匹配即可。这种设计虽然比在房型表里放几个固定字段复杂但胜在灵活。节假日调价只需要往表里插入新记录不需要改代码。3.5 容易被忽略的操作日志表很多中小型系统不做操作日志出了问题就靠猜。我强烈建议加一张operation_log表字段包括操作人ID、操作类型、操作内容、业务关联ID、操作时间、IP地址。前台误操作把订单金额改小了财务问起来日志里一查就知道谁什么时候改了什么。这个表带来的价值远远超过它的建造成本。4. 后端SpringBootMyBatis的实现细节后端实现是整个系统的大脑。我会把几个关键功能点的实现思路和代码片段写出来这些是项目中最有技术含量的部分。4.1 登录鉴权拦截器Token方案酒店系统的用户角色比较简单管理员、前台、客房部、财务。我的实现方式是用户登录成功后后端生成一个UUID Token存到Redis并设置过期时间返回给前端前端每次请求在Header里带Authorization: token。后端写一个拦截器统一校验所有需要登录的接口。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/user/login)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) Boolean.TRUE.equals(redisTemplate.hasKey(token))) { // 续期 redisTemplate.expire(token, 8, TimeUnit.HOURS); return true; } response.setStatus(401); return false; } }权限校验用角色控制在需要权限的接口上加注解判断或者再写一个角色拦截器。对于这个体量的系统这种轻量方案比Spring Security JWT要简单直接足够应对实际需求。4.2 订单状态机的流转与实现订单状态我前面已经列出来了但状态之间的流转规则必须在代码里明确控制。比如已预订只能流转到在住或已取消不能直接跳到已退房在住只能流转到已退房。实现方法是在Service层写统一的状态流转方法public void updateOrderStatus(Long orderId, Integer targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new RuntimeException(订单不存在); } // 校验状态流转是否合法 boolean valid checkTransition(order.getStatus(), targetStatus); if (!valid) { throw new RuntimeException(非法状态流转); } order.setStatus(targetStatus); orderMapper.updateById(order); }checkTransition里维护一个合法流转Map。这样做的好处是防止业务层代码到处散落状态更新逻辑一不小心就把状态改乱了。4.3 MyBatis动态SQL处理多条件订单查询订单查询是后台管理页面最常用的功能。用户可能根据日期范围、订单状态、客户姓名、房型来筛选条件不固定。MyBatis的动态SQL在这里作用很大。select idselectOrderList resultTypecom.example.entity.OrderVO SELECT o.*, c.name AS customerName, r.room_number AS roomNumber FROM hotel_order o LEFT JOIN customer c ON o.customer_id c.id LEFT JOIN room r ON o.room_id r.id where if teststatus ! null AND o.status #{status} /if if testcustomerName ! null and customerName ! AND c.name LIKE CONCAT(%, #{customerName}, %) /if if testroomTypeId ! null AND o.room_type_id #{roomTypeId} /if if teststartDate ! null AND o.check_in_date gt; #{startDate} /if if testendDate ! null AND o.check_out_date lt; #{endDate} /if /where ORDER BY o.create_time DESC /selectwhere标签会自动处理掉第一个AND动态拼接条件时不会出现SQL语法错误。注意gt;和lt;是XML里的转义写法别直接写和。4.4 事务控制支付和订单创建的一致性办理入住时可能要同时做几件事创建订单、扣减押金、更新房间状态、写操作日志。任何一步失败都不能留下半截数据。SpringBoot里用Transactional可以轻松搞定Transactional(rollbackFor Exception.class) public Order checkIn(CheckInDTO dto) { // 1. 创建订单 // 2. 更新房间状态为在住 // 3. 写入操作日志 // 4. 返回订单 }这里有一个并发问题很容易被忽略同一个房间前后台同时办理入住可能导致两个人分到同一间房。最简单的兜底方案是在房间表加一个status字段配合UPDATE ... WHERE status0的乐观锁思路更新时判断房间状态是否为空净房更新影响行数为0则说明房间已被占用。5. 前端Vue页面是给前台用的不是给开发看的前端部分如果只追求开发效率而忽略实际使用场景做出来的页面在真实酒店环境里会很难用。前台工作节奏快操作路径要短关键信息要显眼。5.1 房间状态看板一眼看清所有房态房间状态看板是系统首页它直接用色块代表房间状态绿色空净房、蓝色在住房、灰色脏房、红色维修房。每个房间卡片显示房号、房型、住客信息和入住时间。看板实现的核心是数据来源和更新时机。我用Vue的轮询实现setInterval(fetchRoomStatus, 30000); function fetchRoomStatus() { http.get(/room/status).then(res { roomList.value res.data }) }为什么不选择WebSocket因为WebSocket在单店场景和简单部署环境下会增加复杂度30秒轮询对100间房的体量来说服务器的压力完全可以忽略。体验上稍微有一点延迟但不会给前台造成困扰。5.2 入住登记表单的字段校验入住登记是整个系统最常用的表单。字段多包括客户姓名、手机号、身份证号、入住日期、离店日期、房型、房价等。前端要做两层校验必填校验和格式校验。Element UI/Element Plus的Form组件自带规则校验身份证号用正则做格式检查手机号也做长度和开头校验。const rules { customerName: [{ required: true, message: 请输入客户姓名, trigger: blur }], phone: [ { required: true, message: 请输入手机号, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ], idCard: [ { required: true, message: 请输入身份证号, trigger: blur }, { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: 身份证号格式不正确, trigger: blur } ] }这里有一点要注意前端校验只能提升用户体验后端接口必须做同样的校验前端弹窗提示是给人看的后端的校验才是真正拦得住脏数据的。5.3 Axios封装与接口错误处理Vue项目里我习惯在utils/request.js里统一封装Axios实例做三件事设置baseURL、请求头统一附带Token、响应拦截器统一处理错误码。import axios from axios const http axios.create({ baseURL: /api, timeout: 15000 }) http.interceptors.request.use(config { config.headers.Authorization localStorage.getItem(token) return config }) http.interceptors.response.use( res { if (res.data.code 401) { router.push(/login) } return res.data }, err { ElMessage.error(err.message) return Promise.reject(err) } ) export default http这样业务代码里就不用每个接口都写一遍错误弹窗代码会干净很多。5.4 多角色路由与按钮权限菜单权限用Vue Router的路由守卫控制。用户登录成功后后端返回当前用户的角色和权限标识列表前端根据这个列表动态生成可访问的菜单。按钮级别权限用自定义指令v-permission控制比如只有admin角色能看到删除订单按钮。具体实现是在指令里判断角色列表是否包含对应权限码不包含就移除DOM元素。6. 部署运行全记录从源码到能访问的系统拿到源码后怎么把它跑起来是很多初学者卡住的地方。这一章我按完整步骤写照着做就能在本地启动项目。6.1 本地环境准备需要准备的工具JDK 8或JDK 11SpringBoot 2.x推荐JDK 8/11太高版本可能遇到兼容问题Maven 3.6Node.js 14Vue 2项目14够用Vue 3建议16MySQL 5.7或8.0Redis如果登录方案用了Redis下载完成后在命令行验证java -version mvn -v node -v npm -v mysql -V6.2 初始化数据库在MySQL里创建数据库CREATE DATABASE hotel_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;导入项目doc目录下的hotel_system.sql或者用Navicat直接运行SQL脚本。脚本里包含建表语句和初始数据比如管理员账号、房型数据、房间数据。导入后检查一下表数量确认所有核心表都建出来了。6.3 修改配置并启动后端打开application.yml修改数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/hotel_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: localhost port: 6379在项目根目录执行mvn spring-boot:run如果看到Spring Boot启动成功的日志说明后端没问题。常见的报错有这么几种Access denied for user rootlocalhost用户名或密码不对。Unknown database hotel_system数据库没创建或名称不对。Table xxx doesnt existSQL脚本没导入完整。6.4 启动前端并解决跨域问题进入前端目录cd hotel-admin npm installnpm install容易遇到依赖下载慢或失败的问题建议用国内镜像源npm config set registry https://registry.npmmirror.com依赖装完后npm run dev前端默认跑在8080端口后端跑在8081端口这时候直接访问8080会碰到跨域问题。解决办法有两种后端加CORS配置。前端开发环境配置代理在vue.config.js里做module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端请求/api/xxx会被代理到后端8081端口避免跨域。6.5 打包部署到服务器前端构建npm run build生成dist目录把它部署到Nginx的静态目录然后配置反向代理server { listen 80; server_name your_domain.com; location / { root /var/www/hotel-admin; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; } }后端打包mvn clean package -DskipTests把生成的jar包放到服务器nohup java -jar hotel-system.jar logs.log 21 数据库地址改成云数据库或服务器本地MySQL地址即可。7. 这些坑我花了很长时间才想通最后分享几个实施过程中容易踩的坑有的坑表面上看是代码问题实际是设计思路问题。7.1 MyBatis单个数字字符比较的陷阱写MyBatis动态SQL时如果用一个字符类型的参数和数据库字段比较比如if teststatus 1 AND order.status #{status} /if这里的1在MyBatis中会被当作字符常量而不是字符串可能会报数字格式异常。正确写法是if teststatus 1 AND order.status #{status} /if外层单引号内层双引号或者用status 1.toString()。这个问题不实际踩一次很难记住排查时又不好定位。7.2 MyBatis一级缓存与二级缓存的坑MyBatis默认开启一级缓存SqlSession级别同一个Session中执行相同SQL会直接命中缓存不查询数据库。这在某些场景下会带来问题比如同一事务里先查订单再改订单金额再查订单拿到的还是旧数据。解决办法是虽然Spring管理的Mapper通常每次请求都创建新SqlSession一级缓存影响不大但如果你在循环里多次查询同一个对象的复杂统计还是要小心。二级缓存默认是关闭的除非查询非常频繁、数据一致性要求不高一般不建议开启。7.3 订单金额计算的精度问题金额计算不能直接用double或floatJava里涉及金钱一定要用BigDecimal。很多新手在前面预订接口里直接price * days一旦原价是88.5元仪表盘统计出来的营收总额就有精度误差。下面的写法是我平时用的BigDecimal price order.getPrice(); BigDecimal total price.multiply(BigDecimal.valueOf(days)) .add(order.getExtraFee()) .subtract(order.getDiscount());7.4 索引不是越多越好订单表查询频繁我在create_time、customer_id、status和room_id上建了索引。但要注意索引建多了写入时会增加维护成本。如果某张表的写操作频繁而查询很少用到某个字段就不要给那个字段建索引。还有一个经验复合索引的字段顺序很重要。比如经常用status create_time联合查询那就把这两个字段建成一个复合索引(status, create_time)而不是各建一个单列索引。学会了用EXPLAIN查看SQL执行计划索引设计才算是真正入门。8. 我在这个项目里保留的最后一个习惯系统开发完不是终点运维和后续迭代才是长期的活。我自己在交付这套系统时会给使用方留一份数据字典文档把每个表的字段含义、状态枚举值、相互关联关系写清楚。这样一来以后不管是我自己维护还是换人接手都不用对着代码猜字段含义。如果你准备基于这套系统做二次开发我的建议是先跑通完整流程再从一个具体业务痛点入手去改。比如先做房价策略按节假日设置这个小功能把后端表结构、接口、前端页面全部打通你对整个系统的理解会比只跑Demo深得多。开发过程中遇到问题先把SQL拿到数据库客户端里跑一遍确定数据本身没问题再回头查代码逻辑这是排查效率最高的一条路径。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门