☰
Spring Boot+Vue民宿管理系统毕设开发全攻略:从表设计到部署避坑
2026/9/26 5:20:15 网站建设 项目流程

1. 项目从哪来:先把民宿系统的需求盘清楚

做毕设最容易犯的错,不是技术不会,而是拿到题目就开写。民宿管理系统听起来就是个“增删改查”,但要是真按照酒店管理系统那套来设计,后面会把自己坑死。民宿和酒店最大的区别在于:酒店房间固定、房型标准、价格统一;而民宿是“房东把自家闲置房间挂出来”,每间房的面积、朝向、价格、设施都不一样,甚至同一个房源下不同日期价格还会浮动。所以这套系统的核心逻辑应该是“房源—房间—订单”这条链路,而不是“房型—房价—入住”这种酒店模式。

先盘一下这套系统到底要给谁用。我按最常见的毕设要求拆成三类角色:

  • 游客(未登录用户):浏览首页、按城市/关键词搜索民宿、查看房源详情与评价。权限最小,但页面必须好看,首页就是门面。
  • 注册用户(租客):登录后可收藏房源、提交订单、在线支付(模拟)、对入住过的民宿写评价、管理个人订单。
  • 管理员(房东/平台运营):民宿审核与管理、房态管理(上下架)、订单处理(确认/取消/退房)、用户管理、数据统计。

这套角色划分是毕设评审老师最喜欢看到的“需求分析”部分。你要能在文档里写清楚每个角色有哪些用例,对应的菜单和按钮是什么。别小看这一步,答辩时五分钟讲明白“谁能干什么”,远比你讲十分钟技术细节拿分。

功能模块如果要画功能结构图,大概是这个骨架:前台展示、用户中心、民宿管理、订单中心、评论中心、后台管理。前台走的是“浏览—搜索—详情—下单”路径,后台走的是“上下架—订单处理—数据查看”路径。整个系统的业务闭环就是:用户下单 → 管理员接单确认 → 用户入住 → 离店评价。这个闭环打通了,项目在业务上就是完整的。

1.1 为什么选 Spring Boot + Vue 这套组合

说实话,现在毕设技术选型已经没什么悬念了。Spring Boot + Vue 就是当前Java方向毕业设计的绝对主流,原因很简单:企业里大量中小型项目就是这么做的,你写完这套东西,简历上有东西可写,面试时也能聊。

后端用 Spring Boot 的好处不用我多嘴,自动配置、内嵌Tomcat、生态成熟。前端配 Vue 则是因为组件化开发写页面效率高,Element UI 拖点组件就能把后台管理界面拼出来,不需要懂太多CSS。前后端通过 RESTful API 交互,JSON 传数据,这套模式本身就是Java开发的主流技能栈。

我的建议是:别用 JSP,别用 Thymeleaf,别用 jQuery。你可能在某些教程里见过老项目这么写,但放到2025年的毕设答辩现场,老师会问“为什么不用前后端分离”。这不是说老技术不行,而是你要体现自己学过现代开发模式。前后端分离还能顺便展示你懂跨域、懂接口设计、懂打包部署,这些全是加分项。

2. 环境准备与项目骨架:搭建脚手架

开始敲代码之前,先把环境和工具链准备好。我用的是这么一套,你可以直接照抄:

分类工具/版本说明
JDK1.8 或 11建议 JDK 8,兼容性最稳,很多老代码找得到解决方案
数据库MySQL 5.7 / 8.0两者都行,8.0 要注意驱动和时区配置
后端构建Maven 3.6+管理依赖必备
前端构建Node.js 14+ / npmVue 2 项目建议 Node 14~16,太高会有 node-sass 兼容问题
后端框架Spring Boot 2.7.x别一上来就追 3.x,很多教程和依赖还不兼容
前端框架Vue 2.x + Element UI毕设首选,千万别用Vue 3硬扛,生态和教程都不如Vue 2顺手
ORMMyBatis Plus单表CRUD几乎不用写SQL,省一半时间
鉴权JWT + 拦截器不引入Spring Security,毕设没必要上重武器

