
做民宿客房预订系统是我第一次真正把 SpringBoot 和 Vue 完整地拼在一起做前后端分离项目。之前写课程设计也好做外包小项目也好都是后端套个 Thymeleaf 页面就算完事这次因为客户明确要求移动端和 PC 端都能流畅使用才下决心用 Vue 重写前端。做完之后回头看这系统虽然叫“民宿客房预订管理系统”看起来简单实际里面藏着的坑真不少——从库存扣减到订单状态机从日历价格到支付回调每一个环节都值得拿出来单独说一说。这篇就把整个项目的设计思路、核心功能实现、还有我踩过的一些坑都摊开聊一聊给正在做类似 SpringBootVue 全栈项目的同学一个参考。这个项目适合谁看正在准备 SpringBoot 毕业设计的同学、想练手前后端分离的初学者、或者接了民宿类小项目不知道怎么落地的外包开发者都能从中找到对应的模块和代码思路。整个系统核心是四个字房态管理。围绕民宿预订的完整链路我拆成了房源管理、日历房价、订单中心、客户管理、评价反馈五个主要模块再配合后台管理端和用户端两个入口构成一套可以真正上线跑的业务闭环。1. 项目整体设计与技术选型思路1.1 为什么是 SpringBoot Vue 这对组合选 SpringBoot 做后端几乎没有悬念——Java 生态里它就是目前最适合快速落地业务系统的框架。自动装配让配置量大幅减少内嵌的 Tomcat 让部署也变得简单再加上 Maven 管理依赖一套标准的三层架构跑起来非常省心。我在项目里用到的核心依赖就那么几个Spring Web、MyBatis-Plus、MySQL 驱动、Lombok、JWT 鉴权、以及后面加上的 MinIO 客户端。相比 SSM 时代要手动写一堆 XML 配置SpringBoot 确实把开发者的精力从“配环境”解放到了“写业务”上。前端选 Vue 而不是 JSP 或者 Thymeleaf核心原因是交互体验和前后端解耦。民宿预订有个很强的交互场景——用户选入住日期、看房态日历、切换房型这些操作如果靠后端模板渲染每一次点击都要刷新页面体验太差了。Vue 的响应式数据绑定和组件化开发天生适合这种高频交互页面。再加上现在 Vue 的生态已经很成熟Element Plus 做后台管理界面、Vant 做移动端 H5一套代码两种终端都能覆盖到。技术选型对比下来我的结论是这样的方案优点缺点适用场景SpringBoot Vue 前后端分离开发效率高前后端并行体验好跨域处理、鉴权需要额外设计中大型项目、需要多端适配SpringBoot Thymeleaf部署简单不用考虑跨域交互体验一般前后端耦合后台管理系统、原型验证SSM JSP老牌方案资料多配置繁琐开发效率低维护老项目实话说如果只是做一个给管理员用的内部 CRUD 系统Thymeleaf 完全够用。但民宿预订系统面向的是 C 端用户预订流程、日历选择、订单支付这些都是强交互场景Vue 几乎是必选项。1.2 民宿预订系统的核心业务域拆解这个系统虽然经过我的手但需求其实是朋友的民宿提出来的。他们当时遇到的最头疼的问题是——房价和房态全靠 Excel 记录经常出现一房两卖。所以我设计系统的时候第一个原则就是一切以“房态日历”为中心。整个系统可以拆成三个核心业务域房源域包含民宿信息、房型大床房/双床房/loft、具体房间同一房型下的多个房间编号。这里要注意区分“房型”和“房间”——房型是商品的抽象房间是可预订的物理实体。比如一个民宿有“山景大床房”这个房型下面有 101、102 两个房间用户在系统里看到的日历房价是按房型展示的但底层扣库存时必须精确到“某个房间某一天”。订单域包含预订单、入住人信息、价格明细、订单状态。民宿订单跟酒店订单最大的区别在于——民宿的订单经常有连住多天、整栋包场、节假日调价这些复杂场景所以订单跟“日历”的关联性特别强。用户域包含 C 端用户在小程序/H5 上下单的人和 B 端用户民宿老板、前台管理员。两个角色权限完全不同我是用 JWT 拦截器做的权限控制后面前端再通过路由守卫配合做页面级控制。这三个业务域对应到数据库表就是后面要详细讲的核心设计。1.3 目录结构与项目骨架规划项目一开始我就把前后端彻底分开了目录结构是这样的homestay-system/ ├── backend/ # SpringBoot 后端 │ ├── src/main/java/com/homestay │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # MyBatis-Plus Mapper │ │ ├── entity/ # 数据库实体 │ │ ├── dto/ # 请求/响应对象 │ │ ├── config/ # 配置类跨域、JWT、MinIO │ │ ├── common/ # 统一返回结果、异常处理 │ │ └── utils/ # 工具类 │ └── src/main/resources/ │ ├── mapper/ # XML 映射文件 │ └── application.yml └── frontend/ # Vue 前端 ├── src/ │ ├── api/ # axios 请求封装 │ ├── router/ # 路由配置 │ ├── store/ # Pinia 状态管理 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ └── utils/ # 前端工具 └── package.json后端的东西看起来小但分包这块我建议一开始就规划好不然后面代码一多想重构就很痛苦。我用的是标准的 Controller-Service-Mapper 三层结构再额外加了 config 和 common 两个包来放公共逻辑这样每个模块之间边界清晰后面加新功能的时候不用动老代码。2. 数据库设计与核心表结构数据模型是整个系统的地基这块要是设计不好后面写代码会各种别扭。我在设计表的时候重点考虑了三件事房态怎么存、订单怎么关联房态、价格怎么跟随日期变化。2.1 房型、房源与日历库存的设计先看房源相关的表一共四张民宿表homestay、房型表room_type、房间表room、日历房价表room_calendar。民宿表比较简单就是名称、地址、简介、封面图等基础信息。房型表关联民宿字段包括房型名称、面积、可住人数、床型、设施标签。房间表是房型的具体实例比如“山景大床房”这个房型下有房间 101、102。关键的其实是room_calendar这张表CREATE TABLE room_calendar ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_type_id BIGINT NOT NULL COMMENT 房型ID, calendar_date DATE NOT NULL COMMENT 日期, price DECIMAL(10,2) NOT NULL COMMENT 当日价格, stock INT NOT NULL DEFAULT 0 COMMENT 可售库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1可售 0停售, UNIQUE KEY uk_room_type_date (room_type_id, calendar_date) ) COMMENT 日历房价表;用这张表来管理每一天、每个房型的库存和价格。那有读者可能会问为什么库存要挂在房型上而不是房间上因为用户订房的时候是选“房型”而不是选“具体哪个房间”民宿老板可以在后台调配具体房间。库存的含义就是“这个房型当天还剩几间可以卖”。这样可以避免很多不必要的复杂性。库存扣减的 SQL 是后面性能优化的关键UPDATE room_calendar SET stock stock - 1 WHERE room_type_id #{roomTypeId} AND calendar_date BETWEEN #{startDate} AND #{endDate} AND stock 0这个 SQL 是保证不超卖的核心后面在订单模块会详细说。2.2 订单表与状态机设计订单表是整个系统业务逻辑最密集的地方。我的订单表拆成了两层主表orders存订单整体信息子表order_items存每个房型、每晚的价格明细。CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 用户ID, homestay_id BIGINT NOT NULL COMMENT 民宿ID, room_type_id BIGINT NOT NULL COMMENT 房型ID, check_in_date DATE NOT NULL COMMENT 入住日期, check_out_date DATE NOT NULL COMMENT 离店日期, nights INT NOT NULL COMMENT 入住晚数, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态, customer_name VARCHAR(50) COMMENT 入住人姓名, customer_phone VARCHAR(20) COMMENT 入住人电话, remark VARCHAR(500) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL, cancel_time DATETIME NULL, cancel_reason VARCHAR(200) NULL ) COMMENT 订单主表;订单状态我定义了一套清晰的状态机0 待支付 - 1 已支付/待入住 - 2 已入住 - 3 已离店/待评价 - 4 已完成 0 待支付 - 5 已取消用户主动取消或超时取消这个状态机是整个订单模块的核心每次状态流转都要满足前置条件比如“已入住”必须有“已支付”的前置状态“已取消”只允许从“待支付”状态跳转。订单编号不能直接用自增 ID我用的是“年月日时分秒 随机数”的格式比如2025011515304512345678。这样一方面订单号有可读性另一方面也方便后续对接支付系统时的幂等性处理。2.3 价格策略与优惠的表结构思路民宿的价格不是一成不变的节假日、周末、淡旺季价格都不一样。我之前在表设计里把价格直接挂在了日历上每个房型每天都有独立的售价老板在后台改某一天的价格即可这种模式叫“日历房价模式”。后面客户还提了一个需求连住优惠。住 3 晚以上打 9 折住 5 晚以上打 85 折。这个我额外加了一张discount_rule表CREATE TABLE discount_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, homestay_id BIGINT NOT NULL, min_nights INT NOT NULL COMMENT 最少入住晚数, discount_rate DECIMAL(3,2) NOT NULL COMMENT 折扣率0.90表示9折, priority INT DEFAULT 0 COMMENT 优先级, is_active TINYINT DEFAULT 1 ) COMMENT 连住优惠规则表;计算总价的时候先根据room_calendar查出每晚的价格加总后再用最优折扣规则打折。这块逻辑在后端封装成PriceCalculator工具类保证前端展示的预估价和后端算出来的实际价完全一致。3. 后端核心功能实现3.1 接口设计与统一返回结构前后端分离的项目接口协议一定要定义清楚。我封装了一个统一的返回结构Data public class ApiResultT { private int code; private String message; private T data; public static T ApiResultT success(T data) { ApiResultT result new ApiResult(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ApiResultT error(int code, String message) { ApiResultT result new ApiResult(); result.setCode(code); result.setMessage(message); return result; } }所有接口统一返回这个结构不管成功失败前端拿到后先判断code再处理data或者错误提示。这样接口输出非常规整前端 axios 拦截器里统一做处理省了很多重复劳动。接口设计遵循 RESTful 风格比如GET /api/homestay/{id} 获取民宿详情 GET /api/room-type/{homestayId} 获取房型列表 GET /api/calendar/{roomTypeId} 获取日历房价 POST /api/order 创建订单 GET /api/order/{orderNo} 查询订单详情 POST /api/order/{orderNo}/cancel 取消订单分页查询用 MyBatis-Plus 内置的分页插件所有列表接口都带page和size参数避免一次性查出全量数据导致前端卡顿。3.2 抢占式库存扣减与防超卖这是整个项目我最想重点说的部分。民宿系统里一个用户选了 3 天 2 晚的房间点击下单后端要做的操作是把这 3 天里每一天的库存都减 1这期间必须保证不会有第二个用户也把这 3 天的库存减掉。我用的方案是创建订单时直接执行条件更新。Transactional public Order createOrder(OrderCreateDTO dto) { // 1. 生成订单编号 String orderNo generateOrderNo(); // 2. 构造订单实体 Order order new Order(); order.setOrderNo(orderNo); // ... 设置订单字段 ListDate nights getNightsBetween(dto.getCheckInDate(), dto.getCheckOutDate()); // 3. 逐天扣减库存任何一天失败则事务回滚 for (Date date : nights) { int affected roomCalendarMapper.decreaseStock(dto.getRoomTypeId(), date); if (affected 0) { throw new BizException(所选日期库存不足请调整日期); } } // 4. 计算价格、保存订单 BigDecimal totalAmount priceCalculator.calculate(dto); order.setTotalAmount(totalAmount); orderMapper.insert(order); return order; }为什么这个方案能防超卖关键在于decreaseStock里带了stock 0这个条件SQL 本身在数据库层面做了并发控制。两个请求同时执行这条 SQL 时数据库的锁机制保证只有一个是真正扣减成功的另一个 affected0就会抛异常触发回滚。Update(UPDATE room_calendar SET stock stock - 1 WHERE room_type_id #{roomTypeId} AND calendar_date #{date} AND stock 0) int decreaseStock(Param(roomTypeId) Long roomTypeId, Param(date) Date date);这种方式的好处是简单、可靠、不需要引入 Redis。如果后续并发量上来了可以再改造为 Redis 预扣减 数据库最终扣减的模式但小体量的民宿预订系统用数据库条件更新已经完全足够了。这里有很重要的一点要提醒整个扣库存和插订单的操作必须在同一个Transactional里任何一步失败全部回滚。我之前就是因为漏了Transactional出现过库存扣了但订单没生成的脏数据。3.3 订单状态流转与取消规则订单状态机设计完后我会在代码里把每个状态的变化都写得很明确。尤其在取消订单这块里面藏了不少业务规则。取消订单的逻辑Transactional public void cancelOrder(String orderNo, String reason) { Order order orderMapper.selectByOrderNo(orderNo); if (order null) { throw new BizException(订单不存在); } // 仅待支付订单可以取消 if (order.getStatus() ! 0) { throw new BizException(当前订单状态不可取消); } // 归还库存 ListDate nights getNightsBetween(order.getCheckInDate(), order.getCheckOutDate()); for (Date date : nights) { roomCalendarMapper.increaseStock(order.getRoomTypeId(), date); } // 更新订单状态 order.setStatus(5); order.setCancelReason(reason); order.setCancelTime(new Date()); orderMapper.updateById(order); }库存归还是跟扣减对称的也要在事务里一起执行。这里我还加了一个“超时未支付自动取消”的机制创建一个定时任务每 5 分钟扫描一次超过 30 分钟未支付的订单自动取消并归还库存。Component public class OrderTimeoutTask { Scheduled(fixedDelay 300000) public void processExpiredOrders() { ListOrder expiredOrders orderMapper.selectExpiredUnpaidOrders(30); for (Order order : expiredOrders) { cancelOrder(order.getOrderNo(), 超时未支付系统自动取消); } } }这个定时任务用的是 Spring 自带的Scheduled注解单机部署完全够用。如果以后要上集群就得考虑分布式定时任务和幂等控制了不过那都是后话小项目别过度设计。3.4 支付对接与回调处理支付这块是很多做 SpringBoot 项目的同学最头疼的地方。民宿这种场景用微信支付比较普遍但正经对接微信支付 API v3 需要商户号、API 证书、回调验签这一大堆东西容易绕晕。如果你只是做毕设或者 demo我建议用一个更轻量的方案模拟支付 支付回调接口。我当时的做法是创建订单后前端跳到“收银台”页面展示订单金额和“模拟支付”按钮。点击按钮后后端接口把订单状态从“待支付”改成“已支付”然后前端跳转到订单详情页。但要注意真实场景绝对不能这么干——一定要通过支付平台回调来确认支付结果。回调接口的设计如下PostMapping(/api/payment/wx-notify) public String wxNotify(RequestBody String xmlData) { // 1. 验证签名 // 2. 解析支付结果 // 3. 修改订单状态校验订单金额一致 // 4. 返回成功应答 }我做模拟支付的原因是把整个系统的业务逻辑跑通把支付相关的接口、状态流转、回调处理都留好了位置等真的拿到商户号只需要替换支付实现类即可。这种“接口先行、实现后补”的思路在开发前期能节省大量时间。4. 前端 Vue 实现要点4.1 路由与权限设计前端路由分了两个区域用户端H5 风格适配手机访问和管理端Element Plus 后台界面。我在路由设计时用到了 Vue Router 的动态路由能力——根据用户的角色动态注册可访问的路由。const routes [ { path: /, component: () import(/views/home/HomePage.vue), meta: { title: 首页, public: true } }, { path: /homestay/:id, component: () import(/views/homestay/HomestayDetail.vue), meta: { title: 民宿详情, public: true } }, { path: /order/confirm, component: () import(/views/order/OrderConfirm.vue), meta: { title: 确认订单, requiresAuth: true } }, { path: /admin, component: () import(/layouts/AdminLayout.vue), meta: { title: 管理后台, requiresAuth: true, requiresAdmin: true }, children: [ { path: dashboard, component: () import(/views/admin/Dashboard.vue) }, { path: room-type, component: () import(/views/admin/RoomTypeManage.vue) }, { path: calendar, component: () import(/views/admin/CalendarManage.vue) }, { path: orders, component: () import(/views/admin/OrderManage.vue) } ] } ]路由守卫里做登录校验和角色校验。requiresAuth的路由没有 token 直接跳登录页requiresAdmin的路由还要检查当前用户的角色是不是管理员。router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.requiresAdmin userInfo.role ! ADMIN) { next(/) return } next() })前端路由守卫只是用户体验层面的控制真正的接口安全还是靠后端的 JWT 拦截器来保证两层配合才靠谱。4.2 民宿日历组件的选型与封装日历是民宿预订系统里最核心的前端组件。用户要选择一个范围入住日期和离店日期同时要看到每一天是否有房、价格多少。我没有自己手写日历而是用了vue-calendar加自定义修改的方案因为手写日历要考虑农历、禁用日期、范围选择这些细节太费时间。核心是把后端返回的room_calendar数据映射到日历的日期单元格上const calendarData ref({}) // 加载指定月份的数据 async function loadCalendar(roomTypeId, yearMonth) { const { data } await getCalendar(roomTypeId, yearMonth) // data: [{ date: 2025-01-01, price: 368, stock: 3 }, ...] const map {} data.forEach(item { map[item.date] { price: item.price, stock: item.stock, canBook: item.stock 0 item.status 1 } }) calendarData.value map }日历渲染时根据状态来控制是否可以点击、颜色区分可订/满房。这个组件的样式我调了很久主要是移动端触控体验——点击开始日期、再点结束日期高亮选中区间边缘情况同一天入住离店、跨月选择都要处理。4.3 订单确认页与前端状态联动订单确认页是交互最复杂的页面。用户从民宿详情页选了入住日期和离店日期点击“立即预订”跳到/order/confirm页面这个页面要通过 URL 参数带过去房型 ID 和日期// 从详情页跳转 router.push({ path: /order/confirm, query: { roomTypeId: roomType.id, checkIn: checkInDate, checkOut: checkOutDate } })确认页要做几件事展示价格明细每晚价格、晚数、优惠、总价、填写入住人信息、提交订单。提交订单成功后路由跳转到收银台页面并把订单号带到收银台// 提交订单成功后 router.push({ path: /payment, query: { orderNo: res.data.orderNo, amount: res.data.totalAmount } })状态管理我用的是 Pinia因为 Vuex 的 API 偏繁琐Pinia 更简洁。全局 Store 里主要存了用户信息和购物车相关的零散状态订单详情都是通过接口实时拉取的不会在全局 State 里存太多敏感数据。4.4 图片上传与 MinIO 集成民宿的封面图、房型图、用户评价晒图都需要一个图片存储方案。直接存到应用服务器磁盘里的方案我直接排除了——重启丢失、扩容困难、磁盘占满都不好处理。云厂商的对象存储要开通服务、配 Bucket 也比较折腾于是选择了 MinIO 自建对象存储。后端集成 MinIO 其实很简单SpringBoot 的配置文件里加几行minio: endpoint: http://192.168.1.100:9000 access-key: homestay secret-key: homestay-secret bucket: homestay-images然后封装一个上传工具类用MinioClient上传文件并返回可访问的 URL。文件上传接口设计成 MultipartFile 接收限制文件大小和类型避免有人上传超大文件或者恶意脚本。这里有个细节文件不能只存 MinIO 的路径还要在数据库里记录因为后续删除、统计都方便。前端上传用el-upload组件配置好 action 地址和 headers把 token 带上。上传成功后返回的 URL 直接绑定到表单的图片字段上展示时用el-image渲染整个流程走得很顺。5. 常见问题与排查技巧实录做这个项目的过程中我记录了不少问题挑几个典型的分享给大家都是踩过坑才总结出来的。问题现象解决方案跨域请求失败前端请求后端接口报 CORS 错误后端配置跨域过滤器允许指定来源库存超卖同一房间同一天被订了两次使用条件更新 SQL 事务控制日期序列化格式不对前端拿到的时间是时间戳配置 Jackson 的日期格式为 yyyy-MM-dd前端路由刷新 404部署到 Nginx 后刷新页面白屏Nginx 配置 try_files 指向 index.html图片上传后访问 403MinIO 文件无法预览修改 Bucket 的访问策略为公开读URL 参数传递时日期错位跨月选择日期后数据对不上统一使用 YYYY-MM-DD 字符串提交避免 Date 对象时区问题5.1 跨域问题别用全局 CORS 配置一刀切前后端分离第一个遇到的就是跨域。前端在 5173 端口后端在 8080 端口不同端口跨域浏览器拦截了请求。我最初直接在 SpringBoot 里加了全局 CORS 配置允许所有来源。结果后面接入真实支付回调时发现第三方服务器请求后端接口也被允许存在安全隐患。后来改成了只允许指定的前端域名跨域访问Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173, http://localhost:3000) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }配置了allowedOrigins后跨域问题解决同时安全性也提高了。这里建议不要用allowedOrigins(*)加allowCredentials(true)的组合这俩是冲突的而且意味着任何网页都能带着用户 cookie 跨域请求后端属于高危配置。5.2 日期序列化与时区问题前后端日期传递是必踩的坑。Java 里的LocalDate、LocalDateTime如果直接用默认序列化前端拿到的是数组格式或者时间戳非常难受。解决方案是在application.yml里配置 Jackson 的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时后端实体类配合使用JsonFormat注解比如订单的入住日期标注为yyyy-MM-dd确保前端能拿到预期的格式。还有一个时区的小坑如果服务器装的 MySQL 和服务的时区不一致直接用new Date()存库可能会出现 8 小时的偏移。建议在 MySQL 连接 URL 里加serverTimezoneAsia/Shanghai确保驱动识别正确的时区。5.3 Nginx 部署与 Vue 路由刷新 404项目开发完要部署上线前端打包后用 Nginx 托管这时候会出现一个很经典的问题——用户在首页点进详情页没事但如果在详情页刷新一下直接 404。这是 Vue Router 的 history 模式导致的。路由是前端控制的但 Nginx 不知道/homestay/123应该返回index.html。解决办法是在 Nginx 配置中加try_fileslocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这样 Nginx 遇到不存在的路径时会去找index.html而不是直接报 404Vue Router 就可以接管路由解析了。后端部署我用的 DockerFROM openjdk:17-jdk-slim COPY target/homestay-backend.jar /app/homestay-backend.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/homestay-backend.jar]镜像很小启动也快配合 docker-compose 把 MySQL、MinIO、后端、前端一起编排起来一条命令就能启动整个环境。5.4 开发中常用的调试技巧项目开发过程中我积累了一些比较有价值的调试经验后端接口调试SprinBoot 项目里用Slf4j打日志很重要。特别是在事务回滚的时候不打印 SQL 和异常堆栈你根本不知道哪一步失败了。我在application.yml里配置了 MyBatis-Plus 的 SQL 日志mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台能看到每条 SQL调试起来一目了然。前端接口调试在 Vue 里封装 axios 实例时统一加了请求拦截器和响应拦截器。请求拦截器把 token 注入 header响应拦截器做了统一错误提示const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )开发的时候我还给 axios 配了 Vite 的代理避免开发环境每个请求都要写完整地址// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.5 订单数据一致性与并发压测项目上线前我做了简单的并发测试用 JMeter 模拟 100 个用户同时抢最后一间房。结果发现确实出现了库存为负数的情况——后来排查发现是 MyBatis-Plus 的updateById没有带库存条件导致的。我的经验是涉及资金和库存的操作尽量用自定义 SQL 手写条件更新而不是依赖框架的实体更新。框架只负责简化常规 CRUD关键业务逻辑必须是显式、可控的。另外事务控制要精确。Transactional默认只在 RuntimeException 时回滚如果业务里抛的是受检异常事务不会回滚。我建议在 Service 里都抛出自定义的BizException继承 RuntimeException这样所有业务失败都会正确回滚。6. 一些可以继续扩展的方向系统做完交付后我自己的体会是民宿预订系统看起来简单真正做起来业务深度完全不输电商系统。房态管理、价格策略、订单状态机、支付回调、并发控制每一个点都能延展出很多问题。如果你也准备做一个类似的系统或者想在上面继续迭代我建议几个方向多民宿平台化当前系统是针对单民宿设计的如果要做成平台需要引入民宿商户体系表结构要增加 merchant_id 维度订单分成、平台抽佣也要跟着设计。促销与优惠券日历房价的基础上加优惠券、满减、会员折扣价格计算器会变得更复杂需要引入规则引擎或者策略模式来管理。消息通知订单创建成功、支付成功、入住提醒通过短信或微信模板消息推送给用户这需要对接第三方短信服务可以在订单状态机里增加事件驱动的代码。数据统计报表民宿老板最关心的其实是经营报表——月度入住率、RevPAR每间可售房收入、房型贡献度、渠道来源分析。后端定时统计生成报表前端用 ECharts 展示。我在实际开发中还发现一个问题民宿行业淡旺季非常明显价格调整也很频繁日历操作在手机上要能做快速修改——比如一键设置国庆假期价格、批量调整周末价格。这个功能我是在管理后台的日历页面上做的具体做法是选中日期范围后弹窗输入加价比例或者固定价格批量更新room_calendar表。做完这个功能老板直说好用。最后分享一个小技巧如果你的项目是给客户交付的一定要写一个简单的部署文档。我把 Docker Compose 编排、Nginx 配置、MinIO 初始化、MySQL 初始化 SQL 都整理好放在项目里的deploy/目录下。这样不管你换机器还是客户换服务器照着文档跑一遍就能恢复整套环境。这个习惯比多写几行代码重要得多。写到这里整个 SpringBootVue 民宿客房预订管理系统的核心内容基本都讲完了。从技术选型到数据库设计从后端并发控制到前端日历交互每个环节都记录了我真实的开发和排坑过程。希望这篇文章能帮到你如果你也在做类似的全栈项目遇到问题欢迎一起交流踩过的坑大家互相帮忙填平做项目的路也会顺很多。