简介:这是一套面向高校计算机专业毕业设计的酒店管理系统完整项目源码,采用Java语言配合SpringBoot、MyBatis与Shiro技术栈开发,适合正在准备毕设或需要企业级Web项目练手的开发者参考。系统按用户端与后台管理端双端设计,覆盖房间预订、房态维护、房型配置、订单流转及用户权限管理等核心业务,能帮助读者理解从需求到落地的完整实现思路。压缩包共1086个文件,约15.45MB,包含39个Java源文件、115个XML配置、66个JavaScript脚本以及大量CSS、HTML、图片与字体资源,前端页面与后端逻辑分层清晰,便于按模块查阅。目前已有1254人学习下载,读者可从中获取可运行的工程结构、数据库映射配置、Shiro认证授权示例及订单状态处理逻辑,适合作为毕设选题参考或二次开发的基础模板。
1. 酒店管理系统毕业设计系统:从开题到答辩的完整落地路径
如果你正在为计算机毕业设计选题发愁,又恰好被导师分到了"信息管理系统"这个方向,那这套基于 Java SpringBoot + MyBatis + Shiro 的酒店管理系统,大概率能让你少熬几个通宵。它不是那种只有增删改查的玩具项目,而是覆盖了客房预订、入住登记、退房结算、房态管理、客户信息维护等酒店核心业务链路的完整系统。技术栈选型也很"毕业设计友好":SpringBoot 负责快速搭建后端服务,MyBatis 做数据持久化,Shiro 处理登录认证和权限控制,前端通常是 Thymeleaf 或 Vue 配合 Layui/ElementUI。适合谁?适合软件工程、计算机科学与技术、信息管理等相关专业,需要一套能跑通、能讲清、能改动的毕设系统的同学。下面我从技术架构、环境搭建、核心模块实现、避坑经验到进阶技巧,把这份资源拆开讲透。
2. 技术选型与架构拆解:为什么是 SpringBoot + MyBatis + Shiro
2.1 三层架构的实际落地方式
这套系统采用的是典型的 MVC 三层架构,但具体落地时有几个值得注意的设计决策。Controller 层负责接收前端请求并做参数校验,Service 层承载业务逻辑(比如"预订时检查房态是否可用"这种判断),Mapper 层通过 MyBatis 与 MySQL 交互。和常见的 JPA/Hibernate 方案不同,MyBatis 把 SQL 写在 XML 映射文件里,这对毕业设计来说反而是优势——答辩时老师问"你这个查询怎么实现的",你可以直接打开 XML 文件指给他看,比解释 Hibernate 的自动生成 SQL 要直观得多。
Shiro 的集成方式也值得说一下。很多毕设项目用 Spring Security,配置复杂、学习曲线陡,而 Shiro 的配置更轻量。这套系统里 Shiro 主要做两件事:一是登录认证(Authentication),二是角色权限控制(Authorization)。比如管理员可以管理所有房间和订单,前台只能操作入住退房,这种权限隔离通过 Shiro 的 Realm 和过滤器链实现。
2.2 环境搭建与依赖配置
拿到源码后第一步是配环境。我一般会先确认三个版本:JDK 用 1.8 或 11(SpringBoot 2.x 对这两个版本兼容最好),MySQL 用 5.7 或 8.0,Maven 用 3.6 以上。版本对不上是新手翻车的高频原因,尤其是 MySQL 8.0 的驱动类名和连接 URL 跟 5.7 有差异。
<!-- pom.xml 核心依赖片段 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.6</version> <!-- 2.x 稳定版,兼容 JDK 8/11 --> </parent> <dependencies> <!-- Web 层:提供 RESTful 接口和 MVC 支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 整合 SpringBoot --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <!-- Shiro 权限框架 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.11.0</version> </dependency> <!-- MySQL 驱动:8.0 用 com.mysql.cj.jdbc.Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>这段配置里最容易出问题的是 Shiro 的 starter 版本。1.11.0 是较新的稳定版,如果你用的是 1.4 以下的老版本,配置方式完全不同(老版本需要手动注册 Filter 和 SecurityManager Bean)。另外 MySQL 驱动从 5.x 升到 8.x 后,application.yml里的 URL 要加时区参数,否则启动时报时区错误。
# application.yml 数据库与 MyBatis 配置 spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml # XML 映射文件路径 type-aliases-package: com.hotel.entity # 实体类包名 configuration: map-underscore-to-camel-case: true # 下划线转驼峰,避免字段映射失败map-underscore-to-camel-case这个配置强烈建议打开。数据库字段通常用room_number这种下划线命名,Java 实体类用roomNumber驼峰命名,不开这个配置的话,查询结果里roomNumber永远是 null,而且不报错——这种静默失败排查起来很折磨人。
2.3 数据库表结构设计要点
酒店管理系统的表结构设计直接影响后续业务逻辑的复杂度。核心表大概有这几张:room(房间信息)、room_type(房型)、customer(客户信息)、order(订单)、check_in(入住记录)、user(系统用户)、role(角色)、permission(权限)。其中订单表和入住记录表的关系需要特别注意——订单是预订行为,入住记录是实际入住行为,两者是一对多关系(一个订单可能对应多次入住,比如续住)。
-- 房间表核心字段 CREATE TABLE `room` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `room_number` VARCHAR(10) NOT NULL UNIQUE COMMENT '房间号', `room_type_id` INT NOT NULL COMMENT '房型ID', `status` TINYINT DEFAULT 0 COMMENT '0-空闲 1-已预订 2-已入住 3-维修中', `floor` INT COMMENT '楼层', FOREIGN KEY (`room_type_id`) REFERENCES `room_type`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status字段用 TINYINT 而不是 ENUM,是因为 ENUM 在 MyBatis 映射时容易出问题,而且后续扩展状态(比如加"清洁中")需要改表结构。用整数配合常量类或枚举类在 Java 层管理,灵活得多。
3. 核心业务模块实现:预订、入住、退房全链路
3.1 客房预订的并发控制
预订模块是整个系统里技术含量最高的部分,因为它涉及并发问题。假设两个前台同时给同一个房间下单,如果不做控制,就会出现超卖。常见做法有两种:悲观锁(SELECT ... FOR UPDATE)和乐观锁(版本号机制)。这套系统我建议用乐观锁,因为酒店预订的并发量不会特别高,乐观锁性能更好。
// RoomMapper.xml 中的乐观锁更新 <update id="updateRoomStatusWithVersion"> UPDATE room SET status = #{newStatus}, version = version + 1 WHERE id = #{roomId} AND status = #{expectedStatus} AND version = #{version} </update>// RoomService.java 预订逻辑 @Transactional public Result bookRoom(Integer roomId, Integer customerId, Date checkInDate) { Room room = roomMapper.selectById(roomId); if (room.getStatus() != 0) { return Result.error("该房间已被预订或入住"); } int affected = roomMapper.updateRoomStatusWithVersion( roomId, 1, 0, room.getVersion() ); if (affected == 0) { return Result.error("预订失败,请重试"); // 版本号不匹配,说明被其他人抢先了 } // 创建订单记录 Order order = new Order(); order.setRoomId(roomId); order.setCustomerId(customerId); order.setCheckInDate(checkInDate); order.setStatus(0); // 0-待入住 orderMapper.insert(order); return Result.success("预订成功"); }这段代码的关键在于updateRoomStatusWithVersion的 WHERE 条件同时校验了status和version。如果两个线程同时读到 status=0、version=1,只有一个能更新成功(version 变成 2),另一个 affected=0,返回"预订失败,请重试"。这就是乐观锁的核心思路——不加锁,但更新时检查数据有没有被别人改过。
3.2 入住登记与退房结算
入住登记的逻辑比预订简单,但有几个业务细节容易漏。第一,入住时要更新房间状态为"已入住"(status=2),同时更新订单状态为"已入住"。第二,退房时要计算实际住宿天数和费用,如果客户延迟退房(超过中午 12 点),是否加收半天房费,这个规则要在代码里体现。
// CheckInService.java 退房结算 public BigDecimal checkout(Integer orderId) { Order order = orderMapper.selectById(orderId); CheckIn checkIn = checkInMapper.selectByOrderId(orderId); // 计算住宿天数:退房日期 - 入住日期 long days = ChronoUnit.DAYS.between( checkIn.getCheckInTime().toLocalDate(), LocalDate.now() ); if (days == 0) days = 1; // 当天入住当天退,算一天 // 延迟退房判断:超过中午12点加收半天 BigDecimal totalFee = order.getRoomPrice() .multiply(BigDecimal.valueOf(days)); if (LocalTime.now().isAfter(LocalTime.NOON)) { totalFee = totalFee.add(order.getRoomPrice() .multiply(BigDecimal.valueOf(0.5))); } // 更新房间状态为空闲 roomMapper.updateStatus(order.getRoomId(), 0); // 更新订单状态为已完成 orderMapper.updateStatus(orderId, 2); return totalFee; }ChronoUnit.DAYS.between是 Java 8 时间 API 的用法,比老式的Calendar计算天数要准确得多。注意days == 0的处理——如果客户上午入住下午退房,between返回 0,但实际应该收一天房费。延迟退房的判断用LocalTime.now().isAfter(LocalTime.NOON),简单直接。
3.3 Shiro 权限控制的实际配置
Shiro 的配置分两块:一是ShiroConfig类,定义过滤器链和安全管理器;二是自定义Realm,从数据库加载用户权限信息。
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); factory.setLoginUrl("/login"); // 未登录跳转 factory.setUnauthorizedUrl("/403"); // 无权限跳转 Map<String, String> filterChain = new LinkedHashMap<>(); filterChain.put("/login", "anon"); // 登录页放行 filterChain.put("/static/**", "anon"); // 静态资源放行 filterChain.put("/admin/**", "roles[admin]"); // 管理员专属 filterChain.put("/**", "authc"); // 其余需登录 factory.setFilterChainDefinitionMap(filterChain); return factory; } }LinkedHashMap的顺序很重要——Shiro 按插入顺序匹配过滤器链,如果把/**放在前面,后面的规则永远不会生效。这是新手最容易踩的坑之一,表现为"明明配了放行规则,但访问登录页还是被拦截"。
4. 避坑与排查:那些让我熬夜的常见问题
4.1 启动报错 "Access denied for user"
现象:项目启动时控制台抛出java.sql.SQLException: Access denied for user 'root'@'localhost'。原因通常是application.yml里的密码跟本地 MySQL 实际密码不一致,或者 MySQL 8.0 的caching_sha2_password认证插件跟老版本驱动不兼容。解决办法:先确认密码无误,如果还报错,在 MySQL 里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';把认证插件改回兼容模式。
4.2 MyBatis 查询返回 null 但 SQL 能查到数据
现象:直接在 MySQL 客户端执行 SQL 有结果,但程序里查出来字段全是 null。原因几乎都是字段名和实体类属性名没对上,且没开map-underscore-to-camel-case。解决:在application.yml的 MyBatis 配置里加上这个参数,或者在 XML 里手动写<resultMap>做映射。我一般两个都做,双保险。
4.3 Shiro 登录后仍然跳回登录页
现象:输入正确的用户名密码,登录接口返回成功,但访问其他页面又被重定向到登录页。原因通常是 Session 管理问题——前后端分离项目里,前端没带上 Cookie 或者后端没配置跨域 Session 共享。解决:如果是前后端分离,在 ShiroConfig 里配置DefaultWebSessionManager并设置sessionIdUrlRewritingEnabled(false),同时前端请求要带withCredentials: true。
4.4 订单状态和房间状态不同步
现象:退房后房间状态还是"已入住",新客户无法预订。原因:退房逻辑里更新了订单状态但忘了更新房间状态,或者两个更新不在同一个事务里,一个成功一个失败。解决:在 Service 方法上加@Transactional注解,确保两个更新要么都成功要么都回滚。另外建议写一个定时任务,每天凌晨扫描异常状态的数据做修复。
4.5 前端页面样式丢失
现象:后端接口正常,但页面排版全乱,CSS/JS 加载 404。原因:SpringBoot 静态资源默认放在src/main/resources/static/目录下,如果前端文件放在其他位置,需要在配置里指定资源映射路径。解决:在application.yml里加spring.web.resources.static-locations: classpath:/static/,classpath:/public/,或者把前端文件挪到正确目录。
5. 进阶技巧:从能跑到能答辩的最后一公里
5.1 用 AOP 做统一日志记录
答辩时老师经常会问"你怎么做系统监控和日志的"。与其现场编,不如提前加一个 AOP 切面,记录每个接口的调用时间、参数和返回值。代码量不大,但答辩时是个加分项。
@Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); @Around("execution(* com.hotel.controller..*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); String method = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); log.info("请求方法: {}, 参数: {}", method, Arrays.toString(args)); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; log.info("返回: {}, 耗时: {}ms", result, cost); return result; } }这个切面拦截controller包下所有方法,记录请求参数、返回结果和耗时。@Around环绕通知的好处是可以在方法执行前后都插入逻辑,joinPoint.proceed()是实际调用目标方法。耗时统计对排查慢接口很有用,答辩时如果老师问"你怎么知道哪个接口慢",这就是答案。
5.2 数据库连接池参数调优
默认的 HikariCP 连接池配置在毕设场景下够用,但如果答辩时老师问"并发量上来怎么办",你可以提前调几个参数。
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
| maximum-pool-size | 10 | 20 | 最大连接数,按并发量调整 |
| minimum-idle | 10 | 5 | 最小空闲连接,避免资源浪费 |
| connection-timeout | 30000 | 10000 | 获取连接超时,单位毫秒 |
| idle-timeout | 600000 | 300000 | 空闲连接回收时间 |
| max-lifetime | 1800000 | 1200000 | 连接最大存活时间 |
配置写在application.yml的spring.datasource.hikari下面。max-lifetime建议比 MySQL 的wait_timeout小一点,否则连接被数据库端断开后,连接池还以为连接可用,拿到的就是死连接。
5.3 答辩演示的准备工作
最后说一个血泪经验:答辩前一定要在演示环境完整走一遍流程,包括登录、预订、入住、退房、查询报表。我见过太多同学本地跑得好好的,到答辩教室换了台电脑就各种报错——数据库没导、端口被占、JDK 版本不对。建议提前准备一个start.bat或start.sh脚本,把启动命令和常见问题排查步骤写进去。另外,数据库导出 SQL 文件时记得勾选"包含建表语句"和"包含数据",别只导结构不导数据,演示时一个客户都没有就尴尬了。
从那以后我每次交付这类系统,都会强制走一遍"换机复现"流程——在另一台电脑上从零部署,把所有依赖和步骤记下来。希望帮到你。
本文还有配套的精品资源,点击获取