☰
SpringBoot+Vue民宿管理系统前后端分离实战:从零搭建到答辩加分
2026/10/8 10:30:04 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与开发者的Java毕业设计完整项目,采用SpringBoot与Vue前后端分离架构实现民宿管理系统,适合作为毕业设计、课程设计、作业或项目立项演示的参考方案,也便于基础较好的学习者在此基础上二次开发扩展功能。资源包共196个文件,约4.51MB,其中138个Java源文件构成后端核心业务逻辑,18个XML与1个YML、1个properties负责框架与项目配置,31张JPG为界面或数据配图,另含Maven包装器、jar依赖及说明文档,结构完整、层次清晰。项目已通过Mac与Windows 10/11环境测试运行,功能正常,并获导师指导认可,答辩评审达95分。目前已有379人学习关注。读者可从中获取完整源码、使用文档与配套资料,涵盖用户、房源信息、商家、聊天、房源退订等模块,便于快速理解前后端分离开发流程、掌握接口设计与数据库交互思路,也可直接用于毕设或课设提交。

1. 民宿管理系统前后端分离:一套能跑通的 SpringBoot + Vue 工程长什么样

很多同学做 Java 毕业设计时,卡住的地方往往不是业务逻辑本身,而是「前后端怎么接起来」。民宿管理系统这个题目尤其典型:房源、订单、入住人、评价、后台审核,模块一多,接口一乱,前端调不通,答辩时演示直接翻车。这套基于 SpringBoot + Vue 前后端分离的民宿管理系统,解决的正是「从零搭一套能演示、能讲清架构、能二次开发」的问题。它适合正在做毕设的 Java 方向学生,也适合想补一次完整前后端分离项目实战的初级工程师。核心链路是:Vue 负责页面渲染和路由跳转,SpringBoot 提供 REST 接口,MyBatis-Plus 操作数据库,JWT 做登录态,前后端通过 JSON 通信。搞清这条链路,你就能把「民宿管理系统」从一份源码变成自己讲得明白的项目。

2. 技术选型与工程结构:为什么是 SpringBoot + Vue 而不是别的组合

2.1 后端为什么选 SpringBoot 而不是 SSM 手写配置

毕业设计里常见的另一条路是 SSM(Spring + SpringMVC + MyBatis)手动配 XML。这套民宿系统选 SpringBoot,理由很实际:自动配置把数据源、事务、JSON 序列化这些重复劳动省掉了,你只需要在application.yml里写连接信息,剩下的交给 starter。对毕设来说,时间要花在业务和答辩上,不是花在web.xml和spring-mvc.xml的标签里。

SpringBoot 的版本选择有个血泪经验:别追最新。热词里「springboot版本太高」是真实痛点,高版本对 JDK 要求高,某些依赖还没跟上,容易在启动阶段就报NoSuchMethodError。稳妥做法是选 2.7.x 这类长期维护、生态成熟的版本,JDK 用 8 或 11,和大多数教程、依赖兼容。

后端分层遵循经典结构,这也是答辩时老师最爱问的:

src/main/java/com/homestay ├── controller // 接收请求,参数校验,返回统一结果 ├── service // 业务逻辑,事务边界 │ └── impl ├── mapper // MyBatis-Plus 接口,继承 BaseMapper ├── entity // 数据库实体,和表一一对应 ├── dto / vo // 入参出参对象,避免实体直接暴露 ├── config // 跨域、拦截器、MyBatis-Plus 配置 └── common // 统一返回体、异常处理、JWT 工具

controller只做参数接收和结果包装,业务判断放service,数据库操作靠mapper。这样分层的好处是:老师问「你的业务逻辑写在哪」,你能明确指到 service,而不是一锅粥全塞在 controller 里。

2.2 前端为什么用 Vue 做前后端分离

前后端分离的核心价值是职责清晰:后端只吐 JSON,前端只管渲染。Vue 的组件化和路由机制天然适合这种模式。民宿系统里,房源列表、房源详情、订单管理、后台审核是不同页面,用 Vue Router 做动态路由,用 axios 发请求,页面之间跳转不刷新,体验接近真实产品。

前端目录大致这样组织:

src ├── api // 按模块封装 axios 请求 ├── router // 路由表,含登录守卫 ├── store // Vuex/Pinia 存用户信息和 token ├── views // 页面级组件 ├── components // 可复用组件,如房源卡片 └── utils // request.js 封装 axios 拦截器

utils/request.js是前后端衔接的关键,统一加 token、统一处理 401。很多同学前端调不通接口,问题就出在这里没配好。

2.3 数据库表怎么设计才撑得起业务

民宿系统的核心表不多,但关系要理清。下面是最小可用的一组:

表名作用关键字段
user用户(游客/房东/管理员)id, username, password, role
house房源id, title, price, address, cover, status
order订单id, user_id, house_id, check_in, check_out, total
comment评价id, order_id, content, score
favorite收藏id, user_id, house_id

order表用user_id和house_id做外键关联,status字段控制订单状态流转(待支付、已支付、已入住、已完成、已取消)。角色用role字段区分,登录后前端根据角色渲染不同菜单。这套设计不复杂,但足够支撑演示和答辩提问。

3. 后端接口落地:从登录鉴权到房源订单的完整实现

3.1 用 JWT 做登录态,别再往 session 里塞

前后端分离后,session 那套不好使了,因为前端可能是独立域名或端口。常见做法是 JWT:登录成功后后端签发 token,前端存起来,之后每次请求放在请求头里。

// JwtUtil.java 关键方法 public static String createToken(Long userId, String role) { // 过期时间设 7 天,毕设演示够用 Date expire = new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L); return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(expire) .signWith(SignatureAlgorithm.HS256, SECRET) // SECRET 放配置里,别硬编码 .compact(); } public static Claims parseToken(String token) { // 解析失败会抛异常,交给全局异常处理 return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); }

逻辑说明:createToken把用户 id 和角色写进 payload,用 HS256 签名,防止前端篡改角色。parseToken在拦截器里调用,解析成功就把用户信息放进ThreadLocal,后续 service 直接取。参数上,SECRET一定要放application.yml而不是写死在代码里,答辩时被问「密钥泄露怎么办」你能答上。过期时间别设太短,否则演示到一半掉线,也别太长,7 天是折中。

拦截器注册在config里,放行登录、注册、房源列表这些公开接口,其余一律校验 token。

3.2 房源和订单接口:MyBatis-Plus 省掉一半 CRUD

MyBatis-Plus 的BaseMapper自带增删改查,房源这种标准 CRUD 几乎不用写 SQL。

// HouseController.java @RestController @RequestMapping("/api/house") public class HouseController { @Autowired private HouseService houseService; // 分页查询房源,支持按标题模糊、按价格区间 @GetMapping("/page") public Result<IPage<House>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) BigDecimal minPrice) { LambdaQueryWrapper<House> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), House::getTitle, keyword) .ge(minPrice != null, House::getPrice, minPrice) .eq(House::getStatus, 1) // 只查已上架 .orderByDesc(House::getCreateTime); return Result.success(houseService.page(new Page<>(pageNum, pageSize), wrapper)); } }

逻辑说明:LambdaQueryWrapper用方法引用代替字符串字段名,改字段时编译期就能发现错误,比手写"title"稳。like、ge、eq的第一个参数是条件开关,条件不成立时该片段不拼进 SQL,这样一套代码同时支持「带关键词」和「不带关键词」两种查询。status=1保证前台只看到已上架房源,后台审核逻辑单独走另一个接口。

订单接口比房源多一层业务校验:下单前要判断房源是否存在、是否已被预订、入住日期是否合法。这些判断放 service,用@Transactional保证「扣库存 + 生成订单」要么都成功要么都回滚。

@Transactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto, Long userId) { House house = houseMapper.selectById(dto.getHouseId()); if (house == null || house.getStatus() != 1) { throw new BizException("房源不存在或已下架"); } // 日期合法性:入住必须早于离店 if (!dto.getCheckIn().isBefore(dto.getCheckOut())) { throw new BizException("入住日期必须早于离店日期"); } Order order = new Order(); order.setUserId(userId); order.setHouseId(dto.getHouseId()); order.setCheckIn(dto.getCheckIn()); order.setCheckOut(dto.getCheckOut()); // 总价 = 单价 × 天数,天数用 ChronoUnit 算 long days = ChronoUnit.DAYS.between(dto.getCheckIn(), dto.getCheckOut()); order.setTotal(house.getPrice().multiply(BigDecimal.valueOf(days))); order.setStatus(0); // 待支付 orderMapper.insert(order); }

参数说明:rollbackFor = Exception.class保证任何异常都回滚,默认只回滚运行时异常,受检异常不回滚,这是常见坑。日期用LocalDate而不是Date,ChronoUnit.DAYS.between算天数比手动减时间戳清晰。总价用BigDecimal避免浮点误差,金额字段千万别用double。

3.3 统一返回体和全局异常处理

前端要的是稳定结构,所以后端所有接口返回统一格式:

@Data public class Result<T> { private Integer code; // 200 成功,其他失败 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("操作成功"); r.setData(data); return r; } }

配合@RestControllerAdvice捕获BizException和系统异常,统一转成Result。这样前端拦截器只需判断code是否为 200,不用每个接口写一套错误处理。答辩时老师问「接口报错前端怎么知道」,这套机制就是答案。

4. 前端对接与联调:Vue 页面怎么把接口串起来

4.1 axios 封装与请求拦截器

前端所有请求走utils/request.js,统一加 baseURL、token 和错误提示。

// utils/request.js import axios from 'axios' import { getToken, removeToken } from '@/utils/auth' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 开发环境指向后端 8080 timeout: 10000 }) // 请求拦截:带上 token service.interceptors.request.use(config => { const token = getToken() if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截:统一处理 code 和 401 service.interceptors.response.use( response => { const res = response.data 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) { removeToken() router.push('/login') // token 失效回登录页 } return Promise.reject(error) } ) export default service

逻辑说明:请求拦截器从本地取 token 塞进请求头,和后端拦截器对应。响应拦截器判断业务code,非 200 弹提示并 reject,页面里就不用每个请求都写错误处理。401 单独处理,清 token 跳登录,避免用户卡在空白页。baseURL用环境变量,开发时指向http://localhost:8080,打包时改成后端地址,这样一套代码两种环境都能用。

4.2 房源列表页:分页、搜索、路由跳转

房源列表是前台核心页面,涉及分页查询和详情跳转。

// views/house/list.vue 的 script 部分 export default { data() { return { list: [], total: 0, query: { pageNum: 1, pageSize: 8, keyword: '' } } }, created() { this.fetchList() }, methods: { async fetchList() { const res = await pageHouse(this.query) // api 里封装好的请求 this.list = res.data.records this.total = res.data.total }, handleSearch() { this.query.pageNum = 1 // 搜索时回到第一页 this.fetchList() }, goDetail(id) { this.$router.push(`/house/${id}`) // 动态路由跳详情 } } }

逻辑说明:created里首次拉数据,handleSearch重置页码再查,避免搜完停在空页。goDetail用动态路由传 id,详情页通过this.$route.params.id取到再请求详情接口。分页参数pageNum、pageSize和后端Page对象对应,字段名要一致,否则分页失效。

4.3 登录守卫与角色菜单

后台管理页要限制未登录访问,用路由守卫:

// router/index.js router.beforeEach((to, from, next) => { const token = getToken() if (to.meta.requireAuth && !token) { next('/login') // 需要登录但没 token,踢回登录 } else { next() } })

在路由表里给后台页面加meta: { requireAuth: true }。角色菜单则根据登录后存的role字段动态渲染,管理员看到审核入口,普通用户看不到。这样前后端权限就闭环了:前端控制显示,后端拦截器控制接口,双保险。

5. 避坑与排查:这套民宿系统最容易翻车的 5 个地方

5.1 跨域报错 CORS,前端请求全红

现象:前端调接口浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy,Network 里请求状态是 failed。

原因:前端跑在 8081,后端在 8080,端口不同就是跨域,浏览器默认拦截。

解决:后端加全局跨域配置,别在每个 controller 上贴@CrossOrigin。

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") // 用 patterns 而非 origins,兼容带凭证 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }

注意allowCredentials(true)时不能用allowedOrigins("*"),必须用allowedOriginPatterns,否则启动就报错,这是高频坑。

5.2 token 传了但后端说未登录

现象:前端明明带了Authorization头,后端拦截器还是返回 401。

原因:多半是请求头名字对不上,或者拦截器里取头的 key 写错。前端写Authorization,后端却取token,自然取不到。

解决:前后端约定统一用Authorization,后端request.getHeader("Authorization")取值。另外注意有些前端封装会把 token 拼成Bearer xxx,后端解析时要先去掉前缀,两边约定好格式,别一边拼一边不拼。

5.3 日期格式前后端对不上,订单提交失败

现象:前端传2024-05-01,后端接收报JSON parse error或日期变成前一天。

原因:LocalDate默认序列化格式和前端传的不一致,或者时区问题导致日期偏移。

解决:实体日期字段加注解统一格式。

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private LocalDate checkIn;

前端也用yyyy-MM-dd字符串传,别传时间戳。时区一定写GMT+8,否则服务器时区不同会差一天,订单日期错一天是致命 bug。

5.4 打包后前端页面 404,刷新就白屏

现象:开发时正常,npm run build后丢进后端或 Nginx,首页能开,一刷新或直接访问子路由就 404。

原因:Vue 是单页应用,路由是前端控制的,服务器找不到/house/1这个真实路径。

解决:Nginx 配置try_files回退到index.html。

location / { try_files $uri $uri/ /index.html; }

如果前端打包放进 SpringBoot 的static目录,也要配一个转发,把非接口请求都指向index.html。热词里「vue打包放进springboot中」说的就是这个场景,配好回退,刷新才不白屏。

5.5 数据库字段和实体对不上,查询返回 null

现象:接口不报错,但返回的房源标题、价格全是 null。

原因:MyBatis-Plus 默认开启驼峰转换,数据库create_time对应实体createTime。如果数据库用了createTime这种驼峰列名,或者实体字段名和列名完全不一致又没加@TableField,就映射不上。

解决:数据库列名统一用下划线(create_time),实体用驼峰(createTime),保持默认转换。特殊情况加注解:

@TableField("house_title") private String title;

排查时打开 MyBatis 的 SQL 日志,看实际执行的 SQL 和返回结果,一眼就能定位是映射问题还是数据问题。

6. 让项目从「能跑」到「能讲」:二次开发与答辩加分技巧

把系统跑通只是及格线,答辩想拿高分,得让项目有「你自己的东西」。这里说几个我实际带学生时验证过有效的方向。

第一,给房源加一个基于简单权重的推荐排序。不用上复杂算法,按「浏览量 × 0.4 + 收藏数 × 0.3 + 订单数 × 0.3」算个分,在列表接口里加个orderBy就行。答辩时你能说清权重怎么来的、为什么这么设,比单纯 CRUD 有说服力。

// 在房源分页查询里追加推荐排序 wrapper.orderByDesc(House::getViewCount) .orderByDesc(House::getFavoriteCount);

第二,把订单状态流转画成状态机,用枚举管理,而不是散落的 if-else。

public enum OrderStatus { PENDING(0, "待支付"), PAID(1, "已支付"), CHECKED_IN(2, "已入住"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; // 构造和 getter 省略 }

状态流转时校验「当前状态能否转到目标状态」,非法流转直接抛异常。老师问「订单能不能从已完成退回待支付」,你能答「不能,状态机限制了」,这就是加分点。

第三,接口文档用 Knife4j 或 Swagger 自动生成,答辩演示时直接打开文档页调接口,比 Postman 截图专业。加依赖、加注解,十分钟的事。

加分项投入答辩价值
推荐排序低体现业务思考
状态机中体现设计能力
接口文档低演示更专业
操作日志中体现工程规范

第四,加一个简单的操作日志,用 AOP 切面记录谁在什么时候改了什么。不用存太多,记关键操作即可。这个点能引出「AOP」「切面」「日志表设计」一串问题,你提前准备好,就是主动引导答辩节奏。

最后说个习惯:每改一个功能,就在本地把「登录 → 浏览房源 → 下单 → 后台审核」这条主链路走一遍。我见过太多项目,单个接口测着没问题,一连起来就断在某个状态没更新。主链路能稳定跑通,比堆十个花哨功能都管用。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询