在实际企业级应用开发中,酒店管理系统是一个集客房管理、订单处理、客户信息、财务统计于一体的综合性项目。它不仅是常见的毕业设计选题,更是检验开发者能否将前后端分离、数据库设计、业务逻辑分层等核心技能融会贯通的实战场景。本文将以一个基于 Spring Boot 后端和 Vue 3 前端的酒店管理系统为例,手把手带你从零开始,完成一个结构清晰、功能完整、可直接运行的实战项目。你将不仅获得一套可用的源码和数据库,更能理解如何将 Spring Boot 的自动配置、MyBatis-Plus 的数据操作、Vue 3 的组合式 API 以及 Element Plus 的组件库串联起来,构建一个具备增删改查、权限控制、数据可视化等典型功能的管理后台。文章会详细解释每个模块的设计思路、关键代码的实现逻辑、配置文件的参数含义,并提供完整的排错路径和部署建议,确保你能在自己的开发环境中成功运行并理解其背后的工程实践。
1. 项目整体架构与技术选型解析
一个可维护、易扩展的酒店管理系统,其技术栈的选型决定了后续开发的效率和项目的稳定性。本项目的核心是前后端分离架构,后端提供 RESTful API,前端负责页面渲染和用户交互。
1.1 后端技术栈:Spring Boot 为核心的 Java 生态
后端采用 Spring Boot 2.7.x 作为基础框架,它极大地简化了 Spring 应用的初始搭建和开发过程。选择此版本是因为其长期支持(LTS)状态稳定,社区资源丰富。
- 持久层:使用 MyBatis-Plus 作为 ORM 框架。它是对 MyBatis 的增强,在保留 MyBatis 灵活性的同时,提供了通用的 CRUD 操作,无需编写大量 XML 映射文件,能显著提升开发效率。例如,对于
Room(客房)表的常规增删改查,通过继承BaseMapper即可获得。 - 数据校验:集成 Hibernate Validator,用于对控制器(Controller)层接收的参数进行注解式校验,确保输入数据的合法性,如客房价格不能为负数。
- 权限控制:采用 Spring Security 结合 JWT(JSON Web Token)实现认证与授权。用户登录成功后,后端生成一个加密的 Token 返回给前端,前端在后续请求中携带此 Token,后端通过过滤器链进行校验,从而保护 API 接口。
- 数据库:使用 MySQL 8.0 作为关系型数据库。其事务支持完善,适合处理订单、账单等需要强一致性的业务。
- 项目管理与构建:使用 Maven 进行依赖管理和项目构建。
1.2 前端技术栈:Vue 3 构建现代化管理界面
前端采用 Vue 3 的组合式 API(Composition API)进行开发,相比 Vue 2 的选项式 API,逻辑组织和复用性更强。
- UI 框架:使用 Element Plus,它是基于 Vue 3 的桌面端组件库,提供了丰富、美观的组件,如表格、表单、弹窗、导航菜单等,能快速搭建出专业的管理后台界面。
- 状态管理:使用 Pinia 作为状态管理库。它是 Vue 官方推荐的状态管理工具,比 Vuex 更简洁,TypeScript 支持更好,用于管理用户登录状态、全局配置等跨组件数据。
- 路由管理:使用 Vue Router 4.x 进行前端路由控制,实现单页面应用(SPA)的无刷新页面切换。
- HTTP 客户端:使用 Axios 库发起 HTTP 请求,与后端 API 进行通信。我们会对其进行封装,统一处理请求拦截(如添加 Token)、响应拦截(如处理通用错误)和基础配置。
- 构建工具:使用 Vite 作为前端构建工具。Vite 提供了极速的冷启动和快速的热更新,开发体验远优于传统的 Webpack。
1.3 系统核心功能模块设计
酒店管理系统的业务模块通常围绕核心实体展开。以下是本项目设计的主要功能模块:
| 模块名称 | 核心实体 | 主要功能 |
|---|---|---|
| 权限管理 | User, Role, Menu | 用户登录、注销、角色分配、菜单权限控制。 |
| 客房管理 | Room, RoomType | 客房的增删改查、状态管理(空闲、已预订、入住中、维修中)、房型管理。 |
| 客户管理 | Customer | 客户信息的登记、查询、会员管理。 |
| 订单管理 | Order | 客户预订客房、入住登记、退房结账、订单历史查询。 |
| 财务管理 | Bill | 账单生成、支付记录、收入统计。 |
| 数据统计 | - | 基于 ECharts 的可视化图表,展示客房入住率、收入趋势等。 |
2. 开发环境准备与项目初始化
在开始编码之前,必须确保本地开发环境配置正确,这是项目能成功运行的第一步。
2.1 后端环境准备
- Java 开发工具包 (JDK):安装 JDK 8 或 JDK 11(推荐 JDK 11)。配置
JAVA_HOME环境变量。# 检查安装是否成功 java -version - 集成开发环境 (IDE):使用 IntelliJ IDEA(社区版或旗舰版)或 Eclipse(需安装 Spring Tools)。IDEA 对 Spring Boot 的支持更友好。
- 数据库:安装 MySQL 8.0,并启动服务。创建一个名为
hotel_management的数据库,字符集建议使用utf8mb4。CREATE DATABASE `hotel_management` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 项目管理工具:安装 Maven 3.6+,并配置
MAVEN_HOME和PATH。# 检查安装是否成功 mvn -v
2.2 前端环境准备
- Node.js:安装 Node.js 16+ 和 npm。建议使用 nvm(Node Version Manager)管理多版本。
# 检查安装是否成功 node -v npm -v - 包管理工具:可以使用 npm,但更推荐使用速度更快的
pnpm或yarn。# 安装 pnpm npm install -g pnpm pnpm -v
2.3 初始化 Spring Boot 后端项目
使用 Spring Initializr(可通过 IDEA 内置或访问 start.spring.io )生成项目骨架。
- Project: Maven Project
- Language: Java
- Spring Boot: 2.7.x
- Project Metadata:
- Group:
com.example - Artifact:
hotel-backend - Packaging: Jar
- Group:
- Dependencies: 添加
Spring Web,Spring Security,MyBatis Framework,MySQL Driver,Lombok。
生成并导入 IDE 后,项目结构大致如下:
hotel-backend ├── src/main/java/com/example/hotelbackend │ ├── controller/ # 控制器层,接收请求,返回响应 │ ├── entity/ # 实体类,对应数据库表 │ ├── mapper/ # MyBatis Mapper 接口 │ ├── service/ # 业务逻辑层接口 │ │ └── impl/ # 业务逻辑层实现 │ ├── config/ # 配置类(如Security, MyBatis-Plus, CORS等) │ └── HotelBackendApplication.java # 主启动类 ├── src/main/resources │ ├── application.yml # 主配置文件 │ └── mapper/ # MyBatis XML映射文件(如果使用) └── pom.xml # Maven依赖配置文件2.4 初始化 Vue 3 前端项目
使用 Vite 官方模板快速创建 Vue 3 项目。
# 使用 npm npm create vue@latest hotel-frontend # 或使用 pnpm pnpm create vue@latest hotel-frontend在创建过程中,根据提示选择需要的特性:
- Add TypeScript?Yes(推荐,增强类型安全)
- Add JSX Support?No
- Add Vue Router for Single Page Application?Yes
- Add Pinia for state management?Yes
- Add Vitest for Unit Testing?No(根据需求)
- Add an End-to-End Testing Solution?No
- Add ESLint for code quality?Yes
创建完成后,进入项目目录并安装 Element Plus 和 Axios。
cd hotel-frontend pnpm install element-plus axios # 或使用 npm npm install element-plus axios安装完成后,前端项目结构如下:
hotel-frontend ├── src/ │ ├── api/ # 封装所有后端API请求 │ ├── assets/ # 静态资源 │ ├── components/ # 可复用组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia状态管理 │ ├── views/ # 页面视图组件 │ ├── App.vue # 根组件 │ └── main.ts # 入口文件 ├── index.html # HTML模板 ├── package.json # 项目依赖和脚本 └── vite.config.ts # Vite配置3. 后端核心功能实现详解
我们将以“客房管理”模块为例,贯穿展示从实体设计到接口暴露的全过程。
3.1 数据库设计与实体类映射
首先,在 MySQL 中创建room表。
CREATE TABLE `room` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `room_number` varchar(20) NOT NULL COMMENT '房间号', `room_type_id` bigint NOT NULL COMMENT '房型ID', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-空闲,1-已预订,2-入住中,3-维修中', `price_per_night` decimal(10,2) NOT NULL COMMENT '每晚价格', `description` varchar(500) DEFAULT NULL COMMENT '房间描述', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_room_number` (`room_number`), KEY `idx_room_type` (`room_type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客房表';对应的 Java 实体类Room.java:
package com.example.hotelbackend.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("room") // 指定表名 public class Room { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private String roomNumber; private Long roomTypeId; private Integer status; // 使用枚举更佳,此处为简化用Integer private BigDecimal pricePerNight; private String description; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }这里使用了 Lombok 的@Data注解自动生成 getter、setter 等方法,以及 MyBatis-Plus 的注解进行 ORM 映射。
3.2 配置 MyBatis-Plus 与数据源
在application.yml中配置数据库连接和 MyBatis-Plus。
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hotel_management?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境关闭 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名(如果启用) logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml # XML映射文件位置创建一个配置类MybatisPlusConfig.java,启用 Mapper 接口扫描和分页插件。
package com.example.hotelbackend.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.example.hotelbackend.mapper") // 扫描Mapper接口 public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }3.3 实现 Mapper、Service 和 Controller
1. Mapper 层:创建RoomMapper.java接口,继承 MyBatis-Plus 的BaseMapper。
package com.example.hotelbackend.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.hotelbackend.entity.Room; public interface RoomMapper extends BaseMapper<Room> { // 可以在此定义自定义的复杂SQL方法 }2. Service 层:创建业务接口RoomService.java及其实现类RoomServiceImpl.java。
// RoomService.java package com.example.hotelbackend.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.example.hotelbackend.entity.Room; public interface RoomService extends IService<Room> { Page<Room> getRoomPage(Page<Room> page, String keyword); } // RoomServiceImpl.java package com.example.hotelbackend.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.hotelbackend.entity.Room; import com.example.hotelbackend.mapper.RoomMapper; import com.example.hotelbackend.service.RoomService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @Service public class RoomServiceImpl extends ServiceImpl<RoomMapper, Room> implements RoomService { @Override public Page<Room> getRoomPage(Page<Room> page, String keyword) { LambdaQueryWrapper<Room> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(keyword)) { // 根据房间号或描述模糊查询 wrapper.like(Room::getRoomNumber, keyword) .or() .like(Room::getDescription, keyword); } // 按创建时间倒序 wrapper.orderByDesc(Room::getCreateTime); return baseMapper.selectPage(page, wrapper); } }3. Controller 层:创建RoomController.java,处理 HTTP 请求。
package com.example.hotelbackend.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.hotelbackend.common.Result; import com.example.hotelbackend.entity.Room; import com.example.hotelbackend.service.RoomService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @RestController @RequestMapping("/api/room") public class RoomController { @Autowired private RoomService roomService; // 新增客房 @PostMapping public Result addRoom(@Valid @RequestBody Room room) { // 业务校验,如房间号是否重复 boolean saved = roomService.save(room); return saved ? Result.success() : Result.error("新增失败"); } // 分页查询客房列表 @GetMapping("/page") public Result getRoomPage(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { Page<Room> page = new Page<>(pageNum, pageSize); Page<Room> roomPage = roomService.getRoomPage(page, keyword); return Result.success(roomPage); } // 根据ID查询客房详情 @GetMapping("/{id}") public Result getRoomById(@PathVariable Long id) { Room room = roomService.getById(id); return room != null ? Result.success(room) : Result.error("客房不存在"); } // 更新客房信息 @PutMapping public Result updateRoom(@Valid @RequestBody Room room) { boolean updated = roomService.updateById(room); return updated ? Result.success() : Result.error("更新失败"); } // 删除客房(逻辑删除) @DeleteMapping("/{id}") public Result deleteRoom(@PathVariable Long id) { boolean removed = roomService.removeById(id); return removed ? Result.success() : Result.error("删除失败"); } }这里使用了统一的Result类封装返回结果,包含code、message和data字段,便于前端处理。
3.4 配置 Spring Security 与 JWT
为了安全,需要配置 Spring Security 和 JWT。首先添加依赖到pom.xml。
<!-- JWT 支持 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>创建 JWT 工具类JwtUtil.java用于生成和解析 Token。然后创建SecurityConfig.java配置类,定义密码编码器、认证管理器、安全过滤链等。核心是配置哪些路径需要认证,哪些可以匿名访问,并添加一个自定义的 JWT 认证过滤器。
4. 前端核心功能实现与联调
前端将使用 Vue 3 + Element Plus 构建客房管理的页面,并通过 Axios 与后端 API 通信。
4.1 封装 Axios 请求实例
在src/api/request.ts中创建 Axios 实例,并配置请求/响应拦截器。
import axios from 'axios'; import { ElMessage } from 'element-plus'; import router from '@/router'; import { useUserStore } from '@/stores/user'; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api', // 从环境变量读取 timeout: 10000, }); // 请求拦截器:在请求头中添加 Token request.interceptors.request.use( (config) => { const userStore = useUserStore(); const token = userStore.token; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器:统一处理错误 request.interceptors.response.use( (response) => { const res = response.data; // 假设后端统一返回 { code: 200, message: 'success', data: ... } if (res.code === 200) { return res.data; } else { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message || 'Error')); } }, (error) => { if (error.response?.status === 401) { ElMessage.warning('登录已过期,请重新登录'); router.push('/login'); } else { ElMessage.error(error.response?.data?.message || error.message || '网络错误'); } return Promise.reject(error); } ); export default request;4.2 实现客房管理 API 模块
在src/api/room.ts中定义所有与客房相关的 API 函数。
import request from './request'; // 定义 Room 类型接口(应与后端实体对应) export interface Room { id?: number; roomNumber: string; roomTypeId: number; status: number; pricePerNight: number; description?: string; } // 分页查询参数 export interface RoomPageParams { pageNum: number; pageSize: number; keyword?: string; } // 分页返回结果 export interface PageResult<T> { records: T[]; total: number; size: number; current: number; } // API 函数 export const roomApi = { // 新增客房 addRoom(data: Room) { return request.post('/room', data); }, // 分页查询客房列表 getRoomPage(params: RoomPageParams) { return request.get<PageResult<Room>>('/room/page', { params }); }, // 根据ID查询客房详情 getRoomById(id: number) { return request.get<Room>(`/room/${id}`); }, // 更新客房信息 updateRoom(data: Room) { return request.put('/room', data); }, // 删除客房 deleteRoom(id: number) { return request.delete(`/room/${id}`); }, };4.3 构建客房列表页面
创建src/views/room/RoomList.vue组件,使用 Element Plus 的ElTable和ElPagination组件。
<template> <div class="room-list-container"> <el-card> <template #header> <div class="card-header"> <span>客房管理</span> <el-button type="primary" @click="handleAdd">新增客房</el-button> </div> </template> <!-- 搜索栏 --> <el-form :inline="true" :model="searchForm"> <el-form-item label="关键词"> <el-input v-model="searchForm.keyword" placeholder="房间号/描述" clearable @clear="handleSearch" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="resetSearch">重置</el-button> </el-form-item> </el-form> <!-- 客房表格 --> <el-table :data="tableData" border stripe v-loading="loading"> <el-table-column prop="roomNumber" label="房间号" width="120" /> <el-table-column prop="roomTypeId" label="房型" width="100"> <template #default="scope"> {{ getRoomTypeName(scope.row.roomTypeId) }} </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="scope"> <el-tag :type="statusTagType(scope.row.status)"> {{ getStatusText(scope.row.status) }} </el-tag> </template> </el-table-column> <el-table-column prop="pricePerNight" label="价格(元/晚)" width="120" /> <el-table-column prop="description" label="描述" show-overflow-tooltip /> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="200" fixed="right"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <div class="pagination-wrapper"> <el-pagination v-model:current-page="pageParams.pageNum" v-model:page-size="pageParams.pageSize" :page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </el-card> <!-- 新增/编辑对话框 --> <room-form-dialog v-model="dialogVisible" :form-data="currentRow" :is-edit="isEdit" @success="handleDialogSuccess" /> </div> </template> <script setup lang="ts"> import { ref, reactive, onMounted } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { roomApi, type Room, type RoomPageParams } from '@/api/room'; import RoomFormDialog from './components/RoomFormDialog.vue'; // 搜索表单 const searchForm = reactive({ keyword: '', }); // 分页参数 const pageParams = reactive<RoomPageParams>({ pageNum: 1, pageSize: 10, }); // 表格数据 const tableData = ref<Room[]>([]); const total = ref(0); const loading = ref(false); // 对话框相关 const dialogVisible = ref(false); const currentRow = ref<Partial<Room>>({}); const isEdit = ref(false); // 获取客房列表 const fetchRoomList = async () => { loading.value = true; try { const params = { ...pageParams, ...searchForm }; const result = await roomApi.getRoomPage(params); tableData.value = result.records; total.value = result.total; } catch (error) { console.error('获取客房列表失败:', error); } finally { loading.value = false; } }; // 搜索 const handleSearch = () => { pageParams.pageNum = 1; // 重置到第一页 fetchRoomList(); }; // 重置搜索 const resetSearch = () => { searchForm.keyword = ''; handleSearch(); }; // 分页大小变化 const handleSizeChange = (val: number) => { pageParams.pageSize = val; fetchRoomList(); }; // 当前页变化 const handleCurrentChange = (val: number) => { pageParams.pageNum = val; fetchRoomList(); }; // 新增 const handleAdd = () => { currentRow.value = {}; isEdit.value = false; dialogVisible.value = true; }; // 编辑 const handleEdit = (row: Room) => { currentRow.value = { ...row }; isEdit.value = true; dialogVisible.value = true; }; // 删除 const handleDelete = (row: Room) => { ElMessageBox.confirm(`确定要删除房间 "${row.roomNumber}" 吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }).then(async () => { try { await roomApi.deleteRoom(row.id!); ElMessage.success('删除成功'); fetchRoomList(); } catch (error) { ElMessage.error('删除失败'); } }); }; // 对话框操作成功回调 const handleDialogSuccess = () => { dialogVisible.value = false; fetchRoomList(); }; // 状态映射(可抽离到常量文件) const statusMap = { 0: { text: '空闲', type: 'success' }, 1: { text: '已预订', type: 'warning' }, 2: { text: '入住中', type: 'primary' }, 3: { text: '维修中', type: 'danger' }, }; const getStatusText = (status: number) => statusMap[status as keyof typeof statusMap]?.text || '未知'; const statusTagType = (status: number) => statusMap[status as keyof typeof statusMap]?.type || 'info'; // 模拟获取房型名称(实际应从store或api获取) const getRoomTypeName = (id: number) => `房型${id}`; onMounted(() => { fetchRoomList(); }); </script> <style scoped> .card-header { display: flex; justify-content: space-between; align-items: center; } .pagination-wrapper { margin-top: 20px; display: flex; justify-content: flex-end; } </style>4.4 配置路由与菜单
在src/router/index.ts中配置路由,将客房管理页面挂载到路由上,并在主布局中通过菜单导航访问。
5. 项目运行、验证与常见问题排查
完成前后端核心代码后,需要启动项目并进行功能验证。
5.1 启动后端服务
- 确保 MySQL 服务已启动,且
hotel_management数据库已创建。 - 在
application.yml中检查数据库连接信息是否正确。 - 在 IDEA 中找到主启动类
HotelBackendApplication.java,右键运行。 - 观察控制台日志,确认没有启动错误,并看到类似
Tomcat started on port(s): 8080的日志。
5.2 启动前端开发服务器
- 在终端中进入
hotel-frontend目录。 - 运行
pnpm dev或npm run dev。 - 控制台会输出本地访问地址,通常是
http://localhost:5173。
5.3 功能验证流程
- 访问前端:在浏览器打开
http://localhost:5173,应能看到登录页或主界面。 - 登录系统:使用预设的管理员账号(需要在后端初始化数据或通过数据库插入)登录。
- 导航至客房管理:点击侧边栏菜单,进入客房管理页面。
- 测试增删改查:
- 查询:页面加载后应显示客房列表。
- 新增:点击“新增客房”,填写表单并提交,列表应刷新显示新数据。
- 编辑:点击某行数据的“编辑”按钮,修改信息后提交,列表数据应更新。
- 删除:点击“删除”按钮,确认后该行数据应从列表中消失(后端为逻辑删除)。
- 检查网络请求:打开浏览器开发者工具的“网络(Network)”标签,查看每次操作发出的 API 请求和响应,确认状态码为 200,数据格式正确。
5.4 常见问题与排查路径
在联调过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 前端访问后端 API 报跨域错误 (CORS) | 浏览器安全策略阻止了不同源(域名、端口、协议)的请求。 | 1. 查看浏览器控制台 Network 标签,请求状态为CORS error或OPTIONS 403。2. 检查后端是否配置了 CORS。 | 在后端SecurityConfig或单独配置类中,添加 CORS 配置,允许前端源。 |
| 前端请求返回 401 (Unauthorized) | 1. 未登录或 Token 过期。 2. Token 未正确携带或格式错误。 3. 请求的接口需要特定权限。 | 1. 检查前端请求头Authorization是否包含Bearer <token>。2. 检查后端 Token 解析是否失败(密钥不匹配、过期)。 3. 查看后端 Security 配置,该接口路径是否需要认证。 | 1. 重新登录获取新 Token。 2. 检查前后端 JWT 密钥是否一致。 3. 在 Security 配置中放行该接口或为用户添加对应权限。 |
| 前端请求返回 404 (Not Found) | 1. 后端 API 路径错误。 2. 后端 Controller 未正确映射或未启动。 | 1. 核对前端请求 URL 和后端@RequestMapping路径。2. 检查后端启动日志,确认 Controller 已被扫描加载。 | 1. 修正前端请求 URL 或后端注解路径。 2. 确保主启动类包扫描范围能覆盖到 Controller。 |
| 数据库连接失败 | 1. MySQL 服务未启动。 2. 连接 URL、用户名、密码错误。 3. 驱动版本不匹配。 | 1. 检查 MySQL 服务状态。 2. 核对 application.yml中的连接信息。3. 查看启动日志中的具体异常信息。 | 1. 启动 MySQL 服务。 2. 修正配置文件。 3. 检查 pom.xml中 MySQL 驱动版本。 |
| MyBatis-Plus 插入/更新时字段为 null | 1. 实体类字段名与数据库列名未正确映射(下划线转驼峰默认开启)。 2. 字段填充策略未生效。 | 1. 检查@TableField注解的value属性是否指定了列名。2. 检查是否实现了 MetaObjectHandler来处理字段自动填充。 | 1. 使用@TableField(value = "column_name")明确指定映射。2. 创建配置类实现 MetaObjectHandler接口。 |
| 前端页面样式错乱或 Element Plus 组件未渲染 | 1. Element Plus 未正确引入或注册。 2. Vue 组件未正确导入或注册。 | 1. 检查main.ts中是否正确引入了 Element Plus 及其样式。2. 检查浏览器控制台是否有 JS 错误。 | 1. 参照 Element Plus 官方文档,在main.ts中完成全局注册或按需导入。2. 根据控制台错误信息修复组件导入问题。 |
6. 项目部署与生产环境考量
学习环境运行成功后,若想部署到服务器或用于生产演示,还需要考虑以下方面。
6.1 后端打包与部署
- 打包:在项目根目录执行
mvn clean package -DskipTests,会在target目录生成可执行的 JAR 文件(如hotel-backend-0.0.1-SNAPSHOT.jar)。 - 环境配置:生产环境的数据库密码、JWT 密钥等敏感信息不应写在代码中。应使用
application-prod.yml文件,并通过--spring.profiles.active=prod启动参数激活。或者使用环境变量、配置中心。 - 启动:在服务器上使用
nohup java -jar hotel-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &命令后台启动。 - 反向代理:通常使用 Nginx 作为反向代理,将域名请求转发到后端服务的 8080 端口,并处理 SSL 证书(HTTPS)。
6.2 前端打包与部署
- 打包:执行
pnpm build或npm run build,会在dist目录生成静态资源文件。 - 部署:将
dist目录下的所有文件上传到 Web 服务器(如 Nginx、Apache)的静态资源目录。 - Nginx 配置:配置 Nginx,将前端请求代理到后端 API,并处理 History 路由模式。
server { listen 80; server_name your_domain.com; location / { root /path/to/your/dist; index index.html; try_files $uri $uri/ /index.html; # 支持 Vue Router 的 history 模式 } location /api/ { proxy_pass http://localhost:8080/api/; # 代理后端API proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
6.3 生产环境最佳实践建议
- 数据库:
- 为生产数据库创建专属用户,并授予最小必要权限。
- 定期备份数据库。
- 对核心表(如订单、账单)建立合适的索引以优化查询性能。
- 安全:
- JWT 密钥:使用强随机字符串,并定期更换。
- API 防护:对敏感操作(如删除、支付)增加二次确认或操作日志。
- 输入校验:前后端均需进行严格的输入校验和过滤,防止 SQL 注入和 XSS 攻击。
- 依赖安全:定期使用
mvn dependency:check或npm audit检查依赖库的安全漏洞。
- 日志与监控:
- 配置日志框架(如 Logback)将日志按级别输出到文件,便于排查问题。
- 集成 Spring Boot Actuator 暴露健康检查端点,方便监控应用状态。
- 考虑使用 Prometheus + Grafana 进行应用性能监控。
- 性能:
- 对频繁查询且变化不频繁的数据(如房型、城市列表)使用 Redis 缓存。
- 前端对图片等静态资源进行压缩,并使用 CDN 加速。
通过以上步骤,你不仅完成了一个可运行的酒店管理系统,更实践了从技术选型、环境搭建、模块开发、前后端联调到部署上线的完整流程。这个项目可以作为你深入理解 Spring Boot 和 Vue 3 全栈开发的坚实基础,你可以在此基础上继续扩展功能,如集成工作流引擎处理复杂的订单状态流转,或加入更精细的权限管理模型(如 RBAC),使其更贴近真实的商业系统。