SpringBoot+Vue前后端分离酒店管理系统全栈实战解析

发布时间:2026/9/14 4:12:35
SpringBoot+Vue前后端分离酒店管理系统全栈实战解析 SpringBoot Vue 前后端分离到底怎么落地我建议直接拿一个酒店管理系统来拆。这个项目不是那种只跑通一个登录页的玩具而是把 SpringBoot、Vue、MyBatis、MySQL 这几个主流技术栈全部串起来的完整业务系统有房间管理、客户入住、订单结算、统计报表、权限控制还附带可以照抄的源码结构和部署脚本。不管你是准备毕设、想系统学一遍前后端分离开发流程还是想给简历上加一个拿得出手的全栈项目这套东西都值得从头到尾过一遍。先说我为什么推荐用酒店管理系统来做练手项目。酒店管理天然自带角色划分管理员、前台、财务、核心业务流预定→入住→退房→结算、数据统计需求入住率、营收报表这三样东西恰好对应前后端分离项目里最难讲清楚的权限设计、状态流转和聚合查询。很多新手项目做来做去只有一张用户表和几条 CRUD根本练不到复杂业务场景而酒店管理系统复杂度适中正好卡在能学到东西和又不至于做不完之间。1. 项目整体设计与技术选型1.1 为什么是 SpringBoot Vue MyBatis 这套组合先说结论这是目前 Java 全栈入门性价比最高的一个技术组合。SpringBoot 解决的是后端跑起来的问题。它内置了 Tomcat做了自动化配置写一个 Controller 加一个启动类就能直接启动项目省去了传统 SSM 整合时那一大堆 XML 配置文件。前后端分离项目本身已经拆掉了视图层SpringBoot 只需要专注于提供 RESTful 接口这一层用 SpringBoot 来承载再合适不过。Vue 解决的是前端交互的问题。它采用组件化开发方式页面上的房态格子、订单列表、统计图表各自封装成组件数据变化时页面自动更新不需要像 jQuery 时代那样手动操作 DOM。配合 Vue Router 做前端路由、Vuex 或 Pinia 管理全局状态一个单页应用SPA所需的全部基础设施都有了。MyBatis 解决的是数据库操作这一层。跟 JPA/Hibernate 这种全自动 ORM 框架相比MyBatis 把 SQL 的控制权完全交给你在写多表关联查询、复杂条件筛选、动态 SQL 这些场景下非常有优势。酒店管理系统里不可避免要写查询当前时间段内可用的房间、统计每个月的入住率这类稍微复杂的 SQLMyBatis 写起来比 JPA 直观得多出问题也好排查。至于 MySQL这里不用多说它是目前中小型项目使用最广泛的关系型数据库免费、稳定、生态成熟配合 Navicat 或 MySQL Workbench 管理工具开发效率非常高。额外多说一句有人会纠结 MyBatis 和 MyBatis-Plus 到底选哪个。如果是商业项目直接用 MyBatis-Plus 确实省事它内置了通用 CRUD、分页插件、逻辑删除这些高频功能。但如果是学习项目或者毕设我建议先用原生 MyBatis 把基础 CRUD 和多表查询自己写一遍搞清楚 SQL 是怎么映射到接口的然后再去用增强工具。这个底层逻辑搞清楚了后面用任何 ORM 都不会觉得玄乎。1.2 前后端分离的架构思路拆解前后端分离并不是简单地把代码分成两个目录而是把数据的提供和数据的展示彻底拆成两个独立的应用。在这套架构里后端项目只负责接收 HTTP 请求处理业务逻辑操作数据库然后返回 JSON 数据。它不关心数据最终是展示在网页上、小程序里还是 App 端——只要按约定好的接口格式返回数据谁来调用都行。前端项目只负责调用后端接口拿到 JSON 之后渲染页面、响应用户操作。用户在页面上看到的所有界面效果和交互逻辑全部由前端代码控制。两边通过 HTTP 协议进行通信具体一点就是前端用 Axios 发起请求后端用 SpringBoot 的 RestController 接收请求、返回 JSON。通信格式一般有两种选择要么用传统的 JSON要么用 RESTful 风格的接口设计。我实际开发中更推荐 RESTful 风格比如GET /api/room/list 查询房间列表 POST /api/room 新增房间 PUT /api/room/{id} 修改房间信息 DELETE /api/room/{id} 删除房间这种风格的好处是接口语义很清晰通过 HTTP 方法和 URL 就能知道这个接口在做什么操作前后端联调时也不用费劲去文档里找删除房间的接口路径到底是什么。明确了基本架构之后还有一个必须重视的问题是跨域CORS。前后端分离项目在开发阶段通常是前端跑在 http://localhost:8080后端跑在 http://localhost:9090浏览器会拦截非同源的请求这时候就需要在后端配置跨域放行。实际项目中我一般在 SpringBoot 里用 CorsFilter 全局处理或者用 CrossOrigin 注解解决。注意跨域问题只在开发阶段比较突出。线上部署时如果用 Nginx 做反向代理把前端静态文件和后端接口统一代理到同一个域名下就不存在跨域问题了。所以配置跨域时要考虑到开发和生产环境的不同。2. 数据库设计与需求拆解2.1 核心业务表怎么设计酒店管理系统的数据库设计其实并不复杂但里面有几个值得注意的点。先看核心的表有哪些管理员表admin存放登录账号、密码、角色房间类型表room_type存放单人房、双人房、套房等类型信息包括价格、面积、床型、可住人数房间表room存放具体的房间如 801、802每个房间关联一种房间类型客户表customer记录入住客户的基本信息预订订单表reservation记录客户预订的信息入住记录表checkin记录实际入住的信息退房结算表checkout记录退房和结算信息为什么房间表和房间类型表要拆开这是初学者比较容易忽略的地方。如果不拆直接在房间表里存房间类型名称和价格当套房价格从 500 涨到 600 时你得把所有类型为套房的房间记录全改一遍拆开之后只需要改房间类型表里的一条数据所有关联的房间自动生效。这就是数据库设计里常说的消除数据冗余。再补充一个设计中容易忽略的问题酒店系统的房价其实是分时段变化的节假日和平时的价格不一样。如果需求里明确了这一点需要再拆出一张房价策略表或者在订单表里直接冗余一个成交单价字段。我的建议是订单表里一定要存一份当时的成交单价和总金额快照不要退房时临时去查当前价格——因为价格会变你无法用现在的价格去结算之前的订单到时候对不上账很难排查。2.2 建表语句的关键点核心表设计好后建表时几个关键字段有讲究。第一主键建议使用自增 INT 或 BIGINT项目规模不大时完全够用不用上雪花算法那套复杂的东西。第二金额字段建议用 DECIMAL(10, 2)不要用 FLOAT 或 DOUBLE。浮点数在计算时会产生精度误差0.1 0.2 可能等于 0.30000000000000004这在涉及钱的场景下绝对不能接受。DECIMAL 是精确数值类型专门用来处理这种情况。第三业务状态字段建议用 TINYINT 存数字而不是直接存字符串。比如房间状态0 表示空闲1 表示已入住2 表示打扫中订单状态0 待支付1 已支付2 已入住3 已退房4 已取消。用数字存储的好处是查询效率高、省空间、代码里用常量去对应即可如果只用中文状态后期做多语言版本或者状态调整时会非常痛苦。第四时间字段统一用 DATETIME 类型Java 实体类里对应 LocalDateTime不要用老旧的 java.util.Date。下方是一个精简版的房间表和订单表结构示例CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL COMMENT 房间编号, room_type_id INT NOT NULL COMMENT 房间类型ID, floor INT DEFAULT NULL COMMENT 所在楼层, status TINYINT DEFAULT 0 COMMENT 0空闲 1已入住 2打扫中 3维修中, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, customer_name VARCHAR(50) NOT NULL, customer_phone VARCHAR(20) NOT NULL, room_id INT NOT NULL COMMENT 房间ID, room_type_id INT NOT NULL COMMENT 房间类型ID, check_in_date DATE NOT NULL COMMENT 预计入住日期, check_out_date DATE NOT NULL COMMENT 预计退房日期, price DECIMAL(10,2) NOT NULL COMMENT 每晚单价, total_amount DECIMAL(10,2) NOT NULL COMMENT 总金额, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已入住 3已退房 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意字符集用 utf8mb4不是 utf8。utf8 在 MySQL 里最多只能存 3 个字节的字符遇到 Emoji 表情就会报错utf8mb4 是真正的四字节 UTF-8 编码兼容 Emoji 和生僻字现在建表基本无脑用 utf8mb4。3. 后端核心实现解析3.1 SpringBoot 环境搭建与版本选择SpringBoot 的版本选择说实话很多初学者在这里踩过坑。SpringBoot 2.x 和 3.x 差别非常大3.x 要求 JDK 17 及以上底层是基于 Jakarta EE 的很多依赖包的命名空间都变了而 2.x 最多支持到 JDK 8 或 11。我的建议是学习项目优先用 SpringBoot 2.7.x JDK 8 或 11。不是说 3.x 不好而是网上 90% 的教程、遇到问题时的博客文章、以及各种框架的兼容版本都是基于 2.x 的。你跟着教程做用的 JDK 8 SpringBoot 2.7 组合遇到问题搜解决方案基本一搜一个准一上来就用 SpringBoot 3.x 配 JDK 17版本兼容问题就能让你折腾好几天。提示如果非要用 SpringBoot 3.x重点检查 MyBatis-Spring-Boot-Starter 的版本是否支持建议直接用 mybatis-spring-boot-starter 3.0.3 及以上版本并且确认所有依赖都对应 Jakarta 命名空间。创建项目的方式我推荐用 IDEA 内置的 Spring Initializr步骤是File → New → Project → Spring Initializr → 选择 JDK 版本 → 勾选 Web、MySQL Driver、MyBatis Framework 这几个依赖。注意国内的网络环境访问 Spring Initializr 官方服务偶尔会超时可以把 Server URL 改成阿里云的镜像地址 https://start.aliyun.com 速度会快很多。3.2 MyBatis 整合与缓存机制MyBatis 和 SpringBoot 整合之后主要的配置集中在 application.yml 文件里spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: 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有几个配置项需要重点关注。首先是 mapper-locations 指定了 XML 映射文件的位置这是 MyBatis 的核心——你写的 SQL 都放在 resources/mapper 目录下的 XML 文件里每一个方法对应一条或一组 SQL。其次是 map-underscore-to-camel-case 设置为 true这个配置可以把数据库的 user_name 字段自动映射成实体类里的 userName 属性省去大量的字段映射配置。强烈建议建表时统一用下划线命名法Java 实体类统一用驼峰命名法配合这个开关直接打通。最后是 log-impl 指定了 StdOutImpl它会把执行的 SQL 和参数打印到控制台。开发阶段这个选项非常有用但部署到生产环境时必须关掉否则 SQL 日志刷屏会导致严重的性能问题。关于 MyBatis 缓存这也是面试里经常问的点。MyBatis 有一级缓存和二级缓存。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 中执行相同的查询会直接返回缓存结果二级缓存是 namespace 级别的可以跨 SqlSession 共享需要手动开启。实际项目里我的建议是小项目直接用 MyBatis 自带的二级缓存就够了但要注意缓存失效的场景。如果同时有多个操作写入同一张表开启二级缓存后可能出现脏读问题这时候需要调用 flushCache 属性或者交给业务层用 Redis 做缓存更稳妥。酒店项目的房间状态数据更新频繁用二级缓存反而容易出现数据不一致的情况这种情况关掉二级缓存配合 Redis 做热点数据的缓存效果更好。3.3 多表关联与动态 SQL 实战酒店管理系统的后端逻辑里最核心也最复杂的 SQL 是查询指定日期区间内可预订的房间。这个需求表面上看起来简单——查哪些房间在某个时间段内没有被订单占用——但实际写出来的 SQL 会用到多表关联和子查询。比如要查询 2025-06-01 到 2025-06-03 期间可用的房间核心思路是先找出这个时间段内所有被占用的房间 ID再看哪些房间类型有可用房间最后关联房间类型表返回详情信息。select idfindAvailableRooms resultTypeRoomVO SELECT r.id, r.room_no, r.floor, r.status, rt.type_name, rt.price, rt.bed_type, rt.max_people FROM room r LEFT JOIN room_type rt ON r.room_type_id rt.id WHERE r.status 0 AND r.id NOT IN ( SELECT room_id FROM reservation WHERE status IN (1, 2) AND #{checkInDate} lt; check_out_date AND #{checkOutDate} gt; check_in_date ) /select这里的核心逻辑是判断两个日期区间是否有重叠。公式是新订单的入住日期 已有订单的退房日期 且 新订单的退房日期 已有订单的入住日期只要这两个条件同时成立说明时间区间有交集这个房间在目标时间段内就被占用了。 和 在 XML 文件里必须转义成 和 这是我在实际开发中遇到过的很低级但很常见的报错。如果不转义XML 解析器会报错。也可以在 MyBatis 的配置里开启 CDATA 标签包裹 SQL 解决这个问题但我更喜欢用转义因为写起来直观。动态 SQL 是 MyBatis 的另一个核心能力。酒店管理系统的房间管理页面通常有多个筛选条件房间号、楼层、状态、房间类型。在 MyBatis 的 XML 里可以用 和 标签实现动态条件拼接select idfindRoomList resultTypeRoomVO SELECT r.*, rt.type_name, rt.price FROM room r LEFT JOIN room_type rt ON r.room_type_id rt.id where if testroomNo ! null and roomNo ! AND r.room_no LIKE CONCAT(%, #{roomNo}, %) /if if testfloor ! null AND r.floor #{floor} /if if teststatus ! null AND r.status #{status} /if if testroomTypeId ! null AND r.room_type_id #{roomTypeId} /if /where ORDER BY r.floor ASC, r.room_no ASC /select标签会自动处理关键词 AND 或 OR 的问题——如果条件都不满足它不会生成多余的 WHERE 子句也不会出现 SQL 语法错误。这种写法比用字符串拼接条件再手动判断是否加了 WHERE要优雅和安全得多还能顺便防止一部分 SQL 注入问题。3.4 JWT 登录鉴权与接口保护前后端分离项目的登录认证方案绝大多数情况都会选择 JWTJSON Web Token。为什么不用 Session因为 Session 是依附于服务器的前端拿到了 Session ID 到 Cookie 里跨域请求时 Cookie 的携带规则比较麻烦而且如果将来要做成多实例部署Session 还需要额外做会话共享。JWT 是无状态的服务端不保存任何会话信息用户登录成功之后服务端签发一个 Token前端每次请求时把 Token 放到请求头里带过来服务端验签通过就放行。JWT 的结构是由 Header、Payload、Signature 三部分组成的。Header 声明了签名算法和 Token 类型Payload 里存放用户ID、用户名、角色权限、过期时间等自定义字段Signature 部分由服务端用密钥对前面两段进行签名生成用来防止数据被篡改。在 SpringBoot 项目中整合 JWT我一般用 jjwt 库是官方做 JWT 解析的 Java 库。public class JwtUtil { private static final String SECRET your-secret-key-please-change-in-production; private static final long EXPIRE_TIME 7 * 24 * 60 * 60 * 1000; // 7天 public static String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }有了 JwtUtil 工具类之后还需要实现一个拦截器来做统一鉴权。这里有两个方案一个是实现 Spring MVC 的 HandlerInterceptor只拦截需要认证的路径另一个是借助 Spring Security 框架来统一管理。酒店管理系统如果要快速实现、不引入太多复杂依赖直接用拦截器就够了如果项目本身还要应付更细粒度的权限控制比如不同的角色只能访问指定的管理菜单那上 Spring Security 会更顺理成章。我们的项目里登录用户角色分成管理员和普通前台工作人员他们能看到的菜单和可执行的接口是不同的。拦截器里实现路径级别权限控制时至少要做三件事放行登录接口/api/login、/api/register以及静态资源检查请求头里是否存在 Token 且 Token 是否有效如果角色不匹配返回 403 Forbidden 错误码核心代码如下Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } // 如果前端带了Bearer 前缀需要截取 if (token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.getSubject()); request.setAttribute(role, claims.get(role)); // 这里可以根据 role 做更细粒度的权限判断 return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\Token无效或已过期\}); return false; } }同时要把拦截器注册到 WebMvcConfigurer 中注意排除登录接口和静态资源路径否则页面还没打开就被拦截器挡在外面了。注意所有涉及金额、确定入住、确定退房这类影响核心业务状态的接口必须做后台校验。前端传什么就信什么是最危险的做法。比如退房结算的时候前端不能直接把总金额传给后端后端的正确做法是只接收订单 ID自己去数据库查出入住时间、房价、然后计算总金额。前端传过来的数据只能作为辅助参数。4. 前端核心实现解析4.1 Vue 环境准备与项目创建前端部分我选择 Vue 2 Element UI 还是 Vue 3 Element Plus这里要先说清楚。如果你的目标是快速完成一个能跑的前后端分离项目并且希望踩坑最少Vue 2 Element UI 的思路会更稳妥网上资料多遇到问题基本上有现成答案如果是从零开始学习前端直接迈入 Vue 3 Composition API Element Plus 也没毛病毕竟是未来的主流方向。不管是哪一套环境准备是固定的先装 Node.jsnpm 是随 Node.js 一起安装的包管理工具。国内用户最好把 npm 源切换成淘宝镜像npm config set registry https://registry.npmmirror.com然后使用 Vue CLI 创建项目Vue 3 也可以用 Vite速度更快但对于初学者我建议用 Vue CLI因为它的 webpack 配置文档多npm install -g vue/cli vue create hotel-admin-front创建时需要选择预设勾选 Router 和 Vuex或 Pinia这两个分别是前端路由和状态管理工具。前端项目的基础目录结构大概长这样src/ |-- api/ # 所有接口调用的封装 |-- assets/ # 静态资源如图片、全局样式 |-- components/ # 公共组件 |-- router/ # 路由配置 |-- store/ # 全局状态管理 |-- views/ # 页面组件 |-- utils/ # 工具方法 |-- App.vue # 根组件 |-- main.js # 入口文件api 目录的用途是统一管理所有的接口调用不要在组件里直接写 axios.get(xxx)那样一来接口地址散落各处维护成本直线上升。建议每种业务模块单独建一个文件。4.2 Axios 封装与 Token 注入前端最核心的基础设施是 Axios 的请求封装。为什么必须封装而不是直接在页面里调 axios因为你需要处理两件重复性的事第一每个请求都得带上 Token第二后端返回 401 表示 Token 过期时前端需要统一跳到登录页面。我在项目里通常这样封装// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, // 开发时通过 Vite/Vue CLI 配置代理生产由 Nginx 转发 timeout: 15000 }) // 请求拦截器注入 Token service.interceptors.request.use(config { const token localStorage.getItem(hotel_token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理异常 service.interceptors.response.use(response { const res response.data // 后端约定 code200 表示业务成功 if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(hotel_token) router.push(/login) } else { Message.error(error.message || 网络异常) } return Promise.reject(error) }) export default service接口调用文件里再引入这个 service 实例// api/room.js import request from /utils/request export function getRoomList(params) { return request({ url: /room/list, method: get, params }) } export function addRoom(data) { return request({ url: /room, method: post, data }) }这样封装之后页面组件里的调用就非常简单了import { getRoomList } from /api/room async function loadRooms() { const loading this.$loading({ text: 加载中 }) try { const res await getRoomList({ status: 1, page: 1, limit: 10 }) this.roomList res.data.list } finally { loading.close() } }Axios 的请求和响应拦截器这类公共逻辑写一次就能在整个项目里生效这是前后端分离项目前端工程化最基本也是收益最高的实践。开发阶段的跨域问题前端这边可以采用代理的方式解决不需要后端每次启动都配置 CORS。在 Vue CLI 项目的 vue.config.js 里配置 devServer 代理module.exports { devServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }这样前端发的 /api 开头的请求在开发阶段会自动转发到后端的 9090 端口浏览器里看到的是同源的请求不会触发跨域。4.3 路由设计与权限守卫前端路由使用 Vue Router页面结构大致分为登录页、后台主布局包含侧边菜单和内容区、各个业务功能页房间管理、订单管理、客户管理、统计报表等。核心策略是把登录页单独放在 /login其他页面放在 /layout 这个父路由下。 /layout 内部结构固定为顶部导航栏 左侧菜单 中间内容区。菜单项与路由配置对应。路由守卫是防未登录直接输入 URL 跳进后台的关键。在 router/index.js 里配置全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(hotel_token) if (to.path /login) { next() } else { if (!token) { next(/login) } else { next() } } })路由守卫只做是否登录的拦截角色权限层面后端接口做校验前端主要控制菜单的显隐。比如管理员登录后能看到员工管理菜单前台登录后看不到这些通过菜单数组和角色字段做条件渲染即可。但页面能不能进去、接口能不能调通后端是最终的防线。4.4 核心业务页面实现思路房态管理页面是酒店管理系统里最有意思的一个页面。它像酒店大堂的宣传屏一样用一个个小格子展示每间房的当前状态绿色代表空闲、红色代表已入住、黄色代表打扫中、灰色代表维修中。前端实现这种页面本质上就是拿到一个房间列表数据之后根据每个房间的状态字段绑定对应的 CSS class。点一个空闲房间弹出操作框可以直接下单办理入住。订单管理页面就要复杂一点了。列表默认加载一周之内的订单支持按状态筛选待支付/已支付/已入住/已退房/已取消还能查看每条订单的详情。退房操作的流程是点击退房→后端校验是否有未结清费用→计算总消费金额和押金退款→生成结算记录→改变房间状态为打扫中→改变订单状态为已退房。每一步操作后端返回更新的数据前端再刷新页面。统计页面需要引入图表库主流选择是 ECharts。入住率折线图、月度营收入柱状图、房间类型占比饼图数据主要靠后端一个聚合统计接口返回。这类接口一般会用到 MySQL 的 GROUP BY 和日期函数例如按月份统计营收SELECT DATE_FORMAT(check_out_date, %Y-%m) AS month, SUM(total_amount) AS revenue FROM reservation WHERE status 3 GROUP BY DATE_FORMAT(check_out_date, %Y-%m) ORDER BY month DESCDATE_FORMAT 是处理日期格式化的常用函数在这个案例里就是要把 DATETIME 类型字段转成精确到月份的字符串并按这个字符串分组汇总。5. 联调、部署与发布5.1 前后端联调的注意点开发阶段两边是分开跑的联调就是让前端页面和后端接口真正对接起来。这个阶段最容易出的问题集中在三个地方。第一是参数格式不匹配。后端用 LocalDateTime 接收日期前端传过来的是字符串 2025-06-01 14:30:00格式必须能被 JSON 反序列化识别。通常在后端用 JsonFormat(pattern yyyy-MM-dd HH:mm:ss) 注解来约束前端传参时也要保持统一格式。第二是 null 值的处理。后端返回的 JSON 里某个字段为 null 时前端拿到的可能是 null、也可能是空字符串这取决于后端如何配置 Jackson 序列化器。统一约定比改 bug 省事得多。我的习惯是后端把需要给前端的字段一次性返回完整不给 null前端拿到的数据格式永远是可控的。第三是接口文档。前后端分离的项目里接口文档就是双方约定的法律。推荐使用 Swagger/OpenAPI 或者直接维护一份 Markdown 的接口文档把每个接口的 URL、请求方式、入参、返回示例写得清清楚楚。前后端联调的过程你会发现大部分耗时不是在写代码而是在核对接口数据结构和参数定义。5.2 打包发布到服务器后端打包比较简单。IDEA 里的 Maven 面板执行 package 命令即可生成一个 JAR 文件。执行完把它放到服务器上用 java -jar 命令启动java -jar hotel-admin-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod建议在 application.yml 里区分开发环境和生产环境的配置。用 spring.profiles.active 参数切换开发环境的数据库连接和生产环境的连接分开配置数据库密码不要提交到 Git 仓库。前端打包执行 npm run build执行完成后会生成一个 dist 目录里面就是编译压缩后的纯静态文件HTML JS CSS。前端打包后有一个非常容易被忽略的问题路由模式如果用的是 HTML5 history 模式即 URL 形如 /room/list部署到 Nginx 后刷新页面会 404因为 Nginx 在磁盘上找不到这个路由对应的真实文件。解决办法是在 Nginx 配置里加一行location / { try_files $uri $uri/ /index.html; }它的含义是先尝试访问请求的真实路径如果找不到就统一返回 index.html再由前端的 Vue Router 接管路由。这样刷新任意子页面都不会出现 404。生产环境的 Nginx 反向代理配置可以这样写server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/hotel-admin/dist; index index.html; location /api/ { proxy_pass http://localhost:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }通过 Nginx 将前端的 /api 路径反向代理到后端的 9090 端口浏览器访问的始终是同域地址后端不再单独处理跨域问题这是生产环境最标准的部署手法。6. 常见问题与排查技巧实录6.1 SpringBoot 版本过高引发的兼容性问题SpringBoot 版本太高这个问题我在很多学生项目里见过。典型表现是代码照着教程写的一模一样启动时报各种莫名其妙的错误比如 javax.servlet 找不到、MyBatis 无法注入等等。大部分情况都是因为 SpringBoot 3.x 要求 JDK 17 并改用 jakarta 命名空间而旧代码里用的是 javax。解决办法要么降级 SpringBoot 到 2.7.x 且使用 JDK 8/11要么升级所有涉及 servlet 的依赖到 jakarta 版本。排查顺序可以这样一看 IDEA 的 JDK 版本二看 pom.xml 里的 spring-boot-starter-parent 版本三看 MyBatis 依赖版本。这三个地方有一个不匹配都会引发连锁反应。6.2 Vue 打包后布局异常开发环境下跑得好好的打包部署上去之后样式乱了、字体图标变了或者显示空白这个问题也很常见。第一个原因是路由模式问题。开发环境用的是 history 模式在本地 devServer 里没问题部署后 Nginx 没配 try_files刷新直接 404这时候页面变成空白或者报错。按上面说的配置 try_files 即可解决。第二个原因是 publicPath 配置错误。资源引用的路径默认是根路径 /如果你的站点部署在服务器根路径下没问题如果部署在子目录比如 http://ip:8080/hotel/就必须要配置 publicPath: /hotel/ 才能正确加载资源。Vue CLI 里在 vue.config.js 加module.exports { publicPath: /hotel/ }第三个原因是浏览器缓存了旧的 JS 文件强制刷新或清缓存之后再看。6.3 MySQL 安装配置的坑MySQL 安装本身不难但新手经常在连接环境上出问题。服务能启动Java 代码却报 Access denied for user rootlocalhost一般分三种情况密码错误重新设置认证密码用户权限问题针对远程连接需要给用户授权驱动版本问题MySQL 8.x 需要用 com.mysql.cj.jdbc.Driver老版本的驱动连不上MySQL 5.7 和 MySQL 8.x 在连接参数上还有一个常见差异8.x 默认使用 caching_sha2_password 认证插件老版本驱动不识别。解决方式是在 URL 里加 allowPublicKeyRetrievaltrue useSSLfalse或者建用户时指定 mysql_native_password 插件。6.4 一个小细节前端视频/文件播放问题最近有朋友问我在酒店管理系统里能不能播放实时监控视频或者酒店展示视频前端拿到的是 m3u8 的流媒体地址。这种场景在 Vue 里确实能落你需要引入 hls.js 库去解析 m3u8 流并直接喂给 video 标签原理就像浏览器默认不认可这种格式你需要一个翻译器。这个超出了酒店管理系统的核心范围属于增值需求但如果你在页面上要嵌入这类视频可以把播放器封装成一个 Vue 组件用 hls.js 装载地址并监听错误事件做降级提示。核心操作就是先判断浏览器原生是否支持 HLS不支持就 new Hls() 然后 attachMedia 到 video 节点上。总结酒店管理系统的开发核心不在于某一项技术有多深而在于你能否把这套组合拳打得顺畅。从数据库表设计到 MyBatis 的中介作用从 SpringBoot 的接口实现到 Vue 的表单交互整个流程走通一遍后你对前后端分离的理解完全不是看几篇教程能比的。我个人建议你拿到源码之后务必自己动手从零敲一遍核心模块尤其是 JWT 登录、权限拦截器、可用房间查询这些逻辑——这些正是面试官最爱追问、实际工作中每天都在用的东西。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询