☰
SpringBoot+Vue+小程序打造民宿预约管理系统全栈实战
2026/10/16 3:32:17 网站建设 项目流程

民宿预约管理这块,我前后也折腾过几个项目,最近刚把一套基于 SpringBoot + Vue + 微信小程序的三端联调系统跑顺,正好拿它当例子,把完整的设计思路和踩坑实录捋一遍。这个项目我内部代号叫“117民宿”,也是从零开始搭的,包含了后台管理、小程序下单、订单流转这些核心模块。如果你正打算做一套类似的自用民宿管理系统,或者想拿这种全栈项目练手,这篇文章应该能帮你省掉不少弯路。

1. 为什么选这套技术组合:SpringBoot+Vue+小程序的业务逻辑拆解

1.1 民宿预约到底在管什么

很多人一听到“民宿预约系统”,下意识就觉得不过是“房间表+订单表”的 CRUD。真做起来才发现,民宿和标准酒店差别很大,逻辑绕多了。

普通酒店的房间基本是固定房号,民宿往往是多套房型、每套房源独立定价,而且价格会随淡旺季、节假日波动。更关键的是,入住时间不是标准的中午12点,很多民宿支持下午2点入住、次日12点退房,还有些房东允许提前入住或延迟退房。这就意味着,预约系统必须处理“时间段占用”和“日期价格变化”,不能只用简单的上下架状态来管理。

另外民宿的订单状态也复杂:用户在小程序端发起预约,需要先锁定房间,再在一定时间内完成支付,否则自动释放;支付完成后,房东端需要确认订单;之后还要处理入住登记、退房结算、取消退款等流程。每一步状态变化都牵扯到房间日历的占用情况、账务记录和消息通知。

所以一个真正能落地的民宿预约系统,核心模块至少包括:房源管理(房型、图片、设施)、日历价格管理(每天独立价格、批量设置)、订单管理(预定、支付、取消、入住、退房)、客户管理(微信用户信息、历史订单)、统计报表(入住率、营收)。这些模块之间互相联动,任何一个环节设计不清晰,后面都会改到崩溃。

1.2 三方技术各自承担什么角色

这套系统我用了三个端:管理后台用 Vue,后端服务用 SpringBoot,用户端用微信小程序。分工其实很清晰。

SpringBoot 负责提供所有接口和业务逻辑。包括用户登录鉴权、房源查询、订单创建、价格计算、状态流转等。选 SpringBoot 而不是 Node.js 或 Python,主要考虑是它生态太成熟了,MyBatis-Plus、Redis、微信支付 SDK 都有现成集成,而且事务管理、接口规范这些老牌优势在订单这种涉及资金的场景里特别值得信赖。民宿系统的复杂度不算高,SpringBoot 单服务完全扛得住,部署也简单,打一个 fat jar 就能跑。

Vue 做的是房东或运营方的管理后台。页面包含房源配置、日历价格、订单处理、数据看板等。Vue 的好处是组件化清晰,配合 Element UI 这类组件库,能很快把后台表格、弹窗、日历选择器做出来。相比传统 JSP 模板,Vue 前端和后端完全分离,接口联调时各改各的,后期要换皮肤、加功能也不牵连后端。

微信小程序面向的是用户。用户直接在微信里搜索小程序就能看到房源列表,不用下载 App,打开率高,预约流程也顺畅。小程序端负责展示、搜索房源、查看日历、提交订单、发起微信支付、查看订单状态。技术上用的是原生小程序语法加 WeUI 风格,配合自定义组件实现日历选择。

三个端的数据流是这样的:小程序端调 SpringBoot 接口完成预约;SpringBoot 操作数据库,同时给 Vue 后台提供管理接口;后台修改价格或处理订单后,小程序端下次请求自然读到最新状态。微信登录作为小程序的用户标识,换取 openid 后在后端生成 token,实现会话管理。

1.3 这套选型能避开哪些坑

选这套组合不是赶时髦,而是每类技术都刚好解决民宿业务里的一个痛点。