这里有个重要的经验要告诉你:毕设项目追求的不是性能多强、架构多复杂,而是快速跑通、能答辩、代码能讲清楚。Spring Security + Redis + 微服务那一套确实高大上,但你要是没有几个月时间打磨,翻车概率极高。JWT + 拦截器实现登录认证,代码量少、原理易懂,答辩时你三句话就能讲明白,老师也挑不出毛病。

2.1 后端项目初始化:用 start.spring.io 还是自己搭

两种方式我都试过。如果你网络环境好,直接用 start.spring.io 生成一个 Spring Boot 项目,勾选 Web、MySQL、Lombok 依赖,下载解压就能用。网络不行的话就手动建 Maven 项目,在 pom.xml 里写依赖,本质没区别。

关键是 pom.xml 里的依赖不要贪多。我的最小依赖清单是:spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、jwt(用 jjwt 或 hutool 的工具类)、hutool-all(可选,用来做验证码、日期处理)。加多了反而麻烦,比如你加了 spring-boot-starter-data-redis 又没配置Redis,项目启动直接报错,白白浪费时间。

application.yml 里几个要特别注意的配置:

server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/min_su?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

后端端口我习惯用 8081,避开前端开发服务器默认的 8080,这样联调时不会搞混。MyBatis Plus 配置里那个 map-underscore-to-camel-case 一定要开,否则数据库的 create_time 映射不到 createTime,查出来全是 null,别问我怎么知道的。

2.2 前端项目初始化:Vue 脚手架的坑和破解方式

前端用官方脚手架vue create或者直接npm install -g @vue/cli建项目。创建完跑起来之前,先看一眼 package.json 里的依赖版本,把 node-sass 换掉。为啥?node-sass 是出了名的编译地狱,Node 版本一高就报错。现在社区方案是换成sass+sass-loader,或者干脆不用 Less/Sass,Element UI 自带的样式已经够用。

我还建议在初始化阶段就把前端项目需要的目录结构建好。别全堆在 views 一个文件夹里,后面找文件找哭你:

src/ ├── api/ # 所有接口请求封装,按模块分文件 ├── assets/ # 静态资源 ├── components/ # 公共组件(轮播图、分页等) ├── router/ # 路由配置 ├── store/ # Vuex(存token、用户信息) ├── views/ │ ├── home/ # 前台页面 │ ├── user/ # 用户中心 │ └── admin/ # 后台管理页面 └── utils/ # axios封装、工具函数

有了这个骨架,你后面写代码就是在填空,思路不会乱。

3. 数据库设计:这表设计能撑起你的答辩

数据库设计是毕设评分的大头。民宿管理系统的表不需要太多,正常情况下 8 到 10 张表就足够完整了。我按开发顺序给你列出来,后面写代码直接照这个来。

核心表就是下面这几张:

用户表 t_user

字段名类型说明
idbigint主键自增
usernamevarchar(50)登录名
passwordvarchar(100)加密存储
nicknamevarchar(50)昵称
phonevarchar(20)手机号
avatarvarchar(255)头像地址
roletinyint0普通用户 1管理员
create_timedatetime注册时间

民宿表 t_homestay

字段名类型说明
idbigint主键
titlevarchar(100)民宿标题
covervarchar(255)封面图
descriptiontext民宿描述
cityvarchar(50)所在城市
addressvarchar(255)详细地址
pricedecimal(10,2)默认价格(起价)
statustinyint0待审核 1已上架 2已下架
owner_idbigint房东/发布者ID
create_timedatetime创建时间

房间表 t_room

字段名类型说明
idbigint主键
homestay_idbigint所属民宿ID
namevarchar(50)房型名称
areaint面积(平方米)
bed_typevarchar(20)床型
max_peopleint可住人数
pricedecimal(10,2)每晚价格

订单表 t_order

字段名类型说明
idbigint主键
order_novarchar(32)订单号(唯一)
user_idbigint下单用户
homestay_idbigint民宿ID
room_idbigint房间ID
check_in_datedate入住日期
check_out_datedate离店日期
daysint入住天数
amountdecimal(10,2)订单金额
statustinyint0待确认 1已确认 2已入住 3已完成 4已取消
create_timedatetime下单时间

