简介:基于SSM(Spring+SpringMVC+MyBatis)与MySQL实现的酒店客房预定管理系统,是一套面向计算机专业毕业设计、课程设计及SSM框架进阶学习者的完整项目资源。系统覆盖个人中心、用户管理、客房类型管理、酒店客房管理、客房预定管理、开房记录管理、退房结账管理、系统管理等核心业务,并设有管理员与普通用户两级权限,同时支持入住率报表、客房类型统计等数据分析功能,能完整体现SSM框架在Web业务系统中的应用思路。压缩包共1254个文件,大小约18.9MB,其中包含112个Java源文件、103个JSP页面、364个JavaScript脚本、146个CSS样式文件,以及大量图片素材、SQL数据库脚本、设计文档、部署说明和视频演示。源码分层清晰,Controller、Service、Mapper结构一目了然,SQL脚本提供建表语句和初始数据,部署说明可帮助快速搭建运行环境。目前已有185人学习下载,项目源码经过测试校正可百分百运行,适合需要直接获得可运行工程以支撑毕业设计、课程答辩或项目实战的读者,也可用于二次开发和功能扩展,实践价值较高。
1. 拿到“基于SSM+MySQL的酒店客房预定管理系统”压缩包之后,第一件事不是解压
A同学把压缩包解压后,把里面每个看起来像启动入口的文件都双击了一遍,一个都没起来。这不是手气问题——这个标题拆开就是一套典型的Java Web后台:Spring+SpringMVC+MyBatis做三层架构,MySQL存数据,业务范围是客房查询、预订、入住、退房。系统把原来写在台账上的房间状态和订单状态,变成了数据库表和一条条SQL。它适合正在做课程设计、刚接触SSM、或者需要拿一个可运行项目当脚手架的人。为什么说第一件事不是解压?因为不知道启动方式就双击,结果多半是被环境问题劝退。
2. SSM + MySQL为什么能撑起酒店客房预订:三层架构与业务状态流转
在我经手过的教学项目里,SSM这套组合被吐槽最多的就是“老”。但放到这个标题的语境里,它反而是优点:Spring管对象和依赖,SpringMVC管请求分发,MyBatis管SQL映射,三层边界清楚,出了问题能直接定位到某一层。这一章先把架构和业务讲透,后面部署、排错才有根据。
2.1 为什么还在用SSM:三层架构解决的是代码堆一起的问题
SSM三个成员各自只干一件事。Spring管理所有对象的创建和依赖关系,Service层要用的Mapper接口由它注入,不需要你手动new;SpringMVC接收浏览器来的HTTP请求,按URL把请求分给对应Controller,Controller再把视图名返回给前端页面;MyBatis负责把Mapper接口的方法和XML里的SQL绑起来。数据流向是固定的:浏览器发起请求→DispatcherServlet→Controller→Service→Mapper接口→MySQL,查询结果再按原路返回。
这套流程里有个容易被忽略的好处:Controller不写SQL,Mapper不写业务。新接手的人拿到工程,看Controller只知道入口,看Service知道业务规则,看Mapper XML才知道具体查了哪张表,三层各查各的,定位问题很快。相比之下,Spring Boot也基于这三大件,只是把配置自动装配了。如果这是一个需要写设计文档、需要讲清“为什么这样分层”的交付物,手写SSM配置反而更容易展开讲。
我一般会建议拿到这类工程后先画一张“请求→响应”的链路图,标出每个请求进了哪个Controller,再调了哪个Service,最后用到哪张表。这张图画得出来,系统就懂了六成。画不出来的部分,就是设计文档或者代码里写得比较含糊的部分,答辩前要重点补。
2.2 业务闭环:查房、下单、入住、退房,两套状态不能混
酒店预订的核心流程不复杂,但容易在状态设计上翻车。完整闭环是:用户按入住日期和离店日期查房;选一间可售房间生成预订订单;支付后订单变成已支付待入住;客人到店前台办入住,房间从“已订”变成“已入住”;退房时做结算,房间变成“清洁中”,打扫完再变回“空闲可售”。
最关键的是一件事:房间状态和订单状态是两套独立状态,不能混着设计。一个房间可以没有订单但处于清洁中,一个订单可以已支付但房间还没下来。我建议按下面的取值设计:
| 房间状态 | 含义 | 订单状态 | 含义 |
|---|---|---|---|
| 0 | 空闲可售 | 0 | 待支付 |
| 1 | 已预订(未入住) | 1 | 已支付/待入住 |
| 2 | 已入住 | 2 | 已入住 |
| 3 | 清洁中/维修 | 3 | 已退房/已完成 |
| — | — | 4 | 已取消 |
设计文档如果写得细,还会对每个状态迁移标注“操作人+触发动作”:比如订单从0到1,操作人是用户,动作是支付;房间从2到3,操作人是前台,动作是退房。检查这个表就能看出流程有没有漏洞——哪个状态没有操作入口,或者哪个操作不改变任何状态,那就是设计多了一步或少了一步。
2.3 角色与权限:三个入口,一个登录会话
这类系统一般分三类角色:普通用户在网站端查房、下单、取消订单;前台办理入住和退房;管理员维护房型价格、查看所有订单。权限上不需要引入复杂的RBAC表,常见的做法是登录后在Session里存用户对象和用户类型,用SpringMVC拦截器做访问控制。
<mvc:interceptors> <!-- 拦截所有请求,未登录跳转到登录页 --> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login/**"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.hotel.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这里有两个必须注意的点:一是exclude-mapping必须放行静态资源和登录接口,否则CSS进不来、登录请求也被拦;二是拦截器里判断用户类型时,要对管理端路径做前缀匹配,不能让普通用户直接访问/admin/**。拦截器只拦截“有没有登录”,角色判断放在Controller方法里或拦截器里二次判断,粒度为“菜单级别”就够了。
3. 客房预订系统的数据库设计:拆表、状态机与DDL要点
数据库是整个系统里最先能看出水平的部分。这个标题的业务不算复杂,但表拆得好不好,直接决定后面改需求时是改一行配置还是改一坨SQL。我的建议是按住以下几张表就够了:用户表、房型表、房间表、订单表,复杂的可以再加一张操作日志表。下面按最核心的三张表展开。
3.1 把“房型”和“房间”分开建模
价格、面积、床型、可住人数这些属性,属于“房型”这一级;房间号、楼层、当前状态属于“房间”这一级。如果不拆分,同一种房型有十间房,房型价格要改时你得改十行;拆开后只要改一行。标准做法是两张表,房间表通过外键引用房型表。
CREATE TABLE room_type ( type_id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT '房型名称', bed_type VARCHAR(20) DEFAULT NULL COMMENT '床型', price DECIMAL(10,2) NOT NULL COMMENT '门市价', area INT DEFAULT NULL COMMENT '房间面积', max_people INT DEFAULT 3 COMMENT '可住人数' ); CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL COMMENT '房间号', floor INT DEFAULT NULL COMMENT '楼层', type_id INT, status TINYINT DEFAULT 0 COMMENT '0空闲 1已订 2入住 3清洁/维修', FOREIGN KEY (type_id) REFERENCES room_type(type_id) );状态status放在房间表而不放在房型表,因为同一房型下不同房间状态可能不同:308在住、309空闲,这很正常。这里要提醒一句:订单里的金额不要实时去关联房型表算,而是在下单那一刻把房型价格冗余到订单表里。这是历史快照,因为过两天房价可能调整,老订单的金额不能跟着变。
3.2 订单表设计:订单号、日期区间和冲突判断逻辑
订单表是这个系统里最容易写错的一张表。先说两个容易被带偏的点:一是主键自增id不要直接暴露给用户当订单号,业务上要单独给一个order_no字符串字段;二是入住日期和离店日期用DATE类型,不要用DATETIME,因为房态按天算,不按小时算。
CREATE TABLE book_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '对外业务单号', user_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATE NOT NULL COMMENT '入住日期', check_out_date DATE NOT NULL COMMENT '离店日期', order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额,下单时的房型价格快照', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已入住 3已退房 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_order_room_date (room_id, check_in_date, check_out_date), KEY idx_order_user (user_id) );这个系统最核心的一段SQL是“查可用房间”。要判断某间房在2024-07-01入住、2024-07-03离店是否可用,不是看它现在状态是不是0,而是看它有没有“占房订单”和这个日期区间重叠。区间重叠的判断条件是:已有订单的入住日期 < 新订单的离店日期,且已有订单的离店日期 > 新订单的入住日期。
SELECT r.* FROM room r WHERE r.status = 0 AND r.room_id NOT IN ( SELECT o.room_id FROM book_order o WHERE o.status IN (0, 1, 2) -- 待支付、已支付、已入住都算占房 AND o.check_in_date < '2024-07-03' -- 新订单的离店日期 AND o.check_out_date > '2024-07-01' -- 新订单的入住日期 );这段SQL里的NOT IN子查询是关键:先取出所有“占房订单”对应的房间,再反查不在这个集合里的空闲房间。状态过滤必须包含待支付,因为待支付订单虽然还没付款,但房间已经被锁定,不能再卖给第二个人。边界情况是:已有订单离店日期等于新订单入住日期时,不算冲突,客人退房当天房子就能卖给下一位。
3.3 索引、外键与删除策略:设计文档里要写清楚的规定
索引方面,上面DDL里已经给book_order建了联合索引(room_id, check_in_date, check_out_date),这段SQL正是冲突查询用到的条件列。如果不建这个索引,数据量一上去,每次查可用房间都会全表扫描。status字段不单独建索引,因为它的区分度太低,查1还是查4差别不大。
外键要不要建,我的习惯是“建,但不依赖它做级联操作”。房间表对房型表建外键,订单表对房间表建外键,好处是防止误删——你删一个已经被订单引用的房间时,数据库会拦你。设计文档里必须写明:客房、订单都属于历史数据,物理删除要禁止,所有“删除”都是逻辑删除,比如给房间加一个is_active字段或者把房型置为停用。订单同理,只做“取消”,不做DELETE。
最后是字符集。建库语句统一用utf8mb4而不是utf8,utf8mb4是完整的UTF-8实现,能存生僻字和Emoji,排序规则用utf8mb4_general_ci即可。这一步做完后面基本不会遇到中文乱码,省很多事。
4. 把项目跑起来:从解压到Tomcat启动的最小操作序列
标题里写了“源码+设计文档+部署说明+视频演示”,说明压缩包的内容是齐的。很多同学拿到手直接开IDEA就跑,结果报错一堆。这章按我的操作顺序来,每一步都对应一件事。
4.1 解压后先做的三件事:看目录结构、读设计文档、核对部署说明
不要急着点运行。先解压,然后看四类东西:源码目录里有没有pom.xml,判断是不是Maven工程;设计文档确认业务表结构;部署说明确认JDK、Tomcat、MySQL的版本要求;视频演示不用全看完,拖动到登录和预订页面看一眼预期效果即可,避免自己跑通后不确定“什么样算成功”。
| 压缩包内容 | 你先确认什么 |
|---|---|
| 源码目录 | 是Maven结构还是普通Web工程,入口在哪 |
| 设计文档 | 数据库有几张表,业务有哪些角色 |
| 部署说明 | JDK/Tomcat/MySQL版本,SQL脚本文件名 |
| 视频演示 | 登录账号入口,页面长什么样 |
这个检查本身不需要花很多时间,但它能帮你避开“本地环境版本不对导致启动失败”这种最冤枉的问题。
4.2 建库、导入SQL脚本、修改数据库连接配置
数据库这步是大多数人第一次卡住的地方。先打开部署说明,找到SQL脚本文件的名字,然后启动MySQL客户端建库导入。
mysql -u root -p -- 登录后执行 CREATE DATABASE hotel_db CHARACTER SET utf8mb4; USE hotel_db; SOURCE /path/to/hotel_db.sql;如果部署说明给的是独立SQL文件,也可以直接在系统命令行导入:
mysql -u root -p hotel_db < /path/to/hotel_db.sqlSOURCE是MySQL客户端内部的命令,重定向是系统Shell的方式,两者效果一样。导入后执行SHOW TABLES;确认表的数量,再SELECT * FROM book_order;看有没有初始化数据。很多系统会把初始账号写在SQL脚本的INSERT语句里,这一步能看到管理员账号到底被设成了什么。
接着改连接配置。SSM工程的数据库配置一般在jdbc.properties或db.properties里:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的数据库密码driver取决于MySQL版本:MySQL 5.x用com.mysql.jdbc.Driver,MySQL 8.x用com.mysql.cj.jdbc.Driver,用错会报驱动类找不到。URL里的characterEncoding=utf8解决中文写入问题,serverTimezone=Asia/Shanghai解决MySQL 8.x的时区报错。password必须改成你本机的数据库密码,这里填别人的密码是连不上的。
4.3 IDEA导入Maven工程、配置Tomcat、启动验证
导入工程这步,我的习惯是File → Open直接选源码根目录,等右下角Maven依赖同步完成。如果依赖长时间下载不完成,检查Maven的本地仓库配置和镜像,不要手动去网上一个个下载jar包。JDK版本按部署说明来,一般切到1.8。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.23</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.29</version> </dependency>pom里的版本号以工程自带为准,不要自己往高了换,SSM对版本组合有兼容性要求,换版本可能带来预料外的报错。配置Tomcat时,在Run → Edit Configurations里添加Tomcat Server → Local,Deployment选项卡选war exploded,Application context填/或者部署说明里写的项目名。启动后打开浏览器访问对应地址。
验证成功的标准是:能看到登录页,输入SQL脚本里的初始账号能进后台,打开客房列表能查到数据。这一步走通,整个系统的运行链路基本没问题了。
5. 排错与避坑:数据库连接、端口、静态资源与中文乱码的四类常见问题
这一章写的是我见过最多、也最好复现的几类问题。每条按现象、原因、解决三步走,建议把这一章当成排查手册用,遇到哪个查哪个。
5.1 数据库连不上:Access denied 与时区乱码
现象一:Tomcat启动后访问页面,后台日志报Access denied for user 'root'@'localhost' (using password: YES)。现象二:报错信息里出现The server time zone value '�й���ʱ��',后面跟着一串看不懂的乱码。
第一个原因是密码不对或者数据库没建。注意区分:Access denied代表用户名密码有问题,Unknown database代表库名不存在,这两个是不同的问题。先打开命令行手动执行mysql -u root -p验证能不能登录,再用SHOW DATABASES;确认hotel_db存在。第二个原因是连接串缺了serverTimezone参数,MySQL 8.x默认时区跟系统不一致就会报这个错。
解决方式:在jdbc.properties里把密码改成你本机的真实密码,URL加上serverTimezone=Asia/Shanghai,然后重启Tomcat。改配置后只热部署不重启,连接池里的旧连接不会重新初始化,这是很多人改了配置但问题还在的原因。
5.2 Tomcat起不来:端口被占用
现象:IDEA控制台报Port 8080 was already in use,或者Error running Tomcat: Address already in use,点击启动立刻失败。
原因是本机某个进程已经占用了8080端口,常见的是之前启动过的Tomcat实例没关干净,或者其他开发工具的服务占了这个端口。解决方式,先找到占用进程再决定杀进程还是换端口。
# Windows netstat -ano | findstr 8080 taskkill /PID 对应PID /F # Linux / macOS lsof -i:8080 kill -9 对应PID如果杀不掉或者那个进程不能动,就改Tomcat端口。在conf/server.xml的Connector节点把port="8080"改成8081,再次启动后访问地址也要同步改成http://localhost:8081/项目名/。这里顺便说一句:IDEA的Tomcat配置里也有一个HTTP port,如果改了server.xml的端口,IDEA里的端口也要同步改,否则还会冲突。
5.3 登录页没有样式:CSS和JS全是404
现象:页面能打开,但只有一个光秃秃的HTML骨架,控制台里一堆/css/style.css 404之类的报错。
原因有两个,我遇到过很多次。第一是JSP里静态资源路径写死了,比如href="/css/style.css",部署后项目上下文路径变了,真实路径变成/项目名/css/style.css,所以404。第二是SpringMVC拦截器把静态资源请求也拦了,DispatcherServlet把.css文件当成Controller去匹配,自然找不到。
解决方式:页面里所有静态资源路径前面都加上${pageContext.request.contextPath},这是最稳妥的写法。然后在SpringMVC配置文件里放行静态资源:
<mvc:resources mapping="/static/**" location="/static/"/> <mvc:default-servlet-handler/><mvc:default-servlet-handler/>是让容器默认的Servlet去处理静态文件,没有这一行,Tomcat默认的静态文件处理能力会被DispatcherServlet覆盖掉。加了之后,CSS、JS、图片这类请求不再走业务逻辑,访问页面的样式问题就能解决。
5.4 中文全变问号:三处编码必须一致
现象:页面上输入“张三”,保存后到数据库里看变成“???”。
原因是写入链路上有三处字符集不一致:数据库表、JDBC连接串、页面请求编码。三处只要有一处是latin1或者默认编码,中文就可能变问号。解决方式,统一改成utf8mb4。
先确认表本身的字符集:
SHOW TABLE STATUS LIKE 'book_order';看Collation列,如果不是utf8mb4_general_ci,执行ALTER TABLE book_order CONVERT TO CHARACTER SET utf8mb4;。然后确认连接串里有characterEncoding=utf8。最后在JSP页面头部加<%@ page contentType="text/html;charset=UTF-8" %>。这三点都做到,基本不会再出现中文乱码。如果还有问题,检查Spring的CharacterEncodingFilter是否配置了,它会统一处理请求和响应的编码。
6. 让这个系统在答辩里站得住:两个实用的增量改进方向
基础功能跑通只是及格线。想让评分或者面试官觉得你有想法,可以在不改变整体架构的前提下,加两个有技术含量的改进点。这两个方向工作量不大,但能展示你对业务的思考深度。
6.1 做一个“超时未支付自动取消”的任务
酒店房间是库存资源,用户占着订单不付款,房间就一直不能卖。常见的做法是:订单创建后30分钟内未支付,自动置为取消,释放房间库存。SSM工程里用Spring Task就能实现。
@Component public class OrderTimeoutCancelTask { @Scheduled(cron = "0 */5 * * * ?") public void cancelExpiredOrders() { // 查出当前时间往前推30分钟仍未支付的订单,批量置为已取消 // UPDATE book_order SET status = 4 // WHERE status = 0 AND create_time < #{targetTime} } }这个任务用一条批量UPDATE就能完成,不需要逐条查再逐条改。使用批量更新比循环更新效率高得多。别忘在Spring配置文件里加<task:annotation-driven/>开启定时注解支持,否则@Scheduled不会生效。这个功能在现场演示时会很有说服力,相当于用代码解决了一个真实的业务问题。
6.2 把预订冲突检测包装成一个可讲清楚的技术点
查可用房间那段NOT IN逻辑,是系统里最值得讲的部分。答辩时可以直接把问题抛出来:同一间房在同一个日期区间,两个人的下单请求同时打过来怎么办。这段逻辑虽然能挡住“已经被占的房间”,但并发场景下两个事务同时查到了同一间房可用的结果,就可能产生重复订单。
常见的兜底方案有两种。第一种是在业务代码里对房间ID加锁,把“查询可用房间+插入订单”两步包在同一个事务里,事务边界要清晰。第二种是给订单表加一个版本号字段做乐观锁,更新时带上版本条件,更新影响行数为0就代表被别人抢先。这里不展开代码,但设计文档里把这块的并发处理思路写清楚,比堆很多页面截图更能体现水平。
我自己的习惯是拿到这类工程先动手画一张状态流转图,画不清楚就说明没看懂,画清楚了,后面所有bug都有了一个判断标准。把这个习惯带进这个系统里,你会发现两套状态、一张订单表,远比一堆页面更值得研究。希望帮到你。
本文还有配套的精品资源,点击获取