酒店管理系统毕业设计:SpringBoot+MyBatis+Shiro全链路实现与避坑指南
简介这是一套面向高校计算机专业毕业设计的酒店管理系统完整项目源码采用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 Shiro2.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 或 11SpringBoot 2.x 对这两个版本兼容最好MySQL 用 5.7 或 8.0Maven 用 3.6 以上。版本对不上是新手翻车的高频原因尤其是 MySQL 8.0 的驱动类名和连接 URL 跟 5.7 有差异。!-- pom.xml 核心依赖片段 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.6/version !-- 2.x 稳定版兼容 JDK 8/11 -- /parent dependencies !-- Web 层提供 RESTful 接口和 MVC 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 整合 SpringBoot -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency !-- Shiro 权限框架 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-web-starter/artifactId version1.11.0/version /dependency !-- MySQL 驱动8.0 用 com.mysql.cj.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.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?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghaiuseSSLfalse 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段用 TINYINT 而不是 ENUM是因为 ENUM 在 MyBatis 映射时容易出问题而且后续扩展状态比如加清洁中需要改表结构。用整数配合常量类或枚举类在 Java 层管理灵活得多。3. 核心业务模块实现预订、入住、退房全链路3.1 客房预订的并发控制预订模块是整个系统里技术含量最高的部分因为它涉及并发问题。假设两个前台同时给同一个房间下单如果不做控制就会出现超卖。常见做法有两种悲观锁SELECT ... FOR UPDATE和乐观锁版本号机制。这套系统我建议用乐观锁因为酒店预订的并发量不会特别高乐观锁性能更好。// RoomMapper.xml 中的乐观锁更新 update idupdateRoomStatusWithVersion 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。如果两个线程同时读到 status0、version1只有一个能更新成功version 变成 2另一个 affected0返回预订失败请重试。这就是乐观锁的核心思路——不加锁但更新时检查数据有没有被别人改过。3.2 入住登记与退房结算入住登记的逻辑比预订简单但有几个业务细节容易漏。第一入住时要更新房间状态为已入住status2同时更新订单状态为已入住。第二退房时要计算实际住宿天数和费用如果客户延迟退房超过中午 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); // 无权限跳转 MapString, 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 rootlocalhost。原因通常是application.yml里的密码跟本地 MySQL 实际密码不一致或者 MySQL 8.0 的caching_sha2_password认证插件跟老版本驱动不兼容。解决办法先确认密码无误如果还报错在 MySQL 里执行ALTER USER rootlocalhost 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-size1020最大连接数按并发量调整minimum-idle105最小空闲连接避免资源浪费connection-timeout3000010000获取连接超时单位毫秒idle-timeout600000300000空闲连接回收时间max-lifetime18000001200000连接最大存活时间配置写在application.yml的spring.datasource.hikari下面。max-lifetime建议比 MySQL 的wait_timeout小一点否则连接被数据库端断开后连接池还以为连接可用拿到的就是死连接。5.3 答辩演示的准备工作最后说一个血泪经验答辩前一定要在演示环境完整走一遍流程包括登录、预订、入住、退房、查询报表。我见过太多同学本地跑得好好的到答辩教室换了台电脑就各种报错——数据库没导、端口被占、JDK 版本不对。建议提前准备一个start.bat或start.sh脚本把启动命令和常见问题排查步骤写进去。另外数据库导出 SQL 文件时记得勾选包含建表语句和包含数据别只导结构不导数据演示时一个客户都没有就尴尬了。从那以后我每次交付这类系统都会强制走一遍换机复现流程——在另一台电脑上从零部署把所有依赖和步骤记下来。希望帮到你。本文还有配套的精品资源点击获取