前后端分离做景区民宿预约系统,这个选题在培训班项目和企业练手项目里都快成“标配”了。SpringBoot负责后端接口,Vue搞定前端页面,MyBatis操作MySQL数据,再加一套完整的部署流程,基本就是目前中小型Web系统最实用的技术组合。我最近刚好完整走了一遍这套系统的开发、打包和部署,踩了不少坑,也总结了一些经验,今天把这些内容整理出来,给正在做毕设、找工作或者纯粹想练手前后端分离项目的人做个参考。
这套系统能做的事情很清晰:游客在前端页面浏览民宿列表、查看房型和价格、选择入住日期下单预约;管理员在后端管理民宿房源、处理订单、统计入住情况。核心业务就是“房源展示—在线预约—订单管理”这条链路,再配上用户注册登录、权限控制这些基础能力。技术栈里SpringBoot负责提供RESTful API,Vue负责页面渲染和交互,MyBatis作为持久层框架操作数据库,MySQL存业务数据。整套代码结构完整,接口设计规范,拿来学习前后端分离的开发模式、理解业务系统从零到一的过程,都非常合适。
1. 项目整体设计与思路拆解
1.1 前后端分离架构的核心逻辑
前后端分离,本质上就是“前端管展示,后端管数据”。这个系统里,Vue前端运行在浏览器中,通过HTTP请求调用SpringBoot后端提供的接口,后端处理业务逻辑、读写MySQL数据库,然后把结果以JSON格式返回给前端。前后端之间只通过JSON数据交互,不直接共享代码和运行环境。
这样做的好处很明显。开发阶段,前端和后端可以并行推进——我在改Vue页面的时候,后端同事可以同时调接口逻辑,互不阻塞。部署阶段,前端打包成静态文件扔给Nginx托管,后端打成jar包独立运行,两边可以单独升级、各自扩容。而且接口复用性好,同一个后端API,将来如果要做小程序、App,前端部分重写就能复用。
这个系统的目录结构按照这种架构来组织:
hotel-booking/ ├── frontend/ # Vue 前端工程 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── views/ # 页面组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # 全局状态管理 │ │ └── utils/ # 工具函数 ├── backend/ # SpringBoot 后端工程 │ ├── src/main/java/ │ │ └── com/example/booking/ │ │ ├── controller/ # 控制层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── config/ # 配置类 │ │ └── common/ # 公共工具 │ └── src/main/resources/ │ ├── mapper/ # MyBatis XML映射文件 │ └── application.yml # 配置文件 └── sql/ # 数据库初始化脚本1.2 为什么选这套技术栈而不是其他
有人会问,现在MyBatis-Plus、JPA也挺流行,为什么还用MyBatis?选型基于这样几个考量:第一,MyBatis的SQL完全由开发者控制,对于预约系统里那些“查可用房间”“统计订单数量”之类的业务SQL,写起来更灵活,性能优化也更直接。第二,MyBatis上手门槛低,XML映射文件里写SQL,几乎不需要额外学习成本,对刚接触企业级开发的人非常友好。第三,面试和工作中MyBatis依然是高频技术点,通过这个项目把它的工作机制彻底搞清楚,收益很长线。
前端选Vue而不是React,主要是考虑Vue的学习曲线更平缓,模板语法直观,配合Vue Router和Vuex/Pinia能快速搭起一个完整的中后台系统。民宿预约系统的页面复杂度不算高,Vue的响应式数据和组件化开发完全够用,开发效率很高。
1.3 系统功能模块怎么划分
功能模块的划分直接决定了表结构和接口设计。我把这个系统拆成了五个核心模块:
用户模块:注册、登录、个人信息管理。密码采用MD5加盐存储,登录成功后后端签发JWT Token,前端每次请求带上Token进行身份验证。
民宿管理模块:管理员维护民宿信息,包括民宿名称、地址、描述、图片、配套设施等。前端按列表和详情两种形态展示。
房型与库存模块:每间民宿下辖多个房型,每个房型有价格、面积、床型、可住人数等属性。库存逻辑必须结合日期来处理——某个房型某一天还剩几间,不是简单的总量减订单数,而是要按日期维度判断。
预约下单模块:用户选择民宿、房型、入住日期、离店日期,系统校验该时间段内是否有可用房间,计算总价并生成订单。
订单管理模块:用户查看自己的订单、取消未入住的订单;管理员查看所有订单、确认入住、完成结算。
1.4 用例设计参考
| 角色 | 用例列表 |
|---|---|
| 游客 | 注册账号、浏览民宿列表、查看民宿详情、查看房型价格 |
| 注册用户 | 登录、预约下单、查看订单、取消订单、修改个人信息 |
| 管理员 | 管理民宿信息、管理房型库存、查看所有订单、处理订单状态 |
接口设计围绕这几个用例展开,RESTful风格,路径清晰。比如GET /api/hotels获取民宿列表,POST /api/bookings创建预约订单,PUT /api/admin/bookings/{id}/status更新订单状态。
2. 数据库设计与核心表结构拆解
2.1 数据库设计的关键决策
民宿预约系统的业务核心是“时间+房间”的匹配关系,所以数据库设计要比普通CRUD系统多花一些心思。我设计的时候坚持了几个原则:一是表结构尽量贴合业务流程,每个模块的表职责单一;二是所有金额字段用Decimal类型,避免浮点误差;三是时间字段统一用datetime,方便日期区间查询;四是频繁查询的字段加索引,比如订单表的用户ID、民宿ID,库存表里关联了房型ID和日期。
MySQL默认的InnoDB引擎是必须用的,因为业务涉及订单和库存,需要事务支持。字符集用utf8mb4,因为要存民宿描述里可能出现的特殊符号和Emoji。排序规则用utf8mb4_general_ci,兼容性最好。
2.2 核心表结构详解
这套系统我设计了6张核心表:用户表、民宿表、房型表、房间库存表、预约订单表、订单明细表。直接看SQL脚本的节选更直观:
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(64) NOT NULL COMMENT 'MD5加盐加密', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0-普通用户 1-管理员', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `hotel` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '民宿名称', `address` VARCHAR(255) DEFAULT NULL, `description` TEXT, `cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图URL', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-上架 0-下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='民宿表'; CREATE TABLE `room_type` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `hotel_id` BIGINT NOT NULL COMMENT '所属民宿ID', `name` VARCHAR(50) NOT NULL COMMENT '房型名称', `price` DECIMAL(10,2) NOT NULL COMMENT '每晚价格', `area` DECIMAL(8,2) DEFAULT NULL COMMENT '面积(平方米)', `bed_type` VARCHAR(20) DEFAULT NULL COMMENT '床型', `max_people` TINYINT DEFAULT NULL COMMENT '可住人数', `total_rooms` INT NOT NULL DEFAULT 1 COMMENT '该房型总房间数', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_hotel_id` (`hotel_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房型表'; CREATE TABLE `room_stock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `room_type_id` BIGINT NOT NULL, `stock_date` DATE NOT NULL COMMENT '库存日期', `remaining` INT NOT NULL COMMENT '剩余可订房间数', PRIMARY KEY (`id`), UNIQUE KEY `uk_room_date` (`room_type_id`, `stock_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房型库存表'; CREATE TABLE `booking` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `user_id` BIGINT NOT NULL COMMENT '下单用户ID', `room_type_id` BIGINT NOT NULL, `check_in_date` DATE NOT NULL COMMENT '入住日期', `check_out_date` DATE NOT NULL COMMENT '离店日期', `nights` INT NOT NULL COMMENT '入住晚数', `total_price` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待确认 1-已确认 2-已入住 3-已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_room_type_id` (`room_type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单表';2.3 库存表的设计就是这套系统的灵魂
民宿房间数量少,不像酒店那样有几百间房,但库存逻辑反而要更精细。比如一间民宿只有3间大床房,8月1日被订了2间,8月2日只被订了1间,那8月1日可订数就是1间,8月2日可订数就是2间。如果按传统商品库存的“总库存减已售”,这个逻辑根本没法表达。
所以我把库存设计成了“房型+日期”的二维表,每一天都记录该房型剩余可订数。下单时检查入住日期到离店日期之间每一天的库存是否都充足,全部满足才允许创建订单,并且对每一天的库存做扣减。这是一条完整的库存事务链路。
room_stock表用(room_type_id, stock_date)组成唯一索引,防止同一天重复插入库存记录。初始化的时候,每个房型需要生成未来一段时间的库存数据,这块在后端启动时或管理员维护房型时自动完成。我当时是用一个定时任务在每天早上6点滚动生成未来30天的库存,这样就保证了用户永远能预订未来一个月内的房间。
一个人想看晚上8点后的细节,差不多这几百字够了。接下来看有没有要补充的内容。
2.4 表关系与事务边界
表关系整体上是:用户1对N订单,民宿1对N房型,房型1对N库存,房型1对N订单。订单表同时关联用户和房型,通过user_id和room_type_id两个外键建立联系。
事务边界主要卡在“创建订单+扣减库存”这一步。我用了@Transactional注解包裹创建订单的方法,同时扣减所有入住日期对应的库存记录,任何一步失败都整体回滚。这里不得不提MyBatis的一个细节:更新库存的时候要用条件更新,在UPDATE ... WHERE remaining > 0这种语句上检查返回值,如果更新的行数为0,说明库存已经被抢完了,立即抛出异常触发回滚。这样才能避免超卖。
3. 后端SpringBoot与MyBatis的实现细节
3.1 后端项目结构怎么组织
SpringBoot项目的包结构按照“controller-service-mapper-entity”四层来划分,这是最经典也最容易理解的模式。Controller层只做参数接收和结果返回,Service层写业务逻辑,Mapper层用MyBatis操作数据库,Entity层定义实体类。
这种分层的核心好处是职责清晰:改接口不改业务逻辑,改业务逻辑不碰SQL,改SQL不影响上层调用方。我在项目里还增加了一个common包,统一存放返回结果封装Result<T>、异常处理器、JWT工具类等公共组件。
先看application.yml的关键配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hotel_booking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.booking.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: your-secret-key expire-hours: 72map-underscore-to-camel-case这里必须设成true,否则数据库里的create_time映射不到Java实体类的createTime字段上。log-impl配置成StdOutImpl可以在控制台打印SQL语句,开发阶段排查问题非常有用。上线前我会把这个改成org.apache.ibatis.logging.slf4j.Slf4jImpl,配合日志框架输出到文件。
3.2 MyBatis XML映射文件怎么写才规范
MyBatis有两种用法,注解SQL和XML映射。这个项目我全面采用XML方式,原因是业务SQL普遍较复杂,有动态条件、循环、多表关联,写XML里更容易维护和调试。
以一个查询为例。民宿列表页需要按名称模糊搜索、按价格区间筛选、按入住日期筛有房的民宿,这个查询要关联民宿表、房型表、库存表三张表:
<select id="selectAvailableHotels" resultType="com.example.booking.entity.vo.HotelVO"> SELECT h.id, h.name, h.address, h.description, h.cover_image, MIN(rt.price) AS min_price, COUNT(DISTINCT rt.id) AS room_type_count FROM hotel h JOIN room_type rt ON h.id = rt.hotel_id WHERE h.status = 1 <if test="keyword != null and keyword != ''"> AND (h.name LIKE CONCAT('%', #{keyword}, '%') OR h.address LIKE CONCAT('%', #{keyword}, '%')) </if> AND EXISTS ( SELECT 1 FROM ( SELECT rs.stock_date, SUM(rs.remaining) AS total_remaining FROM room_stock rs JOIN room_type rt2 ON rs.room_type_id = rt2.id WHERE rt2.hotel_id = h.id AND rs.stock_date BETWEEN #{checkInDate} AND #{checkOutDate} GROUP BY rs.stock_date HAVING total_remaining > 0 ) temp ) GROUP BY h.id, h.name, h.address, h.description, h.cover_image ORDER BY min_price ASC </select>这个SQL的动态部分用<if>标签处理,#{keyword}是预编译参数可以防止SQL注入。EXISTS子查询判断该民宿在目标日期区间内每天是否都有房,这是整个检索逻辑里最核心的一段。
3.3 JWT登录认证的完整实现
接口安全不能只靠前端控制按钮显示,后端必须对每个请求做身份验证。我在项目里用JWT实现了无状态认证。
流程是这样的:用户登录成功后,后端生成包含用户ID和角色信息的Token返回给前端。前端把Token存到localStorage,每次请求在HTTP Header里带Authorization: Bearer <token>。后端通过拦截器拦截所有非放行路径,解析并校验Token,然后把用户信息放进ThreadLocal里供后续业务逻辑使用。
放行的路径包括注册、登录、民宿列表查询这些公开接口。管理相关的接口,比如发布民宿、处理订单,还需要校验角色是否为管理员。
核心代码就两块。JWT工具类负责生成和解析:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire-hours}") private Integer expireHours; public String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireHours * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }拦截器负责校验:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = jwtUtil.parseToken(token.substring(7)); UserContext.set(claims); return true; } catch (Exception e) { response.setStatus(401); return false; } } }3.4 创建订单与扣减库存的事务控制
创建订单是整个系统最复杂的业务操作,事务控制是硬指标。伪代码如下:
@Transactional(rollbackFor = Exception.class) public Booking createOrder(BookingCreateDTO dto) { // 1. 校验日期合法性(入住日期不能早于今天,离店日期必须晚于入住日期) // 2. 查询房型信息 RoomType roomType = roomTypeMapper.selectById(dto.getRoomTypeId()); if (roomType == null) { throw new BizException("房型不存在"); } // 3. 计算入住晚数 long nights = ChronoUnit.DAYS.between(dto.getCheckInDate(), dto.getCheckOutDate()); // 4. 查询库存并做乐观锁扣减 for (Date d = checkInDate; d.isBefore(checkOutDate); d.plusDays(1)) { int rows = roomStockMapper.decreaseStock(roomTypeId, d); if (rows == 0) { throw new BizException(d + " 房间已订满"); } } // 5. 生成订单号(时间戳 + 随机数) // 6. 插入订单记录 return order; }decreaseStock对应的XML是:
<update id="decreaseStock"> UPDATE room_stock SET remaining = remaining - 1 WHERE room_type_id = #{roomTypeId} AND stock_date = #{stockDate} AND remaining > 0 </update>这里AND remaining > 0就是乐观锁的关键。如果剩余数为0,更新操作影响的行数就是0,代码拿到返回的rows == 0就知道这天的房间已经订完了,立刻抛异常让事务回滚。整个过程在数据库层面保证了不会超卖。
3.5 MyBatis二级缓存的配置建议
项目里我打开了MyBatis的二级缓存。二级缓存是Mapper级别的全局缓存,多个SqlSession之间共享查询结果,能大大减少数据库查询次数。配置不复杂,在XML映射文件中加一行:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>但要特别注意:二级缓存对“按主键查询”这类操作效果最好,对于列表查询,一旦数据被修改必须及时刷新缓存。正因为有这个坑,我在订单和库存相关的Mapper里没有开二级缓存,只在民宿和房型这类变化频率低的查询上开了。这个经验值得记住——缓存不是越多越好,一致性风险也要一并考虑。
4. 前端Vue页面设计与交互实现
4.1 Vue项目初始化与环境配置
前端我用的Vue 3 + Vite + Vue Router + Pinia这套组合。Vue 3的组合式API让逻辑复用更灵活,Vite的开发服务器启动速度快,热更新响应及时,对日常开发体验提升很明显。
创建项目直接用官方脚手架:
npm create vite@latest frontend -- --template vue cd frontend npm install npm install vue-router@4 pinia axios npm run devnpm install这一步务必在稳定的网络环境下执行,遇到node-sass这类需要编译的原生依赖时更要耐心等待安装完。
项目里封装了axios请求工具,统一处理BaseURL、Token注入、错误响应的拦截:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { return Promise.reject(error) } )4.2 路由设计与权限守卫
路由分为公开页面和需要登录才能访问的页面。民宿列表、民宿详情是公开的,创建订单、个人中心需要登录,后台管理页面需要管理员权限。Vue Router的全局前置守卫来处理这层逻辑:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) return } if (to.meta.requiresAdmin) { const role = localStorage.getItem('role') if (role !== '1') { next({ path: '/403' }) return } } next() })redirect参数的处理很关键,用户没登录就点“立即预约”,跳转到登录页后一旦登录成功,可以直接回到原来的预约页面,不至于让用户重新找一遍。
4.3 民宿预订的交互流程实现
民宿预订的核心页面有三块:民宿列表页、民宿详情页、下单确认页。
列表页用卡片网格展示民宿,封面图、名称、地址、最低价格一目了然。搜索栏支持按名称关键词和入住日期筛选,日期选择器用的是el-date-picker的daterange模式,选好日期范围后直接调用GET /api/hotels?keyword=xxx&checkInDate=xxx&checkOutDate=xxx接口。
详情页是转化率最高的页面,展示民宿相册、介绍、设施列表,下方列出所有房型,每个房型显示价格和“立即预约”按钮。点击预约跳到下单确认页,带上民宿ID、房型ID、入住和离店日期参数。
下单确认页的日期选择逻辑要注意一个体验细节:入住日期不能早于今天,离店日期必须晚于入住日期,用disabledDate函数控制可选范围,避免用户选出不合法的时间。
const disabledDate = (date) => { const today = new Date() today.setHours(0, 0, 0, 0) return date < today // 禁止选择过去的日期 }选择完日期后,前端要展示入住晚数和总价,这个计算很简单,nights = (checkOutDate - checkInDate) / 86400000,totalPrice = nights * roomType.price,精确到两位小数。
4.4 前端调接口时常见的问题
前后端联调阶段最容易出的问题就是跨域请求被拦截。浏览器同源策略要求协议、域名、端口完全一致,Vite开发服务器的默认端口是5173,后端SpringBoot是8080,两边必然跨域。
本地开发时的解决方案:在vite.config.js里配置代理,把/api开头的请求转发到后端地址:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端请求的URL写/api/hotels,实际上Vite把请求代理到http://localhost:8080/api/hotels。因为代理是在服务端完成的,浏览器以为请求的是同源地址,就不会触发跨域限制。
生产环境部署时跨域问题一般交给Nginx处理,在Nginx配置里用location /api和后端起同一个服务,天然不存在跨域。我在部署章节会细说。
5. 数据库初始化与业务数据准备
5.1 建库建表和基础数据导入
拿到项目之后,第一步不是急着跑后端代码,而是先把数据库初始化好。我习惯用MySQL命令行或者Navicat,先建库,再执行SQL脚本。
mysql -u root -p CREATE DATABASE hotel_booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; use hotel_booking; source /path/to/init.sql;执行完脚本可以验证一下表是否都建好了:
SHOW TABLES;基础数据分两类。一类是管理员账号,系统启动时需要有一个默认的管理员可以用,密码是MD5加盐处理过的。另一类是演示数据,比如几个典型的民宿信息、不同价位的房型、未来30天的库存记录。如果没有演示数据,前端页面上就是空荡荡的,没法正常体验业务流程。
5.2 自动生成库存数据的方案
库存数据不能靠手写SQL一条条insert,那样既不用不了几十条就要疯。我在后端实现了自动生成库存的逻辑:每次新增或修改房型时,调一个generateStock(roomTypeId, days)方法,为这个房型生成未来30天每天的库存记录,初始值等于该房型的total_rooms。
这个设计有个好处:每天滚动生成,用户永远能订到未来一个月的房。定时任务可以每天早上跑一次,把缺少的日期补上,这样即使某天忘了手动处理,系统也能自愈。
6. 部署上线的完整流程与踩坑记录
6.1 后端打包与jar包发布
后端打包前先确认两件事:数据库连接信息是否正确、版本号是否更新。SpringBoot项目打包用的是Maven插件:
cd backend mvn clean package -DskipTests打包结束后在target目录下会生成一个backend-0.0.1.jar文件。用java -jar命令直接启动:
java -jar backend-0.0.1.jar --spring.profiles.active=prod生产环境的配置和本地环境是分开的,我准备了application-dev.yml和application-prod.yml两套配置,用spring.profiles.active切换。生产环境的数据库密码通过环境变量注入,不直接写在配置文件里,避免泄露。
6.2 前端构建与Nginx部署
前端构建生成静态文件:
cd frontend npm run build构建完成后dist目录下就是打包后的产物,包括HTML、JS、CSS和图片资源。把这整个目录上传到服务器,配置Nginx把站点根目录指向它。
Nginx配置文件可以参考这一段:
server { listen 80; server_name your-domain.com; root /opt/hotel-booking/frontend; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }location /api/把接口请求反向代理到本地的SpringBoot服务,前端和后端通过Nginx这层统一起了,就不存在跨域问题。try_files那行配置是为了让Vue Router的history模式在刷新页面时不出现404,任何不存在的路径都回退到index.html,由前端路由接管。
6.3 部署过程中最典型的几个坑
端口占用:SpringBoot默认8080,但服务器上可能已经有其他服务占用。如果发现启动失败,用netstat -tlnp | grep 8080查看占用情况,换一个端口或者杀掉占用进程。
MySQL远程连接失败:新装的MySQL默认只允许localhost访问,需要在MySQL用户表里授权远程主机访问:GRANT ALL PRIVILEGES ON hotel_booking.* TO 'root'@'%' IDENTIFIED BY 'password';,然后FLUSH PRIVILEGES;。如果还连不上,检查服务器防火墙和安全组的3306端口是否放行。
版本兼容问题:MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,连接URL还必须带serverTimezone=Asia/Shanghai参数,否则会报时区错误。SpringBoot 2.7和MySQL 8.0的配合没问题,但如果SpringBoot版本太低,就要注意调整对应的驱动依赖版本。
6.4 域名和HTTPS的配置建议
部署完成后,如果要做正式上线,建议配上域名和HTTPS证书。申请证书目前渠道很多,拿到证书后改Nginx配置:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/cert/your_domain.pem; ssl_certificate_key /etc/nginx/cert/your_domain.key; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { root /opt/hotel-booking/frontend; index index.html; try_files $uri $uri/ /index.html; } }HTTPS不仅仅是为了地址栏的那把小锁,它对用户信任度的提升是实实在在的。尤其民宿预约要填手机号和登录信息,没有证书用户心里会打鼓。配置好之后,Nginx上再加一条80端口到443的跳转,所有HTTP流量自动转成HTTPS。
7. 常见问题与排查技巧实录
7.1 后端启动失败的排查清单
后端启动失败,原因就那么几类,按下面的顺序排查,基本能定位绝大多数问题:
配置类问题:缺少必要的配置项,或者配置值不合法。看启动日志,如果提示“Failed to configure a DataSource”,优先查数据库地址、用户名、密码三个配置有没有写对。
依赖类问题:Maven依赖没有完整下载。执行mvn clean install重新拉取依赖,看是否能通过编译。
端口冲突:日志提示“Port 8080 was already in use”。执行netstat -tlnp | grep 8080找到占用进程,PID然后kill -9,或者改端口号。
7.2 线上接口报错怎么快速定位
一个典型场景:前端页面调接口,控制台报500,SQL执行成功但数据没查到。这时候打开后端的日志,重点看异常堆栈,通常里面会直接输出具体的SQL语句和参数值。
MyBatis在开发环境我配置了StdOutImpl,SQL语句和参数会直接打印。定位到具体SQL后,可以直接把SQL语句在Navicat或MySQL命令行执行一遍,看是否返回了预期结果。这种“对比法”能快速判断问题是出在SQL本身、参数传值,还是数据状态不对。
7.3 前端常见告警与解决思路
前端控制台如果出现[Vue Router warn]: No match found for location with path "xxx",说明路由配置里没有匹配这个路径。检查router/index.js里是否配置了对应的路由项,以及路径大小写是否正确。
如果页面能正常渲染但数据为空,先在浏览器的Network面板里看接口请求的响应体。如果返回的JSON里code不是200,再看message字段的内容。常见的原因包括Token过期、参数缺失、该登录状态下的权限不足。
7.4 系统性能可以优化的几个方向
这套系统当前的设计已经能支撑中小流量场景,但如果民宿数量多了、并发上去了,有几个方向值得优化:
接口缓存:民宿详情这类读多写少的接口,可以用Redis缓存热点数据。用户高频率查询的民宿列表,也可以做Redis缓存,根据民宿信息变更主动失效或定时刷新。
数据库索引调优:订单表通过EXPLAIN分析慢查询,确认user_id和room_type_id的索引是否被正确命中。库存表的高频查询集中在room_type_id + stock_date的组合条件上,要给这两个字段建联合索引。
接口限流与防刷:登录接口、下单接口可以做基于IP的访问频率限制,防止脚本恶意调用。简单方案是在Nginx层配置limit_req_zone,或者在后端用拦截器配合Redis做计数器。
图片与静态资源分离:民宿图片如果量大,建议用独立的OSS存储或图床服务,数据库里只存URL。前端再配合CDN加速静态资源加载,访问速度会有很明显提升。
7.5 常见问题速查表
| 异常现象 | 可能原因 | 排查方向 |
|---|---|---|
| 后端启动报数据源错误 | 数据库连接配置不正确 | 检查MySQL连接地址/账号/密码 |
| 前端接口返回401 | Token过期或未携带 | 检查localStorage的token;检查拦截器header注入 |
| 创建订单失败提示库存不足 | 库存确实不足/扣减逻辑异常 | 查看该日期的room_stock记录 |
| 页面刷新404 | 路由模式与Nginx配置不匹配 | 确认try_files配置了fallback到index.html |
| 中文乱码 | 字符集不统一 | 数据库/连接串/页面编码统一utf8mb4 |
| 跨域请求被拦截 | 开发环境proxy未配置/生产未用Nginx同源 | 配置vite proxy或Nginx反代 |
| MySQL连接超时 | 连接池配置/网络不稳定 | 调整druid连接池配置,检查防火墙 |
8. 源码阅读指南与二次开发路线
8.1 从哪个文件开始读代码
拿到源码不要从头到尾翻,要有路线。我推荐按“入口→配置→表结构→核心业务→辅助功能”的顺序阅读:
先看application.yml,了解启动时加载的配置,包括端口、数据库、MyBatis、JWT等;再打开sql/init.sql,对照表结构理解数据模型;接着看HotelController和BookingController的接口方法,梳理出系统的完整功能地图;最后深入BookingServiceImpl和RoomStockMapper,把库存扣减和订单创建这条核心链路读懂。
8.2 基于这套系统能扩展的功能
这套框架搭好之后,扩展新功能非常顺手。
增加评论系统:用户入住后可以对民宿进行评价和评分。新增comment表关联用户和民宿,前端在民宿详情页加一个评论区,后端提供评论的增删查接口。
价格日历功能:不同日期不同价格,是民宿行业的常见需求。把房型表的固定价格升级成“房型价格日历表”,按日期关联价格,然后下单时按每晚的价格分别计算总价。
优惠券模块:增加优惠券表和用户领取记录表,订单结算时校验优惠券有效性并抵扣金额,需要额外考虑优惠券的过期时间和使用条件等边界情况。
消息通知:订单状态变更时给用户发短信或站内信通知,可以用Spring的事件机制解耦业务逻辑和通知发送,异步处理不阻塞主流程。
后端如果不想手写所有代码,可以参考若依这类成熟的前后端分离脚手架。它们的代码生成器能根据数据库表自动生成Controller、Service、Mapper等代码,在此基础上改造业务逻辑,开发效率提升非常明显。
8.3 关于学习建议
从我的实际经验来说,做前后端分离项目最容易出错的地方不在写代码,而在环境搭建和数据模型设计。刚上手的朋友我建议先把环境理顺——Node.js版本要选对(Vue 3建议用16以上),MySQL字符集要统一,Maven仓库镜像配好,这样在开发时能省掉大量莫名其妙的报错排查时间。
数据模型设计值得多花时间琢磨。把库存、订单、房型这三张表的关系想透彻,后面写业务逻辑就会非常顺畅。如果一上来就急着写代码,后面要返工改表结构,那才是真正让人头疼的事情。
9. 总结
前后端分离开发这套景区民宿预约系统,走完整个流程你会发现,SpringBoot、Vue、MyBatis、MySQL这四件套结合起来的威力远比想象中要大。后端通过合理的分层和事务控制保证了数据的安全和一致性,前端通过组件化和路由管理带来了友好的交互体验,Nginx再漂亮地解决生产环境的部署和代理问题。这套系统麻雀虽小,但五脏俱全,从环境搭建到数据库设计,从接口开发到前端联调,从打包部署到线上排错,每一个环节都是实打实的企业级开发流程。希望这篇文章能帮你把整个项目的脉络捋清楚,少走一些弯路。如果你照着部署过后遇到了问题,随时欢迎来交流,我尽量根据实际经验给出具体的建议。