评论表 t_comment、收藏表 t_favorite这两个就简单了:评论表关联订单、订单关联用户和民宿,存评分和内容;收藏表就是用户ID和民宿ID的映射,加上创建时间。

3.1 设计表字段时最容易犯的错

几个特别提醒,都是我在实际开发中踩过的坑:

金额字段必须用 decimal,不用 float/double。浮点数算钱会出精度问题,你写代码算总价时可能觉得没事,但万一答辩老师往深了问一句“钱能用浮点数存吗”,你答不上来就尴尬了。用 decimal(10,2) 最稳。

状态字段用 tinyint,表示语义要能对上。我的习惯是 0 永远表示“待处理”或“禁用”,1 表示“正常”或“启用”,然后每个状态在代码里写成常量或枚举,千万不要散落在代码各个角落用魔法数字。比如订单状态你要是写if (status == 2),过两周你自己都看不懂 2 是啥意思。

时间字段统一用 datetime,别混用 timestamp。MySQL 里这俩有区别,但在 Java 里映射到 LocalDateTime 没有任何差别,统一一种写法看着舒服,也避免了我见过的一些同学出现的“时间莫名其妙变了8小时”的灵异事件——那多半是时区配置问题。

逻辑删除必须安排上。MyBatis Plus 里配置那些 logic-delete 字段,删除数据时不真删,只把 deleted 置为 1。这既是企业开发的主流做法,也能让老师觉得你有工程意识。

建表语句自己用 Navicat 或者命令行建就行,不用提交一堆 SQL 文件。但文档里一定要把 E-R 图放上,用 PowerDesigner、Visio、Draw.io 都行,画得清楚一点,这是文档评分的关键材料。

4. 后端核心模块实现:一个接口一个接口打通

4.1 登录注册与 JWT 鉴权:别用Session

民宿系统的登录注册是整个后端的第一块拼图。为什么不用 Session?Session 依赖服务器保存状态,前后端分离以后,前端可能部署在 Nginx,后端在另一个端口,Session 需要额外配置跨域携带 Cookie,麻烦得很。用 JWT 的思路就简单:用户登录成功以后,后端生成一个带用户信息的 token 字符串返回给前端,前端存到本地,之后每次请求在请求头里带上Authorization: Bearer <token>,后端拦截器解析 token,解析成功就放行。

JWT 是个很朴素的实现方案。核心代码半页纸就够:

// 生成token String token = JWT.create() .withClaim("userId", user.getId()) .withClaim("role", user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7)) .sign(Algorithm.HMAC256("your-secret-key")); // 解析token DecodedJWT decoded = JWT.require(Algorithm.HMAC256("your-secret-key")) .build() .verify(token);

我习惯用 cn.hutool 的 JWT 工具类或者 com.auth0 的 java-jwt,两种都行。密钥那串字符串别写太简单,答辩老师可能会问“token 被伪造怎么办”,你就说密钥保存在服务端,客户端无法获取,然后加一句“实际企业项目中会用 RSA 非对称加密或者引入 Spring Security OAuth2 体系”,这一句话就够展示你的知识广度了。

4.2 登录拦截器如何写

