SSM框架开发酒店管理系统:从分层架构到事务落地实践
2026/9/16 13:21:41 网站建设 项目流程

简介:这是一份基于SSM(Spring+Spring MVC+MyBatis)框架的酒店管理系统毕业设计源码包,采用JavaWeb技术栈,面向计算机、通信、人工智能、自动化等专业的本专科学生及教师,可用于毕业设计、课程设计或项目实训。项目为个人毕设作品,答辩评分98分,代码经调试可运行,并附带文档说明,能帮助学习者快速掌握SSM整合、前后台业务开发、数据库设计等核心技能。包内共包含2000个文件,以Java源码、JSP页面、HTML、CSS、JavaScript为主,辅以SQL数据库脚本、XML配置文件、PNG/GIF图片素材等,压缩包大小约9.02MB,目录结构清晰,便于按模块查阅与二次开发。项目内置数据库建表脚本与核心配置,可快速完成本地环境部署;前端采用H-ui、EasyUI等组件构建,界面简洁、贴近真实酒店业务场景。目前已有255人浏览学习,适合作为毕设起步模板或进阶练手项目,基础较好的读者还可在此基础上扩展客房预订、会员管理、营收统计等功能,学习借鉴价值较高。

1. 毕业设计选SSM酒店管理系统,到底在选什么

SSM 被很多文章判为“过时”,但放在毕业设计这个场景里,这个结论并不成立。指导老师能一眼看懂 Spring、SpringMVC、MyBatis 三分天下的代码结构,答辩时可以顺着请求链路连续追问;遇到编译错误或运行期异常,几乎都能在博客里搜到现成的解决方案。酒店管理系统作为题目,业务边界清楚:订房、入住、退房、结账、房间状态变更,每件事都要求数据一致,恰好能把 SSM 里最值得写进论文的事务控制、分层解耦和 SQL 映射完整展示。如果你需要交付源码加文档说明,又希望短期达到“拿得出手、扛得住追问”的完成度,这个题目的性价比很高。

2. SSM框架分层与酒店业务模块的映射关系

2.1 Spring管对象、SpringMVC管请求、MyBatis管SQL

SSM 是 Spring、SpringMVC、MyBatis 三个框架的缩写。它解决的问题很具体:Spring 管理对象创建和依赖注入,Service 实例、Mapper 实例都由容器统一管理,AOP 才能把事务、日志这类横切逻辑织入进去。SpringMVC 管理 HTTP 请求,把 URL 映射到 Controller 方法、把请求参数绑定到 Java 对象、再把返回值渲染成 JSP 或 JSON。MyBatis 管理 Java 方法与 SQL 的关系,Mapper XML 里写真正执行的语句,框架负责参数注入和结果集映射。

这三层看似各管一段,真正把项目拼起来的是 Spring 的集成能力。MyBatis 通过 mybatis-spring 适配器把 SqlSessionFactory 交给 Spring 管理,SpringMVC 的 DispatcherServlet 由 Web 容器在启动时初始化。一条典型请求路径是:JSP 表单提交 -> DispatcherServlet -> Controller -> Service -> Mapper -> MySQL。毕业设计里最容易丢分的就是这条链的衔接处,比如配置文件漏了 component-scan 导致 Controller 或 Service 扫不到,或者注解与 XML 混用导致同一个 Bean 被实例化两份。

提示:约定 Controller 不写业务逻辑,Service 管事务边界,Mapper 只管 SQL。答辩时能把这句话讲明白,胜过贴大段代码。

2.2 酒店管理系统的核心模块与分层落点

酒店管理系统业务面很宽,毕业设计需要覆盖的通常集中在 5 个模块:房间管理、客户管理、入住与退房管理、结算管理、权限与员工管理。房间管理负责房型、门牌号和房间状态维护;客户管理维护客人的基本信息和联系方式;入住与退房是核心业务,涉及订单创建、状态切换和房间状态联动;结算管理在退房时计算房费,可能叠加加床、餐饮、赔偿等费用;权限管理区分管理员和前台员工。