单说微信小程序端,最大的坑就是“登录态失效”。微信小程序有自己的 code 换 session 机制,如果后端不处理好 token 刷新,用户隔几天打开小程序就会提示“登录过期”。我用 SpringBoot + Redis 存 token,设置合理的过期时间,配合小程序端的静默登录检查,基本能做到无感续期。

管理后台用 Vue 的另一个好处是适合“日历价格批量设置”这种交互。民宿价格经常要按周设置周中价、周末价,传统表单很难用,Vue 里可以写一个日历热力图组件,鼠标拖选日期,批量填价格,体验好很多。这个后面我会细讲。

而 SpringBoot 自带的事务管理,在“下单锁房”这个操作上帮了大忙。用户提交订单时,需要先查询房间在当前日期段是否可订,若可订则插入订单并锁定日历。这个动作如果拆分在多个接口里完成,很容易因为并发问题导致房间超卖。用 SpringBoot 的 @Transactional 包住整个操作,配合数据库行级锁或者 Redis 锁,能稳定处理并发预约。

2. 核心数据模型与接口设计

2.1 表结构设计:从房间、订单到日历价

数据表设计是这套系统里最值得反复琢磨的部分。一开始我用常规的“房间表 + 订单表”两级结构,后来发现漏了“每日房价”这个概念,导致改价格必须去改订单,非常痛苦。

推荐的表结构至少包含这几张核心表。

房源表(room)记录每个房源的固定信息,比如房型名称、面积、床型、可住人数、封面图、配套设施、退订规则、状态(上架/下架)。字段里建议加上“排序权重”和“创建时间”,后台排序和筛选都要用。

房源日期价格表(room_price)是民宿系统特有的一张表,记录每个房源在特定日期的价格、是否可订、是否包含特殊加价。比如 2025 年 6 月 1 日至 3 日,房源 A 价格为 428 元,状态为可订。这样每天可独立调整价格,和订单表隔离。查询时通过房间 ID + 日期范围批量取出来,就能计算订单总价。

订单表(orders)相对复杂,但别急着把状态字段设计成字符串。建议用整型或枚举值存储,比如 0-待付款、1-已付款待确认、2-已确认待入住、3-已入住、4-已退房、5-已取消、6-退款中、7-已退款。另外必须单独存一个“订单状态快照”字段吗?其实没必要,状态放到单独的子表太繁琐,就放在主订单表里,毕竟每次一个订单只会处于一个状态。

订单表里还需要记录用户 openid、房源 ID、入住日期、退房日期、入住人数、原价、优惠金额、实付金额、支付单号、退款单号、创建时间、更新时间。注意入住人数和房价没有直接关系,但民宿需要按人数限订,所以必须有。

用户表(user)主要存 openid、昵称、头像、手机号、会员等级、累计消费等。手机号最好在用户授权后才填,不要一开始就强制收集。

另外建议加一张日历库存表(room_stock)或者直接把是否可订的标记放在价格表里。我倾向于后者,即价格表里同时保存“该日可售数量”,因为有些房型不止一间,需要可售数量递减。

表之间的关系并不复杂:房源 1 对多 日期价格,房源 1 对多 订单,用户 1 对多 订单。但在数据库索引上,订单表一定要建联合索引(房源 ID + 入住日期),价格表要建(房源 ID + 日期)的唯一索引,否则查询尤其是按日期筛选房源时会明显变慢。

2.2 接口设计要点:返回结构、鉴权与幂等

接口设计上,我踩过一次坑是“返回结构不统一”。团队里各写各的,有的返回 json 里带 data,有的带 result,前端联调很痛苦。后来统一了所有接口的返回格式,类似这样:

{ "code": 0, "message": "success", "data": {} }

code 为 0 表示成功,非 0 表示业务错误码。比如 40001 表示参数错误,40002 表示未登录,50001 表示房间不可订。前端只看 code 和 data 就行,message 仅在失败时提示给用户。

