SpringBoot3+Vue3酒店管理系统开发指南:毕设从骨架搭建到答辩避坑
简介这套酒店管理系统是一份面向毕业设计与课程设计的完整实战源码采用 SpringBoot3 与 Vue.js3 搭建前后端分离架构后端借助 SpringBoot3 简化配置、快速构建 REST 服务前端通过 Vue.js3 实现组件化页面与响应式交互配合 MySQL8 保证数据一致性与事务处理。系统覆盖房间预订、客户管理、订单管理、房价设定、员工管理等典型业务用户前台负责预订与支付管理后台完成订单维护和参数设置完整呈现从需求分析、系统设计到编码实现、测试部署的开发链路。资源包共 7 个文件整体约为 77.48MB包含项目源码压缩包、数据库 SQL 脚本、开题报告与任务书文档以及操作录屏 mp4。目前已有 69 人学习下载。其中文档与录屏尤其适合边看边操作既能为毕业设计的开题、答辩提供材料支撑也能帮助初学者按步骤搭建后台管理和用户前台在此项目基础上进行功能扩展与二次开发。1. 酒店管理系统选 SpringBoot3Vue.js3给 2025 届毕设的一条现实路线这几天陆续有学弟学妹来问同一个问题酒店管理系统做毕业设计用 SpringBoot3 和 Vue.js3 到底行不行我的回答一直很直接——行而且这是目前性价比最高的一条路线。酒店管理系统表面看是典型的增删改查但把房间预订、入住退房、账单结算这些环节串起来之后并发控制、权限校验、跨域联调、日志排查全都会轮到难度刚好卡在能讲出深度、又不至于做不完的位置。选 SpringBoot3 而不是 SpringBoot2是因为 2025 年这个时间点JDK17 SpringBoot3 已经成了多数新项目的默认组合网上那些基于 SpringBoot2 Vue2 的旧教程大多不能直接照抄照着做反而容易在包名和依赖上卡住。Vue.js3 配合 Vite 开发效率比 Vue2 Vue CLI 高出一截组合式 API 写业务也顺手许多。这套技术栈拿去做别的管理系统题目一样能复用不算白学。这篇笔记会沿着一条可实现的主线拆开前后端骨架怎么搭、数据库怎么设计、后端核心接口怎么写、Vue3 前端怎么接、哪些坑每年都有一批人踩以及答辩前用什么方法把系统验到敢上台。适合已经选了酒店管理系统这个题、准备真的把项目跑起来再去答辩的同学也适合想快速评估这个题目工作量的朋友。2. 搭前后端骨架SpringBoot3 项目初始化与数据库设计2.1 技术栈选型JDK17、SpringBoot3 与 Vue3 的版本配套先说版本配套这是最容易翻车的起点。SpringBoot3 要求 JDK17 起所以不要一上来就照抄 SpringBoot2 教程里的 Java8 配置。以我手头常用的组合为例后端 JDK17 SpringBoot 3.2.x MyBatis-Plus 3.5.5 及以上 MySQL 8.x前端 Node.js 18 以上 Vite Vue3 Element Plus Pinia Axios。这个组合在 2025 年做毕设是够得着、讲得清的。为什么要强调3.5.5 及以上的 MyBatis-Plus因为 SpringBoot3 把包名前缀从 javax 换成了 jakarta旧版 MyBatis-Plus 内部依赖的 mybatis-spring 还是老坐标直接引进来会在启动阶段报 NoClassDefFoundError。这类版本问题在答辩前两周集中爆发基本都是当初图省事复制了旧依赖导致的。新旧技术栈的差异可以用下面这张对比表理清答辩被问到为什么不用旧版时也方便递话对比项SpringBoot2 旧链路SpringBoot3 这套影响JDK 要求Java 8 即可最低 Java 17环境不对则编译直接失败Servlet 包名javax.servletjakarta.servlet旧代码 import 全部报红依赖管理Spring Boot 2.xSpring Boot 3.x版本号不能混用MyBatis-Plus 坐标mybatis-plus-boot-startermybatis-plus-spring-boot3-starter引错坐标启动报错前端构建Vue CLI / WebpackVite配置方式完全不同另一个常被忽略的选择是 MySQL 8。虽然 MariaDB、PostgreSQL 也能跑但绝大多数毕业设计相关的教程、同学的互助、老师的常规提问都基于 MySQL遇到问题时搜到答案的概率高得多。数据库引擎和字符集用默认即可记得把默认字符集设成 utf8mb4防止录入客人姓名或备注时遇到 emoji 直接报 Incorrect string value。2.2 后端骨架pom.xml 关键依赖与 application.yml 参数说明创建项目我习惯直接在 IDEA 里用 Spring Initializr勾选 Spring Web 和 LombokJava 版本选 17然后手动补依赖。最小可跑的 pom.xml 里核心依赖是这样的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies !-- Web 依赖SpringBoot3 内置 MVC 与 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 的 SpringBoot3 专用坐标注意不是 mybatis-plus-boot-starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency !-- MySQL 8 驱动SpringBoot3 自动管理版本 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- JWT 用 jjwt 0.12 系列API 与 0.9/0.11 差别很大 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.12.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.12.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里把三个 jjwt 依赖都写上是因为 jjwt 从 0.12 开始把 API、实现和 JSON 序列化拆成了三个模块漏掉 jjwt-jackson 会在解析 token 时报 Jackson 相关的类找不到。MyBatis-Plus 用 3.5.5 这个下限值我实际用过没问题如果你新建项目时已经有了更新的小版本直接往上走也行。接着是 application.yml。数据源相关的几个参数一定要按自己的环境改我一般这样写server: port: 8080 spring: application: name: hotel-management datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的数据库密码 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpldriver-class-name 必须写成 com.mysql.cj.jdbc.Driver旧教程里的 com.mysql.jdbc.Driver 在 MySQL 8 驱动里已经移除了。连接串里的 serverTimezoneAsia/Shanghai 是止血用的不加它LocalDateTime 字段存进数据库常常差 8 小时后面避坑章节再展开。mybatis-plus 的 map-underscore-to-camel-case 建议打开这样数据库里的 room_type_name 能自动映射到实体类的 roomTypeName少写一堆 TableField。log-impl 配成 StdOutImpl 是为了开发期在控制台直接看到 SQL项目快答辩时可以去掉因为日志量有点吵。2.3 数据库设计酒店管理系统从房型到账单的数据流转数据库是这类系统的地基。地面上的房间、房型、订单、账单如果关系没理清后期写接口会处处别扭。我建议按一条完整入住链路来设计客人先预订某个房型到店后分配到具体房间离店时按房型价格和入住天数结算。顺着链路核心表可以控制在七张以内。表名作用关键字段状态设计sys_user后台登录账号id, username, password, real_name, rolestatus 1启用 0禁用room_type房型及基础价格id, type_name, price, bed_count, area无room具体房间id, room_no, room_type_id, floor, status0空闲 1已预订 2入住 3维修customer住客资料id, name, phone, id_card无booking预订单id, room_type_id, customer_name, customer_phone, check_in_date, check_out_date, statuspending confirmed checked_in cancelledcheck_in_record入住登记id, booking_id, room_id, actual_check_in, actual_check_out无bill账单id, check_in_record_id, nights, total_amount, pay_timeunpaid paidroom_type 和 room 为什么要拆两张表因为大床房是一个类别而301 房间、302 房间是具体资源。预订时只需要锁定房型入住时才绑定具体房间。如果把房型信息直接塞进 room 表改一次价格要批量更新几十个房间后患无穷。预订和入住的状态也值得多说一句。我见过很多半成品系统把状态做成布尔值入住状态 0/1 看着简单但已预订但未入住这种中间态就没法表示。所以 room 表用 0 到 3 四个值booking 表用字符串枚举虽然多写几行判断但业务流程完整。建表时一个容易后悔的细节是字段类型。价格用 DECIMAL(10,2) 而不是 DOUBLE金额用浮点算会出现 0.10.2 不等于 0.3 的问题账单金额对不上时那叫一个难受。日期字段预订用 DATE 就够入住登记里的时间才需要 DATETIME。2.4 前端骨架Vite 创建 Vue3 工程与代理配置前端骨架我用 Vite 创建命令是 npm create vuelatest交互式选项里选 Vue3 和 JavaScriptRouter 和 Pinia 都勾上然后用 npm 安装 Element Plus 和 Axios。目录结构我习惯保持这样my-hotel-frontend/ ├── src/ │ ├── api/ # 所有请求封装按模块拆文件 │ ├── assets/ │ ├── components/ # 公共组件 │ ├── router/ # 路由与守卫 │ ├── stores/ # Pinia 状态 │ ├── views/ # 页面 │ │ ├── LoginView.vue │ │ ├── RoomView.vue │ │ ├── BookingView.vue │ │ └── DashboardView.vue │ ├── App.vue │ └── main.js └── vite.config.jsview 层按业务模块命名不用 Component 的思路去拆路由和页面一一对应答辩时讲结构也省事。vite.config.js 里一定要提前配好代理否则前端 5173 端口向后端 8080 发请求会被浏览器跨域策略拦掉import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })代理的作用是让浏览器以为请求是同源的前端代码里所有接口都写成 /api/...不用写完整域名。这个配置在开发期能省掉一半跨域烦恼但注意它只在开发服务器模式下生效打包后部署到 Nginx 时需要另配 location 转发这个区别答辩老师偶尔会问。3. 后端核心模块落地登录鉴权、预订并发与日志配置3.1 JWT 登录鉴权SpringBoot3 下的拦截器注册方式酒店管理系统的后台不可能裸奔至少要有登录和登录校验。主流做法是用 JWT登录成功后后端签发一个 token前端存到 localStorage之后每个请求在 Authorization 头里带回来。相比 sessionJWT 不需要在服务端存登录状态对毕设来说实现简单答辩也能讲清无状态认证的思路。先写一个生成和解析 token 的工具类以 jjwt 0.12 的 API 为例import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; public class JwtUtil { private static final String SECRET_KEY hotel-management-2025-secret-key-must-be-long-enough; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000L; public static String createToken(String username, String role) { SecretKey key Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); return Jwts.builder() .subject(username) .claim(role, role) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(key) .compact(); } public static Claims parseToken(String token) { SecretKey key Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)); return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); } }这里提醒一下jjwt 0.9 时代常用的 setSigningKey 在 0.12 里已经废弃换成 verifyWith。网上很多中文教程还在用旧 API照抄会得到一堆编译错误这也是我把 jjwt 版本单独拿出来强调的原因。密钥字符串要足够长HS256 算法对长度有校验太短会抛 WeakKeyException。拦截器负责统一校验Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String header request.getHeader(Authorization); if (StringUtils.hasText(header) header.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(header.substring(7)); request.setAttribute(username, claims.getSubject()); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { // token 过期或非法统一按未登录处理 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }SpringBoot3 里注册拦截器的方式是实现 WebMvcConfigurer注意是实现接口不是继承 WebMvcConfigurationSupport后者会覆盖掉 Spring MVC 的自动配置导致静态资源和默认配置失效。这是 SpringBoot2 时代的老坑现在依然有人踩Configuration public class WebConfig implements WebMvcConfigurer { Autowired private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login); } }excludePathPatterns 里要把登录接口、以及留给接口文档的 /v3/api-docs 和 /swagger-ui/** 都排除掉否则接口文档页面也进不来。3.2 预订与入住的并发控制事务和条件更新怎么配合酒店管理系统最容易出问题的不是 CRUD而是同一间房被两个人同时订走。虽然毕设演示时很难制造出真实并发但答辩老师一定会问你怎么保证一间房不会被重复分配答案就这一句话用条件更新保证原子性而不是先查再改。先查再改的问题在于两个请求同时查到房间状态是 0空闲然后都执行更新房间就可能被开出两张单。正确做法是把判断状态和更新状态合并成一条 SQL让数据库来保证原子性。Transactional(rollbackFor Exception.class) public void checkIn(Long bookingId, Long roomId) { Booking booking bookingMapper.selectById(bookingId); if (booking null || !confirmed.equals(booking.getStatus())) { throw new BizException(预订不存在或状态异常); } // 关键只有状态是空闲(0)时才能改成入住(2)影响行数为 0 说明被别人抢了 int rows roomMapper.updateStatusIfStatusIs(roomId, 2, 0); if (rows 0) { throw new BizException(该房间当前不可用请刷新后重试); } CheckInRecord record new CheckInRecord(); record.setBookingId(bookingId); record.setRoomId(roomId); record.setCustomerName(booking.getCustomerName()); record.setCustomerPhone(booking.getCustomerPhone()); record.setCheckInDate(LocalDate.now()); checkInRecordMapper.insert(record); bookingMapper.updateStatus(bookingId, checked_in); }对应的 Mapper 方法要这样写Update(UPDATE room SET status #{targetStatus} WHERE id #{roomId} AND status #{expectStatus}) int updateStatusIfStatusIs(Param(roomId) Long roomId, Param(targetStatus) Integer targetStatus, Param(expectStatus) Integer expectStatus);updateStatusIfStatusIs 返回的 int 是受影响行数。只有当房间还是空闲状态时更新才会成功返回 1如果别的请求已经把房间占了条件不成立返回 0。再用 Transactional 把整个方法包起来中途任何一步抛异常前面的更新都会回滚。这个方法规模不大但它是这个系统里最值得在答辩时展开讲的一块比堆十个增删改查接口有价值得多。预订阶段同理可以根据日期范围先去统计该房型在目标时间段内的空闲房间数再插入预订记录。如果是自用系统不需要那么严谨可以简化成预订时不锁房间、入住时再校验但你要能说清楚为什么可以简化而不是没想过这个问题。3.3 logback-spring.xml 落地按天滚动日志与 log4j2 的选择日志配置在毕设里常常被忽略但联调阶段你会感谢它。SpringBoot3 默认的日志实现是 Logback配置文件名用 logback-spring.xml 而不是 logback.xml区别在于前者支持 springProfile 标签可以按开发、生产环境切换日志级别后者不行。我给这类管理系统的配置一般长这样?xml version1.0 encodingUTF-8? configuration property nameLOG_HOME value./logs/ appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/hotel.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_HOME}/hotel-%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger - %msg%n/pattern /encoder /appender root levelINFO appender-ref refCONSOLE/ appender-ref refFILE/ /root logger namecom.yourcompany.hotel levelDEBUG/ /configurationpattern 里的 %d 是时间%-5level 是日志级别%logger{36} 是类名缩写%msg 是消息内容。关键参数是 fileNamePattern 里的 %d{yyyy-MM-dd}它让日志按天滚动每天生成一个文件maxHistory 保留 30 天。这样联调时想看某天的报错直接去 logs 目录找对应日期的文件。你可能在检索时会看到 springboot3 log4j2 的相关讨论。Log4j2 确实是一个可选项优点是基于 LMAX Disruptor 的异步性能但需要先在 pom 里排除 spring-boot-starter-logging再加入 spring-boot-starter-log4j2然后写 log4j2-spring.xml。对酒店管理系统这个量级Log4j2 的性能优势根本用不出来反而多两处配置失误的风险。我的建议是老老实实用 Logback把时间花在把日志级别调对、把 SQL 打出来这类真的能帮你 debug的事情上。如果为了简历上多写一行熟悉 Log4j2而强行换答辩时被问到配置细节反而容易露怯。3.4 接口文档springdoc-openapi 替代 Swagger2 的迁移给后端写接口文档能直接生成可在线调试的页面答辩演示时打开 Swagger UI 秀一下比翻代码讲接口直观得多。但注意 SpringBoot3 下不能再用 springfox 那套 Swagger2它的底层停留在 javax 时代启动就会失败。现在主流做法是用 springdoc 的 starterdependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.6.0/version /dependency依赖加进去后在 application.yml 里补两行springdoc: api-docs: enabled: true swagger-ui: path: /swagger-ui.html启动后访问 http://localhost:8080/swagger-ui.html 就能看到页面。注解也换了原来的 Api 换成 TagApiOperation 换成 OperationApiParam 换成 Parameter。写接口时顺手加这些注解工作量不大但生成的文档可读性完全不一样。有一个细节要留意如果拦截器把 /api/** 全部拦了springdoc 的接口文档路径不在放行名单里页面就打不开。我在 3.1 提过要在 excludePathPatterns 里放行 /v3/api-docs 和 /swagger-ui/**这里再强调一次因为这两类问题经常一起出现排查时容易绕圈子。4. Vue3 前端落地登录态、房间管理页与预订联调4.1 Axios 拦截器让每个请求自动带上 Token前端接入后端的第一步不是写页面而是把请求层封装好。所有接口请求统一走一个 Axios 实例在拦截器里做两件事请求发出前带上 token响应返回时统一处理业务码和 401。// src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器把登录后存的 token 塞进请求头 request.interceptors.request.use(config { const token localStorage.getItem(hotel_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理业务码和登录过期 request.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 { if (error.response error.response.status 401) { localStorage.removeItem(hotel_token) router.push(/login) ElMessage.warning(登录已过期请重新登录) } else { ElMessage.error(error.response?.data?.message || 网络异常) } return Promise.reject(error) } ) export default requestbaseURL 写成 /api 而不是 http://localhost:8080是为了配合上一章 Vite 代理的工作方式。后端返回结构约定成{ code, message, data }三件套前端拦截器里统一判断 code页面上就不再需要每个请求都写一遍错误提示。token 放 localStorage 对毕设系统足够不用担心 XSS 那层攻击答辩能说清这个取舍就行。4.2 Element Plus 房间管理页表格加弹窗的最小实现房间管理页是这类系统最标准的 CRUD 页面用 Element Plus 的 table dialog 就能完成。下面这个示例是去掉大量样式后的核心骨架template el-table :datarooms border stripe el-table-column proproomNo label房间号 width120/ el-table-column proproomTypeName label房型/ el-table-column propfloor label楼层 width80/ el-table-column label状态 width100 template #default{ row } el-tag :typetagType(row.status) {{ statusLabel(row.status) }} /el-tag /template /el-table-column el-table-column label操作 width160 template #default{ row } el-button link typeprimary clickopenDialog(row)编辑/el-button el-button link typedanger clickremoveRoom(row)删除/el-button /template /el-table-column /el-table el-button typeprimary clickopenDialog()新增房间/el-button el-dialog v-modeldialogVisible :titleform.id ? 编辑房间 : 新增房间 width500px el-form :modelform label-width80px el-form-item label房间号 el-input v-modelform.roomNo/ /el-form-item el-form-item label房型 el-select v-modelform.roomTypeId placeholder选择房型 el-option v-fort in roomTypes :keyt.id :labelt.typeName :valuet.id/ /el-select /el-form-item el-form-item label楼层 el-input-number v-modelform.floor :min1/ /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksaveRoom保存/el-button /template /el-dialog /template script setup import { ref, onMounted } from vue import { listRoomsApi, saveRoomApi, deleteRoomApi } from /api/room const rooms ref([]) const dialogVisible ref(false) const form ref({}) const statusLabel (s) ({ 0: 空闲, 2: 入住, 3: 维修 }[s] ?? 已预订) const tagType (s) (s 0 ? success : s 2 ? danger : warning) async function loadRooms() { const res await listRoomsApi() rooms.value res.data } function openDialog(row) { form.value row ? { ...row } : { roomNo: , roomTypeId: null, floor: 1 } dialogVisible.value true } async function saveRoom() { await saveRoomApi(form.value) dialogVisible.value false await loadRooms() } async function removeRoom(row) { await deleteRoomApi(row.id) await loadRooms() } onMounted(loadRooms) /script有两个 Vue3 的细节容易出错。第一表格数据用 ref 管理而不是 reactive因为接口返回后直接给 rooms.value 赋值ref 能保持响应式reactive 直接赋值新数组会断开响应链接页面不更新。第二弹窗编辑时用{ ...row }展开对象避免直接修改表格里的原数据否则用户取消编辑表格里也已经变了。4.3 Pinia 管登录态路由守卫与刷新恢复登录态管理我推荐 PiniaVue3 官方状态库比 Vuex 少一层样板代码。核心思路是token 存在 Pinia 里同时同步到 localStorage页面刷新后从 localStorage 恢复。// src/stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(hotel_token) || , userInfo: null }), actions: { setToken(token) { this.token token localStorage.setItem(hotel_token, token) }, setUserInfo(info) { this.userInfo info }, logout() { this.token this.userInfo null localStorage.removeItem(hotel_token) } } })路由守卫负责挡住未登录的访问// src/router/index.js import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user router.beforeEach((to) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { return { path: /login } } return true })这里把 token 是否存在的判断放在 Pinia 的 state 里而不是每次都读 localStorage好处是登录和注销动作都收敛到 store 的 action 里路由守卫和 Axios 拦截器读的是同一份状态不会出现页面显示已登录、请求却被 401的错位。4.4 联调配置Vite 代理解决跨域后端 CORS 兜底开发时 Vite 代理已经处理了大部分跨域问题但有些场景比如直接访问后端接口文档、或者临时用 Postman 测试还是会走后端 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); } }这里有个 2025 年依然有人在踩的坑allowedOrigins() 和 allowCredentials(true) 同时使用Spring 会直接报 IllegalArgumentException。解决办法一是 allowedOriginPatterns() 代替二是把 allowCredentials 设成 false。对这个毕设系统前端和后端都是自己写的安全边界没那么严格用 allowedOriginPatterns 省事。联调时如果发现请求发出去了但响应被浏览器拦下先看控制台里 CORS error 的具体信息别着急改代码。最常见的其实是代理没生效——比如前端请求写成了全路径 http://localhost:8080绕过了 Vite 代理又回到了跨域的老问题上。5. 避坑清单SpringBoot3Vue3 联调阶段最容易栽的五个坑5.1 javax 全部报红SpringBoot3 的 jakarta 包名迁移现象从旧教程或学长代码里复制过来的文件出现大面积 import 报红提示 package javax.servlet 不存在。原因SpringBoot3 基于 Jakarta EE 9官方把 javax 开头的包名整体迁移到了 jakarta。这个改动是 SpringBoot2 升 3 最直观的破坏性变化很多老代码片段直接失去了意义。解决在 IDEA 里用全局替换把 import javax. 改成 import jakarta.。需要注意 servlet 相关 API 里的类名本身没变变的只有包名所以批量替换是安全的。5.2 MyBatis-Plus 配置了但启动就报错现象项目启动时抛异常堆栈里有 ClassNotFoundError 或者关于 SqlSessionFactory 的 BeanCreationException而且信息指向 mybatis-spring 相关类。原因用了老坐标 mybatis-plus-boot-starter而它默认带的是适配 SpringBoot2 的 mybatis-spring在 SpringBoot3 的类加载环境下直接不兼容。解决换成 mybatis-plus-spring-boot3-starter并把版本提到 3.5.5 及以上。这个坑的特点是一旦踩中报错时机在启动早期信息也不直接指向版本不兼容不熟悉的人会在数据源配置上反复排查浪费不少时间。5.3 数据库时间和页面时间差了 8 小时现象前端提交的入住日期是 2025-05-01存到数据库后查出来变成 2025-04-30。原因MySQL 连接的时区没有设置JDBC 驱动默认按照 JVM 所在时区去解析而连接串里的 serverTimezone 缺失时双方各按各的时区解释时间。解决连接串加上 serverTimezoneAsia/Shanghai同时检查 Jackson 侧的时间序列化配置。如果后端给前端返回时间也差 8 小时那就要在 application.yml 里给 Jackson 配 time-zone。这个坑不到实际联调不会暴露而且出错位置可能在数据库、可能在 JSON 序列化、也可能在浏览器显示排查时先统一确认某一层别两头乱改。5.4 Vue3 里 reactive 赋值后页面不更新现象接口数据返回后console.log 能看到数据但页面上表格空白或者还是旧数据。原因reactive 声明的对象如果整体赋一个新数组等于把响应式代理对象换成了一个普通对象Vue3 的依赖收集失效。这是 Vue3 新手最常见的玄学问题也是从 Vue2 的 data 写法迁移过来最容易踩的。解决要么列表数据用 ref 声明赋值时写 rooms.value res.data要么保留 reactive 但用 push 或 splice 更新。我的习惯是列表这种整体替换频繁的数据一律 ref表单这种嵌套结构用 reactive 配合 Object.assign 更新。5.5 CORS 配了还是被拦现象前端控制台报 CORS error后端也有 CORS 配置但请求就是过不去。原因有两种常见情况。一是 allowedOrigins() 配了 credentialsSpring 直接拒绝这种组合二是后端同时有 Spring Security安全过滤器链的顺序比 MVC 的 CORS 配置更靠前请求在到达你的 CorsConfig 之前就被拦了。解决先用 Vite 代理兜底把跨域问题从开发期挪走后端 CORS 配置用 allowedOriginPatterns() 并加上 OPTIONS 方法如果引了 Spring Security在 SecurityConfig 里调用 cors() 让安全框架感知跨域配置。跨域问题看起来是配置问题实际上经常是过滤器顺序问题排查时按先确认请求到达了哪一层这个思路来。6. 答辩前用一条业务流把系统验到能上台6.1 完整业务流自测清单很多毕设翻车不是功能没写完而是功能之间没有串起来。我的习惯是答辩前两周开始每天跑一遍完整业务流用一个固定的操作清单序号操作预期结果1用管理员账号登录跳转首页顶部显示用户名2新增两个房型、四间房房间列表状态均为空闲3创建一个两天后的预订预订状态为已确认对应房型房间标记已预订4对该预订办理入住分配具体房间房间状态变为入住5查询该订单详情订单状态变为已入住6办理退房并结算生成账单金额天数×房型价格7查看房间列表该房间恢复空闲8未登录直接访问后台页面被重定向到登录页9刷新页面后再操作登录态保持不需要重新登录10看当天日志文件能查到每一步的请求与关键 SQL这十条能稳定跑通答辩演示环节基本不会出现老师指到哪项目死在哪的尴尬。金额对不上、状态跳不对这类问题也都会在反复跑流程时暴露出来。6.2 从日志定位问题的基本姿势联调阶段接口出问题先别急着加打印。按这个顺序走看浏览器 Network 里请求是否到达后端看控制台有没有业务异常堆栈再去 logs 目录里查当天的滚动日志。logback-spring.xml 里配了按天滚动所以查问题直接打开对应日期的文件搜索异常时间点前后几十行基本就能定位到是哪个接口、哪条 SQL 出的问题。一个值得养的调试习惯把日志级别里的 SQL 输出保留到答辩前一两天再改成只输出 INFO。这样平时能看到 MyBatis-Plus 实际执行的 SQL排查参数绑定问题时非常有帮助答辩演示时控制台又不会被大量 SQL 刷屏。6.3 演示时的三个加分细节演示时不用把增删改查都点一遍老师更想看你对自己系统的理解深度。我会重点准备三块登录鉴权的完整链路——登录、发 token、前端拦截器带头、后端拦截器校验预订入住的并发控制——把条件更新的那条 SQL 打开给老师看讲清楚为什么这样能防止超卖日志体系——现场打开当天的日志文件指出一次完整操作在日志里的痕迹。这三个点都讲明白比页面点多十个更有说服力。答辩前最深的感受是能给老师稳定演示一条完整业务流比准备一堆这个功能我还没来得及做的解释要踏实得多。我做这类系统有个习惯每次改完代码都先跑一遍上面的十项清单不通过不继续往下写这个习惯救了我不止一次。现在把这套流程写出来希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →