做农家乐管理系统这套东西,最早是因为一位做旅游行业的朋友找上我。他们那边几十家农家乐还在用Excel记房态、记订单,旺季的时候前台忙得脚不沾地,还经常出现一房多订、退房时账目对不上的问题。后来我用 Java SpringBoot + Vue3 + MyBatis 这套前后端分离方案,配合 MySQL 数据库,给他做了一整套数字化农家乐管理平台,从客房预订、入住登记、餐饮点单到经营结算全部打通。今天不聊虚的,就把这套系统的数据库设计、后端接口思路、前端页面联调、部署上线流程以及我实际踩过的坑,完整拆一遍。
这篇内容适合谁看?一种是正在做毕业设计、想找完整全栈项目参考的同学,另一种是给农家乐、民宿、小型酒店做数字化升级的开发者。不管你是想自己从零搭一套,还是想理解 SpringBoot + Vue3 前后端分离项目的完整链路,这篇都能给你不少可以直接抄作业的东西。
1. 需求拆解:农家乐平台到底要管什么
1.1 传统农家乐的业务痛点
在做这套系统之前,我专门去朋友那边蹲了两天,观察他们的真实工作流。传统农家乐的管理状态基本是这样的:前台一个大本子,记着各类房型的预订情况;厨房下单靠口头喊,或者手写小纸条;退房结账的时候,要把住宿费、餐饮费、游玩项目费用一笔一笔翻出来核对。
这里面最痛的不是累,而是乱。房间状态完全靠人脑记忆,客户电话预订了国庆的房间,前台忘记记录,客人到店发现房间被重复订出去了,体验直接崩塌。餐饮和住宿分开记账,客人退房时账单对不上,最后往往是老板自己贴钱息事宁人。还有一点,老客户没有沉淀,客人走了就失联了,回头客只能靠缘分。
所以数字化平台要解决的,本质上不是"把纸质换电子",而是把业务流理顺:客户在线看房、下单、支付,前台实时看到房态变化,厨房接到电子订单,退房时系统自动汇总账单,老板在后台看经营报表。一句话,让每一间房、每一笔钱、每一个客人都能被系统追踪到。
1.2 功能模块怎么划分
基于上面的痛点,我把系统拆成了用户端和管理端两大块。
用户端面向游客,核心功能有:
- 注册登录:手机号加密码,用 JWT 做登录态管理。
- 客房展示与预订:按日期查询可订房间,看房型图片、价格、设施说明,提交预订订单。
- 餐饮菜单浏览:查看菜品分类、价格、图片,加入购物车后统一提交。
- 个人中心:查看自己的订单列表、订单详情、取消未支付的订单、发表评论。
- 公告板:查看农家乐发布的优惠活动和园区通知。
管理端面向农家乐老板和前台,功能更多一些:
- 数据看板:今日订单量、今日营收、入住率、近一周营收趋势。
- 房型管理:增删改查房型,设置价格、床位数、面积、描述和图片。
- 房间管理:维护具体房间号、所属楼层、房间状态(空闲/入住中/打扫中)。
- 订单管理:查看所有订单,确认预订、办理入住、办理退房、取消订单。
- 菜品管理:维护菜单,上下架菜品,调整价格。
- 评论管理:查看游客评论,回复或删除不当评论。
- 用户管理:查看注册用户列表,禁用恶意账号。
- 公告管理:发布和下线公告。
1.3 技术栈选型的逻辑
技术栈选择上,SpringBoot + Vue3 + MyBatis + MySQL 这套组合,放在当下依然是中小型管理系统最稳的方案,没有之一。
SpringBoot 的优势是起步快、配置少、生态成熟。一个空项目拉起来,写两个接口跑通,十分钟都不用。对于这种以 CRUD 为主的业务系统,SpringBoot 的开箱即用特性特别合适,不需要像 Spring 老项目那样写一堆 XML 配置。
Vue3 这边,组合式 API 写业务逻辑比选项式 API 更顺手,配合 Vite 的冷启动速度,开发体验很好。再加上 Element Plus 这套组件库,后台管理页面的表格、表单、弹窗、日期选择器基本不用自己造轮子。
MyBatis 在这个项目里比 JPA 更合适。农家乐系统的报表查询、房态冲突检测、订单分页筛选,都是 SQL 极其灵活的查询场景。MyBatis 把 SQL 写在 XML 里,可控性最强,复杂查询出了问题一眼就能看到是哪段 SQL 写错了。JPA 虽然省事,但碰到复杂查询时自动生成的 SQL 往往不是你想要的,排查起来反而更费劲。
MySQL 就不用多说了,中小业务量级下性能、稳定性、运维成本都是最优解。前后端分离则是为了后续扩展,比如以后要接小程序端、App 端,后端接口可以直接复用,不用重新开发。
2. 数据库设计:整个系统的地基
数据库设计是我在这个项目里花时间最多的地方。前期表结构没想清楚,后面写代码全是坑。我最终设计了 9 张核心表,下面把表结构和几个关键设计思路展开讲。
2.1 核心表结构一览
- 用户表 user:id、username、password、nickname、phone、avatar、role、status、create_time。role 字段区分管理员和普通用户,值取 ADMIN 或 USER,不用单独建角色表,这个量级的项目用字段枚举就够了。
- 房型表 room_type:id、name、price、bed_count、area、description、image、status。status 控制房型是否对外展示。
- 房间表 room:id、type_id、room_no、floor、status。status 表示房间当前物理状态,0 空闲、1 入住中、2 打扫中。
- 订单表 orders:id、order_no、user_id、room_id、check_in_date、check_out_date、total_amount、status、create_time、pay_time。status 取 0 待支付、1 已支付待入住、2 已入住、3 已完成、4 已取消。
- 菜品表 menu:id、name、category、price、image、description、status。
- 餐饮订单表 orders_item:id、order_id、menu_id、menu_name、price、quantity。冗余 menu_name 和 price 是为了防止菜品改价或删除后历史订单数据失真。
- 评论表 comment:id、user_id、order_id、rating、content、create_time。
- 公告表 notice:id、title、content、create_time、status。
- 游玩项目表 activity:id、name、price、description、image、status。
2.2 订单表和房间状态的设计思路
这个项目里最重要的表就是 orders。我踩过的第一个坑,就是最初把房间状态设计成了房间表上的一个字段,预订时直接修改状态。后来发现完全行不通,因为房间状态必须是有时间维度的,同一间房今天空闲、明天被订,不能简单用一个状态字段表示。
最终方案是:房间表只维护物理状态(是否在住、是否脏房),预订状态完全由订单表根据入住离店日期动态计算。查询某段时间哪些房可订,就是找该时间段内没有冲突订单的房间。
订单号我用了时间戳加随机数的方式生成,格式类似 ORD20250101123045001,避免并发下订单号重复。金额计算放在后端,前端传过来的价格只做展示。
预订的可用性检测 SQL 是整个系统最核心的一段逻辑。要判断某个房间在入住日期和离店日期之间是否已被占用,不能只看日期相等,还要处理区间重叠,判断逻辑是:新订单的入住日期小于原订单的离店日期,且新订单的离店日期大于原订单的入住日期。这段思路理解了,房态查询就通了。
2.3 数据库初始化与测试数据的准备
项目里我提供了一个 init.sql 脚本,包含建表语句和测试数据。测试数据这块有个建议,别只插几条意思一下,要模拟真实场景:不同房型各插几间房、菜品覆盖多个分类、订单横跨过去三个月到未来一个月、评论覆盖不同评分。
这样开发前端页面时,列表、筛选、分页、图表都有真实数据可以调试,不用边开发边造数。初始化数据还有一个作用,就是验证 SQL 的性能,虽然数据量不大,但该建的索引要建好。orders 表的 room_id、check_in_date、check_out_date、status 字段,room 表的 type_id,user 表的 username,这些都要建索引。预订查询虽然数据量小的时候感受不到差别,但真到旺季几千单的时候,没有索引的查询会让接口明显变慢。
3. 后端实现:SpringBoot + MyBatis 的关键代码思路
3.1 项目分层与包结构设计
后端工程我采用了经典的分层结构,按技术职责横向划分,controller 层接收请求、service 层处理业务、mapper 层操作数据库、entity 放实体类。同时拆出 config、common、dto、util 等辅助包。
这种结构最直观,团队新人也容易上手。业务逻辑简单的项目不需要按模块垂直拆分,否则每个模块里都是同样的 controller、service、mapper,反而显得重复。把公共部分抽出来,比如统一返回结果类 Result、统一异常处理器 GlobalExceptionHandler、JWT 工具类 JwtUtil,放在 common 包里,各模块直接调用。
统一返回结果这块,我封装了一个 Result 类,结构是 code、message、data 三段。所有接口统一返回这个格式,前端 Axios 响应拦截器里统一处理。这样做的好处是,前端不用每个接口单独判断成功失败,一次封装到处复用。
3.2 JWT 认证与拦截器权限控制
登录认证我用了 JWT,没有引入 Spring Security。不是 Spring Security 不好,而是这个项目的权限模型太简单,只有 ADMIN 和 USER 两种角色,用拦截器加注解就能搞定,引入 Security 反而要写一堆配置。
具体实现是:登录接口验证用户名密码成功后,生成一个 token,把用户 ID 和角色塞进 token 的 claims 里,返回给前端。前端存储 token,每次请求在请求头里带上 Authorization 字段。后端定义一个拦截器,拦截所有需要登录的接口,解析 token,解析失败返回 401。
需要管理员权限的接口,我在 controller 方法上加了自定义注解 @RequireAdmin,拦截器里解析 token 中的角色字段,不是 ADMIN 就直接拒绝。这样权限控制的粒度合适,也容易理解。
有个细节要注意:token 过期时间我设置的是 24 小时,但实际用户可能连续住好几天。如果 token 过期了,前端路由守卫会拦截到 401,自动跳转登录页,但这样体验不好。更好的做法是前端在 Axios 响应拦截器里捕获 401,弹出提示,然后跳转登录页,并保留当前页面路径,登录成功后跳回来。这个小体验很多人会忽略,但我建议做上。
3.3 MyBatis 动态 SQL 处理复杂查询
MyBatis 的 XML 里写动态 SQL,是这个项目最出效果的部分。拿房态查询举例,用户在前端选择入住日期、离店日期、房型、价格范围,后端需要组合查询符合条件的房间。如果用 Java 代码拼 SQL,条件一多代码就乱套了,动态 SQL 就干净很多。
下面这段是房态查询的核心 SQL,逻辑是房间没被其他订单占用,并且满足价格和房型筛选条件:
<select id="findAvailableRooms" resultType="com.example.farmstay.entity.Room"> SELECT r.*, rt.name AS typeName, rt.price FROM room r LEFT JOIN room_type rt ON r.type_id = rt.id WHERE r.status = 0 <if test="typeId != null"> AND r.type_id = #{typeId} </if> <if test="minPrice != null"> AND rt.price >= #{minPrice} </if> <if test="maxPrice != null"> AND rt.price <= #{maxPrice} </if> AND r.id NOT IN ( SELECT room_id FROM orders WHERE status IN (1, 2) AND (#{checkIn} < check_out_date AND #{checkOut} > check_in_date) ) </select>这里有个特别容易被新人踩的坑:XML 里小于号要写成<,大于号要写成>。我第一次写的时候直接在 SQL 里写了<,启动报错,找了半天才反应过来是 XML 转义的问题。
3.4 订单状态流转的 Service 层实现
订单状态流转是这个项目业务逻辑的核心。我的实现方式是,在 OrderService 里定义了 cancelOrder、checkIn、checkOut 三个方法,每个方法都先校验当前状态是否符合流转条件,再更新状态和对应时间字段。
举个例子,cancelOrder 只允许取消状态为待支付或已支付的订单,并且要判断入住日期距离当前时间是否大于 24 小时,否则不能取消。checkIn 要求订单状态必须是已支付,同时要更新房间状态为入住中。checkOut 要求订单状态是入住中,计算实际入住天数,更新订单总金额,把房间状态改为打扫中。
这套状态机放在 service 层统一管理,比散落在 controller 里好维护太多。每个方法开头先查订单、校验状态、执行更新,逻辑清晰也不容易出现状态错乱。
4. 前端实现:Vue3 界面与联调细节
4.1 前端工程结构与路由设计
前端用的 Vite + Vue3 + Pinia + Vue Router + Element Plus + Axios 这套组合。工程结构按页面模块划分:views 下面分 user 端页面和 admin 端页面,components 放复用组件,api 下面按业务模块放接口请求文件,router 放路由配置,store 放 Pinia 状态。
路由设计上,我把页面分成三类:公开页面(登录、注册、首页、客房列表、菜单展示)、需要登录的页面(个人中心、订单提交、我的订单)、需要管理员权限的页面(后台所有管理页面)。
通过路由守卫实现权限控制,核心逻辑是判断 token 是否存在,再判断目标路由的 meta 里有没有 requireAdmin 属性,有的话还要从 Pinia 里拿用户角色,如果角色不是 ADMIN 就重定向到首页。这套方案不复杂,但很实用。
4.2 Axios 封装与跨域处理
Axios 封装几乎是每个 Vue 项目都绕不开的环节。我在 api 目录下建了一个 request.js,做统一配置:
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)) } return res.data }, error => { return Promise.reject(error) } )baseURL 设成 /api,开发环境通过 Vite 代理转发到后端,生产环境通过 Nginx 反向代理转发。这样前端代码里不用写死后端地址,环境切换非常方便。
开发环境的跨域问题,在 vite.config.js 里配置代理就行,这是开发联调时最常用的解法。
4.3 核心页面实现要点
前台首页的房态预订页是这个系统的流量入口。我用了一个日历组件让用户选择入住和离店日期,选完日期后调用房态查询接口,返回可订房间列表。这里前端做了一个细节:日期选择器禁止选择今天之前的日期,并且离店日期必须晚于入住日期,用 Element Plus 的 disabledDate 回调就可以实现。
房型卡片上展示房型图片、名称、床位数、面积、价格,点击预订按钮跳转到订单确认页。订单确认页展示了入住天数、单价、总价,用户确认后提交订单,生成待支付订单,然后跳转到订单列表页面。
餐饮点单页我实现了类似购物车的交互:左侧菜品分类,右侧菜品列表,点击加号添加到购物车,右上角显示购物车数量和总价。提交后生成餐饮订单,关联到当前用户。
管理端的订单管理页面是最复杂的页面。用了 Element Plus 的 el-table 展示订单列表,状态列用 el-tag 做不同颜色的标签,操作列根据订单状态动态显示可用按钮。比如待支付订单显示"取消订单"按钮,已支付订单显示"办理入住",已入住订单显示"办理退房"。这样运营人员不用记状态流转规则,系统告诉你能点什么。
5. 部署上线全流程实录
系统开发完,下一步就是部署上线。前后端分离项目的部署比单体项目多几个环节,很多人卡在这一步,我把我实际操作过的一套流程完整写下来。
5.1 环境准备与版本选型
部署环境我建议用一台 Linux 服务器,2核4G 配置就够跑这个系统。下面是版本选型的参考:
- JDK 1.8 以上,建议直接用 JDK 11,使用更稳定
- Maven 3.6 以上,用于后端打包
- MySQL 5.7 或 8.0,推荐 MySQL 8.0,字符集默认 utf8mb4,更好的中文支持
- Node.js 16 以上,用于前端构建
- Nginx 1.20 以上,用于部署前端静态文件和反向代理后端接口
这里有个容易踩的坑:MySQL 8.0 连接串时区问题比较典型,必须加上 serverTimezone=Asia/Shanghai,否则连接报错。
后端数据库连接配置示例:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/farmstay?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver5.2 数据库导入与后端启动
数据库初始化这块,把项目里的 init.sql 复制到服务器上,执行 mysql 命令导入。导入完成后,用 Navicat 或命令行验证一下表结构和测试数据是否正常。
后端打包用 Maven,命令是:
mvn clean package -DskipTests打包完成后,在 target 目录下会生成一个 jar 包,用 java -jar 直接启动程序。我建议用 systemd 或 nohup 方式守护进程,避免终端退出后服务挂掉。
nohup java -jar farmstay-server.jar --server.port=8080 > app.log 2>&1 &启动后用 curl 测试接口是否响应:
curl http://127.0.0.1:8080/api/room/list有 JSON 返回基本就说明后端跑通了。
5.3 前端构建与 Nginx 配置
前端构建需要先安装依赖,再执行构建命令:
npm install npm run build构建完成后,项目目录下生成 dist 文件夹,里面就是打包好的静态文件。把 dist 文件夹整体上传到服务器,放到 /usr/share/nginx/html 目录。
Nginx 配置是整个部署环节最关键的一步。前后端分离项目的核心配置是两个 location 块:第一个处理静态页面请求,第二个把 /api 开头的请求反向代理到后端服务。
server { listen 80; server_name your_domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files 这行配置是 Vue Router history 模式的关键。如果没用这行,用户刷新 /rooms 页面会直接 404,因为服务器上根本没有这个文件。try_files 会把所有不存在的路径回退到 index.html,交给前端路由处理。
配置完成后,重启 Nginx,然后访问服务器 IP 或域名。如果页面能打开、接口能通,整个项目就上线成功了。
6. 踩坑实录:前后端分离项目的常见问题
最后这部分内容,是我在实际开发部署中反复遇到的问题和对应的排查方案,也是很多从零写全栈项目的人最容易卡住的环节。
6.1 跨域问题
开发环境跨域用 Vite 代理解决,这个前面说过。但生产环境往往还会遇到一个情况:用户直接在浏览器访问前端域名,接口请求发到后端域名,如果没有代理层,浏览器会拦截跨域响应。我推荐生产环境用 Nginx 反向代理的方式解决,后端不开启 CORS 反而更安全。
后端需要开放 CORS 也可以,但要注意配置允许的来源,别直接写 *,否则任何网站都能调你的接口。
6.2 MyBatis 查询结果映射不上
最常见的 MyBatis 问题是数据库字段是下划线命名(create_time),Java 属性是驼峰命名(createTime),默认情况下映射不上,查询结果里这个字段是 null。
解决方法是在 application.yml 里配置:
mybatis: configuration: map-underscore-to-camel-case: true这个配置加上之后,MyBatis 会自动把下划线字段映射为驼峰属性,一劳永逸。
6.3 前后端时间格式化不一致
Java 8 的时间类型 LocalDateTime,默认序列化成 JSON 时格式是"2025-01-01T12:30:00",中间带个 T,前端显示不友好。我在 Jackson 配置里统一设置了格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai这样所有接口返回的时间字段都统一成"2025-01-01 12:30:00"格式。前端拿到字符串直接展示或者用 dayjs 处理都可以。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 前端接口全部 404 | Nginx 缺少 try_files 或 proxy_pass 路径不对 | 检查 Nginx 两个 location 配置 |
| 接口返回 500,日志显示 MySQL 通信异常 | 连接串缺少时区参数或数据库连接池耗尽 | 加 serverTimezone=Asia/Shanghai,检查连接池配置 |
| 登录成功但请求接口 401 | token 过期或请求头没带 Authorization | 检查 Axios 拦截器,确认 token 传递方式 |
| 中文乱码 | MySQL 字符集不是 utf8mb4 | 建库时指定 CHARACTER SET utf8mb4 |
| 端口被占用 | 上次启动的后端进程没杀掉 | lsof -i:8080 查进程,kill 掉再启动 |
| 前端刷新页面空白 | 路由模式是 history,缺少 try_files 回退 | Nginx 加 try_files $uri $uri/ /index.html |
6.5 前端构建报错与依赖版本处理
现在很多项目初始化时用的依赖版本比较新,新版本对 Node.js 版本有要求。比如 Vite 5 要求 Node.js 18 以上,如果你本地还是 Node.js 16,构建时会出现一堆报错。我的建议是安装 nvm 管理 Node 版本,项目里用 .nvmrc 文件锁定版本号。
还有 Element Plus 的按需引入,如果用 unplugin-vue-components 插件,要注意插件版本的兼容性。我遇到过组件库版本和插件版本不匹配,导致按钮组件样式消失的问题。排查半天,最后是统一升级到最新版本解决的。
后端这块,SpringBoot 2.7 和 3.x 的差异要注意:SpringBoot 3.x 要求 JDK 17 以上,而且 javax 包名改成了 jakarta。如果你照着 SpringBoot 2.x 的教程写代码,直接套到 3.x 项目里,启动时会报 ClassNotFound 之类的错误。我用的是 SpringBoot 2.7 配合 JDK 11,生态成熟,问题少。强烈建议新手先从这个组合入手,别一上来就追新版本。
我在实际开发中还发现,前后端分离项目的联调效率,很大程度上取决于接口文档的规范程度。我习惯先用 Postman 把每个接口的入参、出参、错误码测试通过,再让前端对接。后端每个接口都统一返回 Result 格式,前端封装好拦截器之后,整个对接过程异常顺利。
最后再说一个实用的小技巧:这套系统的订单状态机设计,其实可以直接复用到其他行业项目里。不管是预约服务、课程报名还是租赁管理,核心都是"预订、确认、履约、完成"这套流转。把订单表的字段抽象成通用的(业务类型、业务 ID、时间区间、状态),就能做成一个通用的订单模块,这是我后来做其他系统时反复验证过的思路。
如果你也要从零搭一套类似的管理平台,我最大的建议是:别急着写代码,先把业务流程图和数据表结构画清楚。我在这个项目里前期花了一半时间在数据库设计和状态流转梳理上,后期写代码几乎没怎么返工。这个习惯,比我写代码本身更值钱。