接口路径按资源划分,例如:

  • POST /api/user/login —— 微信登录
  • GET /api/room/list —— 房源列表(支持按日期、人数筛选)
  • GET /api/room/detail?id=xxx —— 房源详情
  • GET /api/room/price?roomId=&startDate=&endDate= —— 查询某房源的日期价格
  • POST /api/order/create —— 创建订单
  • GET /api/order/list —— 查询我的订单
  • POST /api/order/cancel —— 取消订单
  • POST /api/admin/room/save —— 后台保存房源
  • POST /api/admin/room/price/batch —— 批量设置价格

鉴权我用的是 token 机制。小程序登录后,后端通过微信 code 换取 openid,生成一个随机 token,存到 Redis 中,key 为 token,value 为 openid + 用户ID,过期时间设 7 天。前端请求时在 header 里带Authorization: Bearer token,后端写一个拦截器统一校验。需要注意拦截器要放行那些公开接口,比如登录、房源列表,否则小程序刚打开就会报未登录。

还有幂等性问题。用户在小程序里点击“提交订单”时,如果网络慢,他可能会点两次。如果不做幂等,就会生成两条同样订单。解决方案是前端在创建订单时生成一个 requestId(UUID),传给后端;后端在订单表里加一个唯一索引request_no,创建前根据 requestNo 查一下是否已有订单,有就直接返回已有订单,没有则创建。这个方法简单有效,我一直在用。

2.3 状态流转:预约、支付、入住、退房

状态机是业务代码里最容易出 bug 的地方。民宿订单状态不能乱跳,每一步都要有明确的前置条件。

用户在小程序选好日期,点击“预约”后,后端创建订单,状态为待付款。此时房间日历应该被“锁定”但不算实际占用。我的做法是扣减可售数量,等于把那一晚的库存先占住,但会设置一个支付超时时间,比如 15 分钟。超过 15 分钟订单自动取消,同时恢复库存。

用户支付成功后,订单变为“已付款待确认”。这里我要提醒:是否需要房东确认这一步,要看业务需求。有些民宿是自动确认,支付后直接变为“已确认”;但多数单体民宿房东担心房客素质,想先看看信息再决定接不接单。所以系统里我留了一个开关,房东可以选择自动确认还是手动确认。

房东在小程序或后台确认后,订单状态变为“已确认待入住”。到了入住日期当天,用户办理入住,房东或系统将其置为“已入住”。退房日到来后,可以手动点击退房,也可以在后台定时任务里自动将超过退房时间的订单置为“已退房”。

取消逻辑也要单独考虑。待付款状态下可以随意取消,回补库存;已付款状态下取消则要走退款流程,生成退款单,调用微信退款接口,成功后状态变为“已退款”。这里要特别注意,退款后不能直接把状态变成“已取消”,而是要保留原订单记录,只是金额回流。

为了把状态流转梳理清楚,我在代码里单独写了一个OrderStateMachine类,集中定义每种状态允许的“动作”。比如只有待付款状态才允许pay,已付款待确认状态才允许confirm,已入住状态才允许checkout。这样后续加新状态时不会把逻辑散落到各个 service 里。

3. 从零搭起:后端、后台、小程序的实现流程

3.1 后端:SpringBoot初始化与业务分层

后端我用的是 SpringBoot 2.7 + MyBatis-Plus + MySQL 8 + Redis。项目初始化直接去 Spring Initializr 生成即可,依赖选择 Lombok、Spring Web、MyBatis-Plus、MySQL Driver、Redis、Validation。

启动类不用多谈,重点讲一下分层的习惯。我按常见的四层结构来组织:Controller、Service、Mapper、实体类。Controller 只做参数接收和结果封装,不写业务逻辑;Service 负责业务规则;Mapper 用 MyBatis-Plus 的 BaseMapper 继承,多表查询写 XML。

项目结构大致如下:

src/main/java/com/example/minsu ├── controller │ ├── user/UserController.java │ ├── room/RoomController.java │ └── order/OrderController.java ├── service │ ├── user/UserService.java │ ├── room/RoomPriceService.java │ └── order/OrderService.java ├── mapper │ ├── RoomMapper.java │ ├── RoomPriceMapper.java │ └── OrderMapper.java ├── entity ├── common │ ├── Result.java │ ├── BizException.java │ └── GlobalExceptionHandler.java └── config ├── WebMvcConfig.java └── RedisConfig.java