这 5 个模块映射到 SSM 分层上是规律的。Controller 层只做参数接收、基础校验和页面跳转,以房间管理为例,RoomController 的 list 方法只需从 Service 查询分页结果、放进 Model、返回 room/list.jsp。业务集中在 Service,例如“查询所有空闲房间”“修改房间状态”。真正写 SQL 的是 RoomMapper.xml,一条 update 语句只做一件事。

下面把模块、核心操作和涉及的表对应起来,后面写代码时不容易跑偏:

模块典型操作核心Service方法核心表
房间管理新增房间、改状态、查空闲addRoom / updateRoomStatus / listFreeRoomsroom
客户管理新增客户、按手机号查找addCustomer / findByPhonecustomer
入住退房办理入住、退房结算checkIn / checkOutlive_record
结算管理计算房费、生成账单calcBill / payBillbill
权限管理登录、员工增删、改密login / addEmployee / resetPasswordemployee

注意 live_record 和 bill 这两张表是关键。不要只用一张 orders 表把入住和结算混在一起,退房时关联数据会很难查,答辩也会被追问“入住记录和账单为什么拆开”。Service 方法命名直接对应业务流程中的动词,checkIn、checkOut、calcBill 这类名字,比统一叫 save、update、delete 直观得多。

从模块到分层还有一个容易忽略的点:entity、dto、vo 要分开。entity 和数据库表字段一一对应,dto 接收前端表单,vo 返回给页面展示。如果图省事全用 entity,页面多传一个字段就可能覆盖不该被覆盖的数据,这个坑在第 3 章会看到具体后果。

3. 用SSM落地酒店管理系统——环境搭建与关键代码实现

3.1 最小工程结构:从pom.xml和web.xml起步

工具链选型先说结论:JDK 8 或 11,Maven 构建,Tomcat 8.5 或 9,MySQL 5.7。Spring 用 5.x 稳定系列,MyBatis 用 3.5.x,mybatis-spring 用 2.x。这套组合在 SSM 毕业设计里最稳,网上能搜到的报错基本都落在这个版本范围内。

工程目录按 Maven 约定创建:src/main/java 放 Java 代码,src/main/resources 放 Spring 和 MyBatis 配置文件,src/main/webapp 放 JSP、CSS、JS 和 web.xml。包结构建议 com.xxx.hotel 下分 controller、service、mapper、entity、dto 五个子包,service 下再分 impl 包放实现类。

pom.xml 里去掉测试和打包相关冗余配置,核心依赖如下:

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.27</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.23</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

依赖里有几个容易忽视的匹配点。spring-webmvc 只引了 webmvc,但 spring-core、spring-jdbc 等同组模块必须和它同版本,否则运行期抛 NoSuchMethodError。mybatis-spring 2.0.7 对 Spring 5.3 的兼容性比较可靠。mysql-connector-java 用 8.0.x 时,连接地址必须带 serverTimezone 参数,否则首次建连就报错。druid 可以换成 c3p0 或 DriverManagerDataSource,但 druid 对初始化失败的报错信息更明确。

web.xml 负责启动 Spring 容器和 SpringMVC 分发器,用 Servlet 3.0 时代的经典写法兼容性最好:

<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

contextConfigLocation 指定父容器配置,放数据源、SqlSessionFactory、事务管理和 Service 扫描。DispatcherServlet 初始化参数里的 spring-mvc.xml 是子容器配置,只放 Controller 扫描和视图解析器。父子容器的区分是 SSM 最重要的设计:Controller 在子容器,Service 在父容器,事务切面只对父容器中的 Bean 生效。如果把 Service 扫到子容器里,事务会静默失效。

3.2 “办理入住”请求的完整调用链

业务场景:前台选择空闲房间和客人,提交入住表单,系统要做两步操作——插入入住记录、把房间状态从空闲改成入住中。这两步要么都成功,要么都回滚。

先看 Controller,只处理参数接收和视图跳转:

