简介:这是一套面向计算机专业本科生的毕业设计级酒店管理系统实战项目,聚焦Java全栈开发能力培养,特别适合SpringBoot与Vue.js初学者完成课程设计或毕业课题。资源包含7个文件:3个ZIP包(含完整前后端源码、返修版源码及项目主压缩包)、2份Word文档(开题报告与任务书)、1个SQL数据库脚本和1个MP4系统演示录屏,总大小77.48MB,结构清晰、开箱即用。已有69人学习下载,体现了其在教学实践中的实用认可度。学习者可直接获取可运行的SpringBoot3+Vue.js3前后端分离代码、MySQL8建库脚本、完整开发文档及B站实操录屏(含启动教程与功能演示),覆盖需求分析、模块设计、接口联调到部署验证全流程,有效支撑从理论到落地的闭环训练。
1. 为什么用 SpringBoot3 + Vue.js3 做酒店管理系统,真不是为了“看起来新”——它卡在毕业答辩前最后一道硬门槛上
去年帮三个计算机专业学生改毕设,其中两个用 SpringBoot2.7 + Vue2 写的酒店系统,在答辩现场被老师连问三句:“WebSocket 实时房态同步怎么保证不丢帧?”“Vue Router 的路由守卫在登录态失效时有没有兜底跳转?”“你这个@Valid校验没配@NotNull和@Size组合约束,前台绕过 JS 校验直接 POST 空字符串,后端会抛 500 还是 400?”——当场哑火。不是代码写得差,是技术栈底座扛不住真实业务逻辑的压测边界。SpringBoot3(基于 Jakarta EE 9+)和 Vue.js3(Composition API +<script setup>+ 更严格的响应式代理)组合,本质是一次面向交付闭环的工程收敛:后端用spring-boot-starter-validation+springdoc-openapi-ui自动生成带校验规则的 Swagger 文档,前端用zod或yup做表单 schema 双向对齐,连数据库字段长度、非空约束、枚举值范围,都能从 Java Bean 的@Column(length=32)自动映射到 Vue 表单组件的maxLength和required属性。这不是炫技,是让毕设从“能跑通 CRUD”升级到“能讲清数据契约如何贯穿前后端”。适合正在开题、已卡在选题合规性审查、或正被导师质疑“技术深度不够”的本科生——尤其当你发现教务系统里“基于 SpringBoot 的毕设占比已达 68%”,而其中仅 12% 明确标注 SpringBoot3 版本时,你就知道:这不仅是选题,是答辩材料里的隐性评分项。
2. 用 SpringBoot3 搭建酒店核心服务:从依赖选择到 RESTful 接口契约设计
2.1 为什么必须用 SpringBoot3.2.x 而不是 3.0.x?关键在 Jakarta EE 9.1 的jakarta.validation兼容层
SpringBoot3 强制要求 Jakarta EE 9+,这意味着所有javax.*包全部替换为jakarta.*。但很多同学直接mvn clean install后发现@NotBlank校验失效,日志里刷出java.lang.NoClassDefFoundError: javax/validation/ConstraintViolationException——这是典型的老版 Hibernate Validator(6.2.x)残留。正确做法是显式声明验证器版本:
<!-- pom.xml --> <properties> <hibernate-validator.version>8.0.1.Final</hibernate-validator.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <!-- SpringBoot3.2.x 默认拉取 hibernate-validator 8.0.x,无需额外指定 --> </dependency> </dependencies>提示:
spring-boot-starter-validation在 3.2.x 中已内置jakarta.validation-api3.0.2,若手动引入validation-api2.0.x(javax 版本),Maven 会因包冲突导致@Valid完全不生效。务必执行mvn dependency:tree | grep validation确认输出中只有jakarta.validation。
2.2 酒店业务建模:Room、Booking、Customer 三张表的 JPA 实体设计要点
酒店系统最易翻车的是房态并发控制。比如两个前台同时操作同一间房的入住/退房,传统@Version乐观锁在高并发下仍可能漏判。我们采用“状态机 + 数据库行锁”双保险:
// Room.java @Entity @Table(name = "room") public class Room { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "room_number", unique = true, nullable = false) private String roomNumber; // 房号,如 "1001" @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) private RoomStatus status; // CLEAN, OCCUPIED, MAINTENANCE, DIRTY @Version private Long version; // 乐观锁版本号 // 注意:不要用 @OneToMany(mappedBy = "room") 直接关联 Booking // 改用 Service 层通过 room_id 查询,避免 N+1 和级联删除误删历史订单 }// BookingService.java @Transactional public Booking createBooking(BookingCreateDTO dto) { // 步骤1:SELECT FOR UPDATE 锁定房间行(MySQL InnoDB) Room room = roomRepository.findByRoomNumberAndStatus(dto.getRoomNumber(), RoomStatus.CLEAN) .orElseThrow(() -> new BusinessException("房间不可用:" + dto.getRoomNumber())); // 步骤2:检查是否已被其他事务锁定(此时 version 已被数据库加锁) if (!room.getStatus().equals(RoomStatus.CLEAN)) { throw new BusinessException("房间状态变更,请刷新重试"); } // 步骤3:更新房间状态并保存订单 room.setStatus(RoomStatus.OCCUPIED); roomRepository.save(room); // 触发 version + 1 Booking booking = new Booking(); booking.setRoom(room); booking.setCheckInDate(dto.getCheckInDate()); booking.setCheckOutDate(dto.getCheckOutDate()); return bookingRepository.save(booking); }逻辑说明:SELECT ... FOR UPDATE在事务内对room_number加行锁,确保同一房间不会被两个事务同时读取为CLEAN状态;@Version则防止事务提交时覆盖对方的修改。参数说明:RoomStatus枚举必须包含CLEAN(空闲可订)、OCCUPIED(已入住)、DIRTY(退房未打扫)、MAINTENANCE(维修中)四种状态,缺一不可——这是酒店实际运营的最小状态集,少一个就会导致前台操作逻辑断裂。
2.3 RESTful 接口设计:用 OpenAPI 3.0 自动生成文档,让答辩老师一眼看懂你的接口契约
SpringBoot3 内置springdoc-openapi-starter-webmvc-ui,比旧版springfox更轻量且支持 Jakarta EE。配置只需两步:
# application.yml springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /swagger-ui.html operations-sorter: method # 按 HTTP 方法排序,GET/POST/PUT/DELETE 分组 tags-sorter: alpha # 标签按字母排序然后在 Controller 上用标准 OpenAPI 注解:
@RestController @RequestMapping("/api/v1/rooms") @Tag(name = "客房管理", description = "客房状态查询、预订、退房等操作") public class RoomController { @Operation(summary = "根据房号查询房间详情", description = "返回房间编号、当前状态、楼层、价格等信息") @ApiResponses({ @ApiResponse(responseCode = "200", description = "成功返回房间信息"), @ApiResponse(responseCode = "404", description = "房间不存在") }) @GetMapping("/{roomNumber}") public ResponseEntity<RoomDTO> getRoomByNumber(@PathVariable String roomNumber) { Room room = roomService.findByRoomNumber(roomNumber); return ResponseEntity.ok(roomMapper.toDTO(room)); } }参数说明:@Tag定义模块分组,答辩时老师点开 Swagger UI 就能看到清晰的“客房管理”“订单管理”“客户管理”三大板块;@ApiResponse显式声明每个 HTTP 状态码的业务含义,避免答辩时被问“400 和 404 你怎么区分?”;@Operation的summary必须用动宾短语(如“查询房间详情”),不能写“获取房间”——这是 OpenAPI 规范对可读性的硬性要求。
3. 用 Vue.js3 实现酒店前台交互:Composition API + Pinia + Axios 请求拦截实战
3.1 为什么放弃 Options API?用<script setup>写房态看板的响应式逻辑更直白
酒店前台最核心的页面是“实时房态看板”,需动态渲染 100+ 个房间卡片,并支持点击切换状态。Options API 下,data、methods、computed散落在不同区块,调试时要反复跳转;而 Composition API 可将同一业务逻辑聚合成一个逻辑单元:
<!-- RoomDashboard.vue --> <script setup> import { ref, onMounted, watch } from 'vue' import { useRoomStore } from '@/stores/room' import { getRooms } from '@/api/room' const roomStore = useRoomStore() const loading = ref(true) const filterStatus = ref('ALL') // ALL / CLEAN / OCCUPIED / DIRTY // 1. 初始化加载 onMounted(async () => { await loadRooms() }) // 2. 状态过滤响应式联动 watch(filterStatus, async (newVal) => { loading.value = true await loadRooms() }) // 3. 核心加载逻辑 const loadRooms = async () => { try { const params = newVal === 'ALL' ? {} : { status: newVal } const rooms = await getRooms(params) // 调用封装好的 API roomStore.setRooms(rooms) // 写入 Pinia store } catch (error) { ElMessage.error('加载房间失败:' + error.message) } finally { loading.value = false } } </script> <template> <div class="dashboard"> <el-select v-model="filterStatus" placeholder="筛选状态"> <el-option label="全部" value="ALL" /> <el-option label="空闲" value="CLEAN" /> <el-option label="入住" value="OCCUPIED" /> <el-option label="待打扫" value="DIRTY" /> </el-select> <div v-loading="loading" class="room-grid"> <RoomCard v-for="room in roomStore.filteredRooms" :key="room.id" :room="room" /> </div> </div> </template>逻辑说明:watch监听filterStatus变化自动触发loadRooms,避免手写@change事件;roomStore.filteredRooms是 Pinia store 中的 computed 属性,封装了状态过滤逻辑,组件模板里直接使用,无需在setup中重复计算。这种写法让答辩老师能快速定位“状态筛选”功能的完整链路:从 Select 组件 →watch→ API 请求 → Store 更新 → 模板渲染。
3.2 Pinia 状态管理:为什么不用 Vuex?用defineStore管理跨页面共享的订单数据
Vuex 在 Vue3 中已官方弃用,Pinia 是唯一推荐方案。酒店系统中,用户从“房态页”点击预订进入“订单页”,再跳转到“支付页”,订单数据必须跨路由持久化。Pinia 的persist插件可解决:
// stores/booking.js import { defineStore } from 'pinia' import { createPersistedState } from 'pinia-plugin-persistedstate' export const useBookingStore = defineStore('booking', { state: () => ({ currentBooking: null, // 当前正在编辑的订单 history: [], // 历史订单列表(仅缓存最近 10 条) }), actions: { setBooking(booking) { this.currentBooking = booking // 关键:只持久化 currentBooking,history 不存本地,避免敏感信息泄露 this.$persist() }, clearBooking() { this.currentBooking = null // 清除时主动删除 localStorage 中的 key localStorage.removeItem('pinia_booking_currentBooking') } }, // 持久化配置:只存 currentBooking,且加密存储(简单 base64,毕设够用) persist: { key: 'hotel_booking', storage: { getItem(key) { const raw = localStorage.getItem(key) return raw ? JSON.parse(atob(raw)) : null }, setItem(key, value) { localStorage.setItem(key, btoa(JSON.stringify(value))) } }, paths: ['currentBooking'] // 仅持久化此字段 } })参数说明:paths: ['currentBooking']明确指定只持久化订单对象,避免把history(含客户身份证号、联系方式)也存进浏览器;btoa/atob是基础编码,答辩时可解释“满足毕设安全要求,生产环境应换为 AES 加密”——既体现安全意识,又不增加实现复杂度。
3.3 Axios 请求拦截:统一处理 401 登录态失效,避免每个页面都写重复逻辑
酒店系统多页面需登录态校验,若每个 API 调用都手动判断response.status === 401,代码冗余且易漏。Axios 拦截器是标准解法:
// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import { useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' const request = axios.create({ baseURL: '/api/v1', timeout: 10000, }) // 请求拦截:自动携带 token request.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截:统一处理 401 request.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { const authStore = useAuthStore() authStore.logout() // 清空 token 和用户信息 const router = useRouter() router.push('/login?redirect=' + encodeURIComponent(router.currentRoute.value.fullPath)) ElMessage.warning('登录已过期,请重新登录') } return Promise.reject(error) } ) export default request逻辑说明:router.push('/login?redirect=...')中的redirect参数,确保用户登录后自动跳回原页面,这是酒店前台连续操作(如查房→订房→付款)的体验刚需;authStore.logout()必须同步清除localStorage中的 token 和 Pinia store 中的用户数据,否则可能出现“界面显示已登出,但 API 仍携带旧 token”的玄学问题。
4. 前后端联调避坑指南:5 个让毕设答辩前夜崩溃的真实问题与解法
4.1 现象:Vue 页面请求/api/v1/rooms返回 404,但 Postman 能正常访问
原因:Vue 开发服务器(Vite)默认不代理 API 请求,浏览器实际发送请求到http://localhost:5173/api/v1/rooms,而非后端http://localhost:8080/api/v1/rooms
解决:在vite.config.ts中配置代理,注意路径重写规则:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') // 把 /api/v1/rooms → /v1/rooms } } } })注意:
rewrite是关键!SpringBoot3 默认 context-path 为空,若后端server.servlet.context-path=/hotel,则target应改为http://localhost:8080/hotel,且rewrite改为path.replace(/^\/api/, '/hotel')。
4.2 现象:提交订单时后端报Method Not Allowed,日志显示收到的是 GET 请求
原因:Vue 中axios.post(url, data)被误写成axios.get(url, { data }),而get的第二个参数是config,data被当作params拼到 URL 后,后端收到 GET 请求却期待 POST
解决:严格区分请求方法,POST 必须用axios.post(url, payload),GET 查询用axios.get(url, { params: { key: value } })。可在utils/request.js中封装:
export function postBooking(data) { return request.post('/bookings', data) // 第二个参数是 payload body } export function getRooms(params) { return request.get('/rooms', { params }) // 第二个参数是 { params: {} } }4.3 现象:房间状态切换后,Vue 页面未更新,但 F5 刷新就正常
原因:JPA 实体类中RoomStatus枚举未重写toString(),JSON 序列化后前端收到"status": "OCCUPIED"字符串,但 Vue 组件中v-if="room.status === RoomStatus.OCCUPIED"比较的是RoomStatus.OCCUPIED对象引用,而非字符串值
解决:在枚举类中添加toString():
public enum RoomStatus { CLEAN, OCCUPIED, DIRTY, MAINTENANCE; @Override public String toString() { return name(); // 返回大写字符串,与 JSON 一致 } }并在 Vue 中用字符串字面量比较:v-if="room.status === 'OCCUPIED'",或定义常量const ROOM_STATUS = { CLEAN: 'CLEAN', OCCUPIED: 'OCCUPIED' }。
4.4 现象:Swagger UI 中BookingCreateDTO的checkInDate字段显示为string,但后端期望LocalDateTime
原因:SpringBoot3 默认 JSON 序列化器(Jackson)对LocalDateTime的处理策略未配置,导致 Swagger 无法推断格式
解决:在application.yml中添加:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss serialization: write-dates-as-timestamps: false deserialization: read-dates-as-timestamps: false并在 DTO 字段上加注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime checkInDate;4.5 现象:打包部署后,Vue 静态资源 404,页面空白
原因:Vite 打包后index.html中的资源路径为/assets/xxx.js,但 Nginx 未配置location /指向dist目录,或未开启try_files $uri $uri/ /index.html
解决:Nginx 配置必须包含:
location / { alias /var/www/hotel-frontend/dist/; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }提示:
alias和root的区别是关键!alias会完全替换路径,root会拼接路径,此处必须用alias。
5. 毕设答辩加分技巧:用 Docker Compose 一键启动全栈环境,让老师现场验证
5.1 为什么 Docker 是答辩“后悔药”?它消灭了“在我电脑上是好的”这种致命借口
去年有学生答辩时演示“房态实时同步”,老师用自己笔记本打开页面,结果一片空白——因为他的 JDK 版本是 17,而学生开发用的是 JDK 21,record类型编译失败。Docker 的价值不是“高大上”,是交付确定性:把 SpringBoot3 后端、Vue3 前端、MySQL、Redis 全部打包进docker-compose.yml,老师双击docker-compose up -d就能启动完整环境,连端口映射、网络互通、初始化 SQL 都预置好。这比口头解释“我用了微服务”有力一万倍。
5.2 Docker Compose 文件详解:MySQL 初始化脚本 + Nginx 反向代理配置
# docker-compose.yml version: '3.8' services: db: image: mysql:8.0 container_name: hotel-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: hotel_db MYSQL_USER: hotel_user MYSQL_PASSWORD: hotel_pass volumes: - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql - ./mysql-data:/var/lib/mysql ports: - "3306:3306" backend: build: ./backend container_name: hotel-backend depends_on: - db environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/hotel_db?useSSL=false&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: hotel_user SPRING_DATASOURCE_PASSWORD: hotel_pass ports: - "8080:8080" restart: on-failure frontend: build: ./frontend container_name: hotel-frontend depends_on: - backend ports: - "80:80" restart: on-failure nginx: image: nginx:alpine container_name: hotel-nginx volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html depends_on: - backend - frontend ports: - "80:80"关键点解析:
./sql/init.sql是 MySQL 初始化脚本,必须包含CREATE TABLE room (...)和INSERT INTO room VALUES (...)示例数据,让老师启动后立刻看到房态;SPRING_DATASOURCE_URL中的db是 Docker 内部服务名,不是localhost,这是容器网络互通的前提;nginx.conf需配置反向代理,将/api/转发到backend:8080,静态资源由 Nginx 直接服务,这是生产环境标准架构。
5.3 毕设材料包必备清单:3 个让老师眼前一亮的交付物
| 文件名 | 作用 | 答辩话术 |
|---|---|---|
README.md | 包含docker-compose up -d启动命令、默认账号密码(admin/123456)、核心功能演示路径(如“登录后点击【房态看板】查看实时状态”) | “老师,您只需要运行这一行命令,就能看到完整系统,所有功能都已预置数据” |
architecture.png | 手绘风格架构图:左侧 Vue3 前端(标出 Pinia/Axios),中间 Nginx(标出反向代理),右侧 SpringBoot3 后端(标出 JPA/Validation/OpenAPI),底部 MySQL/Redis | “这是我设计的分层架构,每一层都对应一个 Docker 服务,解耦清晰,便于后期扩展” |
test-case.xlsx | 10 条手工测试用例:如“用 admin 登录 → 查看房态 → 点击 1001 房间预订 → 输入入住时间 → 提交 → 验证房间状态变为 OCCUPIED” | “这些是我在开发过程中执行的回归测试,覆盖了核心业务流,确保功能稳定” |
最后说句血泪经验:我带过的毕设里,凡是在答辩 PPT 第一页就放docker-compose.yml截图、第二页放curl http://localhost/api/v1/rooms \| jq返回结果的同学,答辩通过率 100%。不是因为 Docker 多高级,而是它把“我能做”变成了“您现在就能验证”。技术选型的价值,永远在交付那一刻才真正兑现。希望帮到你。
本文还有配套的精品资源,点击获取