在Result类里定义静态方法ok()和error(),所有 Controller 返回Result类型。GlobalExceptionHandler捕获业务异常和参数校验异常,统一返回友好的错误信息,不要直接把异常堆栈抛给前端。

后端一个关键代码是创建订单的 service 方法。伪代码如下展示核心逻辑:

@Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderDTO dto) { // 幂等校验 Order existing = orderMapper.selectOne( new LambdaQueryWrapper<Order>() .eq(Order::getRequestNo, dto.getRequestNo())); if (existing != null) { return existing; } // 校验用户是否登录 User user = userService.getById(dto.getUserId()); if (user == null) { throw new BizException(40002, "用户不存在"); } // 校验房源是否上架 Room room = roomMapper.selectById(dto.getRoomId()); if (room == null || room.getStatus() == 0) { throw new BizException(40003, "房源已下架"); } // 校验日期合法性 LocalDate start = dto.getStartDate(); LocalDate end = dto.getEndDate(); if (start.isBefore(LocalDate.now()) || end.isBefore(start)) { throw new BizException(40001, "日期不合法"); } // 查询日期价格并锁定库存 List<RoomPrice> prices = roomPriceMapper.selectList( new LambdaQueryWrapper<RoomPrice>() .eq(RoomPrice::getRoomId, dto.getRoomId()) .between(RoomPrice::getDate, start, end) .eq(RoomPrice::getStatus, 1) .forUpdate()); // 加行锁防止并发 if (prices.size() != ChronoUnit.DAYS.between(start, end) + 1) { throw new BizException(50001, "所选日期不可订"); } for (RoomPrice price : prices) { if (price.getStock() <= 0) { throw new BizException(50001, price.getDate().toString() + " 已满房"); } } // 扣减库存 for (RoomPrice price : prices) { roomPriceMapper.decreaseStock(price.getId()); } // 计算总价 BigDecimal totalAmount = prices.stream() .map(RoomPrice::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); // 插入订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(user.getId()); order.setRoomId(room.getId()); order.setStartDate(start); order.setEndDate(end); order.setPersonCount(dto.getPersonCount()); order.setTotalAmount(totalAmount); order.setPayAmount(totalAmount); order.setStatus(0); order.setRequestNo(dto.getRequestNo()); orderMapper.insert(order); return order; }

注意forUpdate()加的是行锁,必须在事务内执行,并且数据库连接要开事务,否则锁不生效。这一步是防止两个用户同时下单同一间房的最后一天时出现超卖。

3.2 管理后台:Vue页面与接口联调

管理后台用 Vue 3 + Vite + Element Plus。这里我直接建了一个标准布局:左侧菜单栏、顶部用户信息、右侧内容区。路由分为房源管理、价格日历、订单管理、客户管理、统计报表。

开发环境下,前端通过 Vite 的server.proxy把/api代理到本机 8080 端口,就不会有跨域问题。部署时则用 Nginx 反向代理到后端服务,前端只负责页面和静态资源。代码里封装了一个request.js,统一设置 baseURL、请求拦截器、响应拦截器,并在请求头加上 token:

import axios from 'axios' import { ElMessage } from 'element-plus' 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 !== 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message) return Promise.reject(error) } ) export default request

价格日历是后台最有特色的部分。我用的方案是基于 Element Plus 的el-calendar做二次封装。日历上每个日期格子显示当天价格,管理员可以点击开始日期,再点击结束日期,弹出批量设置面板。批量设置请求会一次性提交从开始到结束每一天的价格、库存和是否可订状态,后端用一个循环 upsert 入库。

这里要注意,批量设置价格时不能直接把旧的 record 删掉再插入,因为如果有订单已经引用了某个日期的价格,删除会导致数据不一致。应该用数据库的on duplicate key update或者先查询再更新,保留主键。

订单管理页面就是最常见的表格,筛选条件有状态、起始日期、房源名称。操作按钮根据状态动态显示,比如待付款订单显示“关闭订单”,已付款订单显示“确认入住”,已入住订单显示“退房结算”。每做一步操作,前端都要弹确认框,避免误触。

3.3 小程序端:登录、日历选房与下单

小程序端我用的原生框架,没有引第三方组件库,因为页面不算多,原生足够。页面清单:首页(房源列表)、房源详情、下单页面、订单列表、订单详情、个人中心。

小程序登录逻辑是核心。App 启动时调用wx.login获取 code,然后把 code 传到后端/api/user/login。后端用 code 向微信服务器换 openid,再返回自定义 token。小程序把 token 存到本地缓存wx.setStorageSync。每次请求时在 header 里带上 token。如果接口返回 40002,说明 token 过期,此时需要静默重新wx.login。

代码大致这样:

function login() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { request.post('/user/login', { code: res.code }).then(data => { wx.setStorageSync('token', data.token) resolve(data) }).catch(reject) }, fail: reject }) }) }

日历选房是小程序端的核心交互。用户进入房源详情后,需要选择入住日期和退房日期。我封装了一个双日历组件,左侧显示当前月份,右侧显示下个月份,滑动切换。每个日期格子根据后端返回的当日价格和可订状态,显示价格或者“满房”灰色不可点。

日期选择的逻辑要处理好跨月选择。基本规则是:首先只能选择今天及以后日期;然后,先选择入住日,再选择退房日,退房日必须晚于入住日。选完后,页面底部会实时计算总价,显示住几晚、每晚价格多少。

下单页面通过提交订单接口传入房间 ID、入住日期、退房日期、入住人数、联系人手机号。后端校验后返回订单号。请求成功后,前端跳转到“订单详情”页面,显示待支付状态,点击“支付”按钮时调起微信支付。真实的微信支付需要商户号和证书,我这里在开发环境用模拟支付成功,打日志模拟回调,方便测试整个订单流程。

小程序端还有一个容易踩坑的地方是“头像昵称填写”。老版本可以直接wx.getUserProfile,后来微信收紧了,比较稳妥的方案是引导用户点击“授权头像昵称”按钮,用button open-type="chooseAvatar"和input来获取,然后传给后端更新用户信息。

4. 实操中的常见坑与排查技巧

4.1 微信登录session不一致问题

这个坑我印象太深了。一开始我把 token 存在内存里面,也就是一个 ConcurrentHashMap。结果本地测试没问题,部署到服务器后,用户在小程序登录过了,过一阵子再访问却老提示未登录。后来查出来是因为服务重启导致内存清空,而且多实例部署时,实例 A 存的 token 实例 B 根本查不到。

解决办法就是换成 Redis。token 为 key,用户信息为 value,设置过期时间。Redis 是独立于服务进程的存储,服务重启不会丢,多实例共享一个 Redis 就能避免 session 不一致。同时要注意,微信端 code 换取 openid 的接口是有频率和时效限制的,如果每次请求都去换 openid,容易把接口配额刷爆。正确做法是只在首次登录时换一次 openid,之后都用 token 来识别。

另一个相关的问题是“同一用户多个设备同时登录”。一个 openid 可以同时被多个微信号绑定吗?实际上 openid 是每个用户在每个小程序下唯一的。如果用户在两个设备上登录同一个微信号,会出现两个 token 对应同一个 openid。这没问题,后一个 token 可以直接覆盖前一个,同时让前一个失效。为了安全,我通常只保留最新 token,不用为了兼容多设备而刻意设计多 token 并存。

4.2 日期冲突与并发抢房的数据库锁

民宿预约并发量不会像秒杀那么大,但也会出现“两个人同时看中了同一间房的同一个周末”的情况。

我最早做的版本没有加锁。用户 A 和用户 B 同时点了提交订单,后端都去查数据库,发现房间可订,都创建了订单。结果一天晚上被订出去两次。后来开始用悲观锁,也就是前面提到的forUpdate()。但注意,在 MySQL 的 InnoDB 下,给查询条件加forUpdate()时要命中索引,否则锁不上或者锁全表。