@Controller @RequestMapping("/live") public class CheckinController { @Autowired private LiveRecordService liveRecordService; @PostMapping("/checkin") public String checkin(LiveRecordForm form, Model model) { boolean ok = liveRecordService.checkIn( form.getRoomId(), form.getCustomerId(), form.getDays()); if (!ok) { model.addAttribute("error", "房间状态已变化,请刷新后重试"); return "live/checkin"; } return "redirect:/live/list"; } }

form 是专门接收页面参数的 DTO,不要用 entity 直接接收请求参数。页面多传来的字段可能覆盖实体里不该改的数据,比如把 roomStatus 也传进来就会绕过业务逻辑。checkIn 返回 boolean,调用方只关心成败,这是 Service 边界清晰的做法。

Service 实现扛起事务:

@Service public class LiveRecordServiceImpl implements LiveRecordService { @Autowired private LiveRecordMapper liveRecordMapper; @Autowired private RoomMapper roomMapper; @Transactional(rollbackFor = Exception.class) @Override public boolean checkIn(Integer roomId, Integer customerId, Integer days) { if (roomMapper.getStatus(roomId) != RoomStatus.FREE) { return false; } int roomLines = roomMapper.updateStatus(roomId, RoomStatus.FREE, RoomStatus.OCCUPIED); if (roomLines == 0) { return false; } LiveRecord record = new LiveRecord(); record.setRoomId(roomId); record.setCustomerId(customerId); record.setExpectDays(days); record.setStatus(OrderStatus.ACTIVE); liveRecordMapper.insert(record); return true; } }

@Transactional 的 rollbackFor 必须写成 Exception.class,Spring 默认只在抛出 RuntimeException 时才回滚。如果 insert 外面套了 try-catch 又不往外抛,事务会认为方法正常结束,出现入住记录写了但房间状态没改的结果。这里的 updateStatus 是关键。

Mapper 层的 SQL 带条件更新:

<update id="updateStatus"> UPDATE room SET room_status = #{targetStatus} WHERE room_id = #{roomId} AND room_status = #{currentStatus} </update> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO live_record (room_id, customer_id, expect_days, order_status, create_time) VALUES (#{roomId}, #{customerId}, #{expectDays}, #{status}, NOW()) </insert>

updateStatus 把 currentStatus 放进 WHERE 条件,两个请求同时执行时只有第一个成功,第二个影响行数为 0,Service 里 roomLines == 0 返回 false,避免同一间房被同时办理入住。insert 的 useGeneratedKeys 把自增主键回填到 record.id,后续退房时用这个 id 关联账单。这一步解决的就是酒店系统最敏感的超售问题。

4. 数据库建模、订单状态流转与高频踩坑

4.1 酒店核心表设计:room、customer、live_record、bill怎么关联

数据库设计直接决定 Service 层代码能写多干净,建议先看建表 SQL 再回头看代码:

CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, room_type VARCHAR(20) NOT NULL COMMENT 'single/double/suite', room_status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1预订 2入住 3维修', price DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE customer ( customer_id INT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE live_record ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, room_id INT NOT NULL, customer_id INT NOT NULL, expect_days INT NOT NULL, order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0进行中 1已退房 2已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, check_out_time DATETIME, KEY idx_room (room_id), KEY idx_customer (customer_id), CONSTRAINT fk_live_room FOREIGN KEY (room_id) REFERENCES room(room_id), CONSTRAINT fk_live_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE bill ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, pay_time DATETIME, CONSTRAINT fk_bill_record FOREIGN KEY (record_id) REFERENCES live_record(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

room_status 用 TINYINT 加 COMMENT,不用 VARCHAR,数据库体积小且查询快。live_record 的 order_no 是业务可读的唯一编号,不要把自增 id 直接暴露给用户。外键在这里建上,是为了让数据完整性约束在数据库层兜底,答辩时不会出现“系统里能查出属于不存在房间的入住记录”这类漏洞。

4.2 订单状态机:用常量类管理而不是散落的魔法值

入住、退房、取消之间有明确的合法流转路径,不在代码里硬编码 if 判断,用一个常量类统一管理。