有了 JWT 工具,拦截器就很好写。继承 HandlerInterceptor,在 preHandle 里从请求头取 token,校验通过就把用户ID放进 ThreadLocal 或 request 的 attribute 里,方便后面的业务代码取当前用户。这一步很关键,比如下单时要知道“当前操作的人是谁”。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { // 返回401 response.setStatus(401); return false; } try { DecodedJWT jwt = JWT.require(Algorithm.HMAC256("your-secret-key")) .build() .verify(token.substring(7)); request.setAttribute("userId", jwt.getClaim("userId").asLong()); return true; } catch (Exception e) { response.setStatus(401); return false; } } }

写拦截器时有个细节:放行哪些路径,拦截哪些路径,必须配置清楚。我的做法是注册拦截器时排除登录、注册、首页列表、民宿详情、搜索这些路径,剩下的 /api/user/** 和 /api/admin/** 全部拦截。注意,如果前端用 axios 发送请求时带上了 token,而后端没有在拦截其中排除 OPTIONS 请求,就会出现一个经典报错:跨域预检请求(OPTIONS)被拦截器拦下来,前端怎么调接口都报 401。这个坑我至少帮三个人解决过了,你遇到了别慌,在拦截器里加一行if ("OPTIONS".equals(request.getMethod())) { return true; }就好。

4.3 民宿列表的分页与条件查询怎么写

民宿管理系统的核心接口是民宿列表查询。前台要支持按城市筛选、按关键词搜索、按价格区间筛选,还要分页。用 MyBatis Plus 的 LambdaQueryWrapper 写这个查询非常舒服,不需要手写 SQL:

public IPage<HomestayVO> queryHomestayList(int page, int size, String city, String keyword, BigDecimal minPrice, BigDecimal maxPrice) { Page<Homestay> p = new Page<>(page, size); LambdaQueryWrapper<Homestay> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Homestay::getStatus, 1) // 已上架 .eq(StringUtils.isNotBlank(city), Homestay::getCity, city) .and(StringUtils.isNotBlank(keyword), w -> w.like(Homestay::getTitle, keyword) .or().like(Homestay::getAddress, keyword)) .ge(minPrice != null, Homestay::getPrice, minPrice) .le(maxPrice != null, Homestay::getPrice, maxPrice) .orderByDesc(Homestay::getCreateTime); return homestayMapper.selectPage(p, wrapper); }

这段代码里有一个实用技巧值得说:where 条件里用 StringUtils.isNotBlank 的短路效果。当 city 为空时,wrapper 不会拼接这个条件,这样写能省掉一大堆 if。LambdaQueryWrapper 里每一个 eq/ge/le 方法都能接收一个 boolean 作为第一个参数,这是 MyBatis Plus 最常用的特性,答辩时如果被问“你怎么处理动态SQL”,直接拿这个作为例子回答,比“我用了XML里的 if 标签”更能体现你用的是现代化开发方式。

4.4 订单状态机:别把判断写成一坨

订单是整个系统业务最复杂的模块,因为订单状态在流转。我设计的状态有:0待确认、1已确认、2已入住、3已完成、4已取消、5已退款。每个动作对应一次状态变更:

  • 用户提交订单 → 状态0
  • 管理员确认 → 状态1
  • 用户入住(管理员操作) → 状态2
  • 离店 → 状态3
  • 用户取消/管理员拒绝 → 状态4

这里我强烈建议你把状态流转抽成一个方法,而不是散落在 Service 里到处直接 update:

public boolean updateOrderStatus(Long orderId, int targetStatus) { Order order = orderMapper.selectById(orderId); // 校验当前状态是否允许变更到目标状态 boolean allowed = checkTransition(order.getStatus(), targetStatus); if (!allowed) { throw new BusinessException("非法状态流转"); } order.setStatus(targetStatus); return orderMapper.updateById(order) > 0; }

为什么要这么做?因为你要是放手让每段业务代码直接改状态,写“取消”的代码时可能没校验订单是不是已经“完成”了,被老师直接指出“逻辑漏洞”是很尴尬的。做一个状态流转校验表,代码干净,答辩时还能理直气壮地说“我做了状态机校验,非法流转会被拦截”。

这个状态机在代码里的实现方式也很简单,我直接用了一个 Map 存合法流转关系,或者写一个 switch 方法判断也行。关键是你在文档里把状态流转图画出来,这种细节就是高分和及格分的差距。

5. 前端页面开发:把接口接到页面上

前端开发的核心工作分三大块:路由、接口封装、页面组件。很多同学卡在这一步,不是不会写 Vue,而是前后端对接的细节没处理好。

5.1 axios 封装:统一处理 token 和错误码

前端页面所有请求都通过 axios 发出去,如果每个页面都写一遍axios.get(url).then(...),代码重复率太高,而且 token 怎么带、401 怎么处理都没有统一方案。所以最优先要做的,就是封装一个 request.js:

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => response.data, error => { if (error.response) { if (error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } else { Message.error(error.response.data.message || '请求失败') } } return Promise.reject(error) } ) export default request

这段封装的巧妙之处在于:业务代码里完全不用关心 token 和错误处理,只管拿数据。而且 401 时自动跳转登录页,这个交互体验在答辩演示时非常加分。老师登录后台时如果发现 session 过期了,页面自动跳到登录页,他自然能感受到你做了“登录状态管理”,而不是把 token 取出来随手一放。

5.2 路由守卫:用户没登录就进不了后台

Vue Router 的路由守卫配合 axios 拦截器,才是完整的一套前端鉴权方案。光有后端拦截器不够,因为前端还是有路由,你总不能让用户手动输入 URL 也能进后台页面吧。在前端把没有权限的页面挡在渲染前,体验会好很多。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.path.startsWith('/admin')) { if (!token) { next('/login') } else if (role !== 'admin') { next('/') } else { next() } } else if (to.path === '/login' && token) { next('/') } else { next() } })

注意:前端路由守卫只是锦上添花,真正的安全屏障仍然是后端拦截器。前端跳转可以被绕过,但后端接口的校验绕不过去。这一点写在文档的安全设计里,老师的提问基本就在这个范围里:跨域怎么处理、token 存哪里、接口如何防刷。每个问题你都能给出答案,答辩基本不会卡壳。

5.3 后台管理界面的表格页:Element UI 的白嫖手册

后台管理无外乎就是表格 + 表单 + 弹窗。用 Element UI 的 el-table、el-dialog、el-form 组件,半个小时就能拼出一个完整的管理页面。我的经验是:先写列表页,再写新增/编辑弹窗,最后处理删除和状态切换。

常见的后台页面结构长这样:

<template> <div> <el-card> <el-form inline> <el-input v-model="query.keyword" placeholder="搜索民宿名称" /> <el-button type="primary" @click="loadData">搜索</el-button> </el-form> <el-table :data="tableData" border stripe> <el-table-column prop="title" label="民宿名称" /> <el-table-column prop="city" label="城市" width="120" /> <el-table-column prop="price" label="价格" width="120"> <template #default="{ row }"> <span>¥{{ row.price }}</span> </template> </el-table-column> <el-table-column label="状态" width="120"> <template #default="{ row }"> <el-tag :type="row.status === 1 ? 'success' : 'info'"> {{ row.status === 1 ? '已上架' : '已下架' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="220"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="query.page" :page-size="query.size" :total="total" @current-change="loadData" /> </el-card> </div> </template>

几个容易出错的地方要提醒你。el-pagination 的 current-page 绑定的一定要和你后端要的 page 参数一致,size 同理,否则会出现“点击第二页结果还是第一页”的诡异问题。还有就是 modal 弹窗关闭以后,表单数据要重置,否则下次打开会残留上次的数据。这些细节不难,但直接影响演示效果。

6. 前后端联调与部署:从“页面能动”到“系统完整”

6.1 跨域问题:几分钟解决,却挡了八成新手

前后端分离开发时,前端跑在 8080,后端跑在 8081,浏览器就会遇到跨域问题。解决方式就两个:后端加 CORS 配置,或者前端用代理转发。

后端方式更简单,写一个配置类就行:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意 allowedOriginPatterns 而不是 allowedOrigins,后者在 allowCredentials(true) 时会因为浏览器安全策略失效,尤其是 Spring Boot 2.4+ 版本后这个改动很隐晦,报错信息还不直观。你如果遇到“请求能发出去,但浏览器报 CORS error”,大概率就是这里写错了。

如果用的 Vue 开发服务器,还有一种最省事的方案:在 vue.config.js 里配 devServer 的 proxy:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }

前端代码里所有接口路径都以 /api 开头,开发时就完全感受不到跨域的存在。到了部署阶段,Nginx 反代也可以做同样的事情。不过我还是建议两种都写进文档里,毕竟老师喜欢问“跨域问题你怎么解决”,你能说出两种方案,说明你不是只会复制粘贴。

6.2 打包与部署:毕设演示前的最后一步

前端打包执行npm run build,生成 dist 目录;后端打包执行mvn clean package,生成 jar 包。然后有两种部署方式:

第一种是用 Nginx 托管前端静态文件,并反向代理后端接口。这种方式最接近企业真实部署,也能在系统中加入“部署文档”这一章节,显得很专业。Nginx 配置大概长这样:

server { listen 80; server_name your-domain.com; root /usr/share/nginx/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

第二种更省事:把前端打包好的静态资源放到后端项目的 src/main/resources/static 目录下,直接用 Spring Boot 一起启动。这样只需要一条java -jar xx.jar命令就能跑起整个系统,演示时最不容易出岔子。

我个人建议你复习前把两种方案都试一遍。答辩现场可能会出现各种幺蛾子:老师电脑上没装 Node、数据库连接失败、端口被占用。你要是多掌握一种启动方式,现场就能多一条后路。我在帮别人调试项目时最常说的一句话就是:“先把项目跑起来,其他的都好说。”

6.3 数据库初始化:让项目在任何机器上都能跑起来

很多人把数据库文件放在一个 .sql 文件里就没管了,这是不对的。顺手做两件事,能让你的项目在答辩现场顺畅很多:

第一,写一个数据库初始化脚本,包含建库语句、建表语句、几条测试数据。最好和项目一起放进 README,或者干脆在 Spring Boot 启动时用 schema.sql 自动执行。这样即使换了电脑,只要装好 MySQL,执行一遍脚本就可以启动项目,不用一个个表去建。

第二,测试数据一定要有足够量。民宿列表至少放 10 条,订单至少放 5 条,评论至少放几条,否则页面上分页插件、空状态、列表展示的效果都看不出来。你写代码时用的是“有一百条数据”的思路来测,但答辩现场可能因为数据太少,一些 UI 效果没展示出来,老师反而觉得你没做完。

7. 常见的坑与排查技巧:我自己踩过的,你直接拿走

做这种前后端分离项目,后端启动报错、前端编译报错、接口 404、数据查不出来是家常便饭。我把最高频的问题列成速查表,你遇到直接对号入座:

现象可能原因解决方案
Spring Boot 启动报 driver class not foundMySQL 驱动依赖缺失或版本不匹配pom.xml 里检查 mysql-connector-java 依赖
前端 npm run serve 报 node-sass 错误Node 版本和 node-sass 不兼容换成 sass 包,或使用 Node 14
请求接口返回 404路径写错、Controller 没扫描到检查 @RequestMapping 路径,看启动类包结构
请求接口返回 401没带 token、token 过期、拦截器问题查看网络请求的 Authorization 头
数据库中文乱码连接 URL 没加 characterEncodingURL 加 useUnicode=true&characterEncoding=utf8
查询结果为 null 但数据库有值驼峰映射没开启检查 map-underscore-to-camel-case 配置
时间查询相差 8 小时MySQL 时区问题serverTimezone=Asia/Shanghai 写进 URL
前端跨域报错CORS 配置不正确检查 allowedOriginPatterns 和拦截器放行 OPTIONS
刷新页面 404前端路由 history 模式部署问题Nginx 配置 try_files 指向 index.html
MyBatis Plus 分页失效缺少分页插件配置在配置类里添加 PaginationInnerInterceptor

讲几个相对隐蔽的坑:

第一个坑,MyBatis Plus 分页插件没配置。很多同学只加了 mybatis-plus 依赖就开始写selectPage,结果发现查出来的数据是全部而不是一页。原因在于 MP 分页需要注册 PaginationInnerInterceptor,否则 selectPage 只是一个普通查询。这个坑几乎所有人都踩过,我甚至见过有人因为这个问题,自己手动去写 count 查询分页,绕了个大圈。

第二个坑,Lombok 在 IDEA 中没装插件。pom.xml 里加了 lombok 依赖,代码也写了 @Data,结果编译报错找不到 getter/setter。方案就是先去装插件,装完别忘了重启 IDEA。另外注意 JDK 版本高的话,lombok 要用新版,我用 JDK 11 时就遇到过旧版 lombok 在编译时直接报错的情况。

第三个坑,前端所有接口被 404 拦截。Vue 路由用 history 模式时,开发环境没问题,部署到 Nginx 上一刷新就 404。原因是 Nginx 找不到对应的静态文件路径,需要在配置中加 try_files 把请求回退到 index.html。这问题每年答辩季都能见到好几次,提前配置好就不会紧张。

第四个坑,日期格式化。订单列表页面上显示的时间是一串数字,或者显示"2025-01-01T10:00:00"这种带 T 的格式。解决方案是在 LocalDateTime 字段上加上 @JsonFormat 注解,或者用全局配置统一格式化。这种细节属于那种“不致命但很减分”的问题,演示时老师看到你页面上显示的时间格式混乱,多少会觉得粗糙。

7.1 关于“文档+代码讲解+一条龙定制”的一些提醒

最后聊点实在的。很多同学拿到的是“源码+文档+一条龙定制”的套餐,但其实最终答辩时最核心的还是“你能不能用嘴讲清楚”。

文档部分,毕设论文里最不能省略的几块是:开题背景(为什么做民宿系统)、国内外研究现状(网上找相关文献引用几篇)、需求分析(角色/用例图/功能图)、系统设计(架构图、数据库E-R图、表结构)、系统实现(核心功能界面截图+代码说明)、系统测试(测试用例和结果)。这几块凑齐,论文框架就稳了。

代码讲解环节,你要能答上来几个基础问题:项目的技术栈是什么?数据库几张表、分别什么作用?订单状态怎么流转?权限怎么控制的?有些同学代码是花钱定制的,自认为稳了,结果被老师随口一问就露馅。这里我给个建议:拿到任何代码,先自己跑通,再把核心 Controller 的每个接口走读一遍,知道“前端点了哪个按钮,调了哪个接口,后端做了什么”,能讲到这个程度,哪怕代码是别人写的,答辩也没问题。

调试经验也是。我遇到过一个同学,本地跑得好好的,答辩现场老师用他的电脑连不上数据库,结果发现是电脑装了 MySQL 8,代码里驱动还是 MySQL 5 的。这种环境相关的问题,提前把环境清单写进 README,现场就不会手忙脚乱。

7.2 前端演示时最容易被问到的点

演示环节里,老师会随机点一些页面看看有没有 bug。根据经验,重点检查的地方通常是:

搜索功能能不能用,搜索完分页是否归零。很多同学把搜索和分页分开写,导致点第二页时搜索条件丢失。解法是搜索时把当前页码重置为1,并确保分页组件绑定同一个查询对象。

订单日期跨天计算是否准确。系统里入住天数要按日期差计算,别傻傻地用 24 小时换算天数,否则遇到跨夜订单就会差一天。用 ChronoUnit.DAYS.between(checkInDate, checkOutDate) 才是对的。

重复提交问题。前端下单按钮如果没做防重复提交,快速点两下就生成两条相同订单,演示时被发现会非常难看。简单方案是按钮点击后加 loading 状态,或者后端按订单号做唯一约束,双保险最稳。

图片上传问题。如果民宿管理的图片用了本地存储路径,换个电脑路径就失效了。我建议直接把图片路径存数据库,文件保存在后端的 upload 目录下,同时配置虚拟目录映射,这样打 jar 包路径也不会因为路径写死而出问题。

结尾

老实说,民宿管理系统这个课题在毕设里算不上多新颖,但它胜在业务脉络清晰、功能边界明确、技术栈主流。正因如此,你才有机会把每个模块都做得完整、讲得明白。我做了这么多年项目,始终觉得毕设做得好不好,不在于用了多少新技术,而在于“你做出来的东西能不能自圆其说、跑得起来、答得上问”。如果你正在做这个课题,我的建议是:先把数据库表和状态流转图定死,再写后端接口,再接前端页面,不急不躁,一周出活儿是完全可能的。

最后分享一个小习惯:做完一个模块就本地跑一遍,把当时的报错和解决方式记到文档里,这些资料就是你写论文“系统测试”章节的素材。别把所有问题都堆到最后一天再调,那样你会连着通宵好几天,别问我怎么知道的。祝你看完这篇文章后,该踩的坑少踩几个,一次答辩顺利通过。

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

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

立即咨询