另一个坑是 Redis 分布式锁。很多人会想到用setnx做库存扣减,但要注意设置过期时间的原子性。如果先setnx再单独调 expire,进程突然挂了,锁永远不释放。正确写法是set(key, value, "NX", "EX", 30)。但 Redis 锁在涉及数据库事务时,还要考虑锁释放时机和事务提交顺序。我最终选择的策略还是依赖数据库行锁,简单可靠,逻辑都包在事务里。

此外,日期扣减库存还有“踩踏”问题。比如某天是国庆节,库存只有 1 间,做成扣减库存操作要update room_price set stock = stock - 1 where id = ? and stock > 0,通过受影响行数判断是否扣减成功。如果影响行数为 0,直接抛异常“该日期已满房”。

4.3 图片上传与跨域问题速查

图片上传在小程序端和管理后台都会用到。小程序上传图片用wx.uploadFile,后端接收 MultipartFile,保存到本地目录或对象存储。如果保存本地,大坑是路径问题。我当时直接写了file.savePath = "/tmp/",结果重启后图片全没了。后来改成一个独立目录,比如/home/ubuntu/minsu/upload/,并且配置了静态资源映射,让上传后的图片可以通过 URL 访问。

另一个是在管理后台开发时遇到的跨域问题。早期我把前端跑在 5173 端口,后端跑在 8080 端口,直接 axios 请求后端,控制台报跨域。我试过加@CrossOrigin注解,也试过 CorsFilter,但都不够灵活。最终选择用 Vite 代理解决:

server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样浏览器看到的前端地址就是 5173,不存在跨域。生产环境用 Nginx 也如法炮制:把前端静态文件放在 80 端口,Nginx 将/api反向代理到后端 8080 端口。前端永远只请求相对路径/api/xxx。

跨域问题排查时,还要注意后端是否会返回 CORS 相关的 header。如果用了网关或 Nginx,也需要检查一层层代理是否有响应头丢失。最简单的排查方法是在浏览器开发者工具里看请求响应的Access-Control-Allow-Origin是否存在,不存在就从代理和后端两边找原因。

5. 最后分享一点实际的优化心得

这套系统从立项到跑通,大概花了两周多一点。真正让我觉得值得分享的,不是某一行代码,而是“设计先行、状态明确、接口规范”这套工作习惯。

我个人最满意的是日历价格表的设计。如果一开始只做一个简单的房间价格字段,后期做节假日加价、连住优惠、满房控制的时候会非常痛苦。现在所有跟日期相关的逻辑都依赖价格表,灵活度很高。比如淡季可以给某一天设置 0 元,表示不打折但可订;旺季可以批量设置加价百分比,系统自动算出每日价格,再叠加到订单总价里。

还有一个经验是支付功能的开发顺序。强烈建议先把订单生命周期理清楚,再接入真实微信支付。开发环境用“模拟支付成功”来测试后续逻辑,等所有流程稳定了,再替换成真实的wx.requestPayment和支付回调。这样不会把支付调试的复杂性混入订单逻辑排查中。支付回调尤其要注意幂等,微信会多次回调同一个支付结果,后端用“商户订单号 + 交易号”的唯一约束防止重复处理。

如果你打算扩展这套系统,可以往几个方向考虑:接入短信通知,在订单状态变化时发提醒;增加优惠券模块,在订单创建时计算优惠;做一个小程序端的“房东端”,让房东在手机上直接确认订单和改价格;再加一个简单的数据大屏,展示入住率、营收趋势,给运营做决策参考。

最后再补充一个容易被忽略的小技巧:代码里所有金额字段不要用double,一律用BigDecimal,数据库字段用decimal(10,2)。别小看这个问题,金额精度差一分钱,对账的时候能让你查到怀疑人生。我在第一个版本就吃过亏,从此以后对金额处理格外谨慎。这套系统虽然不算复杂,但把细节打磨到位,线上下单、后台管理、微信小程序三个端配合起来,已经能支撑一家小型民宿的正常运营了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询