public final class OrderStatus { public static final int ACTIVE = 0; // 进行中 public static final int CHECKED_OUT = 1; // 已退房 public static final int CANCELED = 2; // 已取消 private static final Map<Integer, List<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(ACTIVE, Arrays.asList(CHECKED_OUT, CANCELED)); TRANSITIONS.put(CHECKED_OUT, Collections.emptyList()); TRANSITIONS.put(CANCELED, Collections.emptyList()); } public static boolean canTransit(int from, int to) { List<Integer> targets = TRANSITIONS.get(from); return targets != null && targets.contains(to); } }

这个状态机只允许 0 -> 1、0 -> 2 两条路径,把 1 -> 0、2 -> 1 这类非法切换直接挡在 Service 方法之外。后续加“换房”状态时只改这里一处,不用满项目找魔法值。答辩时可以说清状态流转集中管理。

4.3 三个高频问题:时区报错、事务失效、日期格式

第一个是 mysql-connector-java 8.x 连 MySQL 5.7 的时区问题。驱动驱动初始化时抛 “The server time zone value” 异常,连接串加上参数即可:

jdbc.url=jdbc:mysql://localhost:3306/hotel_db?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8

serverTimezone 指定了连接会话的时区。useSSL 关闭是为了避免本地开发时证书告警。characterEncoding 保证写入的中文不乱码。

第二个是事务不生效。最常见原因是 Service 被扫进了 spring-mvc.xml 的子容器,或者漏了<tx:annotation-driven>。排查方式很简单:在 checkIn 里主动抛 RuntimeException,看数据库是否回滚;没回滚就检查两个配置文件里的扫描包路径。

第三个是 JSP 里的时间显示。查询出的 java.sql.Timestamp 直接 EL 输出会出现2025-01-01 12:00:00.0的格式,用 JSTL 标签处理:

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> <fmt:formatDate value="${record.createTime}" pattern="yyyy-MM-dd HH:mm" />

formatDate 只负责显示格式化,不动数据库里的原始值。如果页面用普通 EL 输出,也可以在实体类加一个 getCreateTimeStr() 方法,但 JSTL 方式不用改 Java 代码,更适合毕业设计文档里“表示层设计”的说明。

5. 源码配套文档怎么组织,答辩怎么验证

5.1 文档章节与源码目录的对应关系

毕业设计文档要能和源码对得上,评审老师常做的一个动作是翻到文档里的“某某功能”,然后去源码里找对应的类。文档结构建议这样组织:需求分析、数据库设计、系统设计(含分层架构和关键接口说明)、系统实现(按模块写核心流程)、测试与部署。每一章都指到源码的具体位置。

sql 脚本独立放一份工程根目录的 sql/schema.sql,文档里写“数据库设计”时直接引用表名。部署步骤写清楚 Tomcat 的 webapps 目录和 JDK 版本,不然换台机器跑不起来。

mvn clean package -DskipTests cp target/ROOT.war /path/to/tomcat/webapps/ /path/to/tomcat/bin/startup.sh tail -f /path/to/tomcat/logs/catalina.out

-DskipTests 跳过测试减少打包时间。注意 WAR 包名要改成 ROOT.war,访问路径才是http://localhost:8080/,否则要带工程名。catalina.out 是排查启动失败的第一入口,NullPointerException 通常都能在这里定位到。

5.2 答辩前的自测清单

换一台干净机器,按文档步骤从头走一遍,这个动作能排除一半“我这就跑得起来,换台机器就崩”的问题。然后按三个核心业务各测一次:办理入住后房间状态是否变成入住中;退房后是否生成账单且房间恢复空闲;取消订单后房间是否立即释放。数据初始化时预置一间测试房和一位测试客人,避免答辩现场临时造数据。

用管理员账号和员工账号分别登录,确认权限差异:员工登录后看不到管理员菜单,管理员改房间状态时操作的房间号要写入日志。最后检查源代码里的数据库账号密码是否和文档一致,默认密码不要用 root 的真实密码,用简单易记的测试密码就行。答辩时登录不进去是硬伤,比功能少做一两个更伤分。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询