简介:一套基于SSM(Spring+SpringMVC+MyBatis)与MySQL的酒店客房预定管理系统完整项目,面向毕业设计、课程设计及需要快速掌握Java Web开发流程的初学者。系统覆盖个人中心、用户管理、客房类型管理、客房预定、开房记录、退房结账、系统管理等模块,业务流程完整,具备权限分级与基础报表统计能力。压缩包共含1254个文件,以java源码、jsp页面、js脚本、css样式及png/gif图片资源为主,另含sql数据库脚本、xml配置文件、设计文档、部署说明和视频演示,整体约18.9MB。资源已经过测试校正,可百分百运行,帮助读者省去环境搭建和排错时间,直接对照学习SSM整合思路与酒店管理业务实现。目前已有185人学习下载,适合作为实战练手或毕业设计参考。
1. 基于SSM+MySQL的酒店客房预定管理系统:毕业设计项目的核心技术拆解
做毕设选项目,最怕的不是功能少,而是源码拿到手跑不起来。这套基于SSM框架(Spring+SpringMVC+MyBatis)的酒店客房预定管理系统,属于典型的Java Web课程设计/毕业设计项目——Spring管理业务对象,SpringMVC接收请求,MyBatis操作MySQL数据库,前端用JSP渲染页面,配了Bootstrap和ElementUI做界面样式。项目源码经过测试校正,附带完整设计文档和部署说明,适合需要快速交付的从业者:不管是做毕业设计还是课程设计,拿到就能跑通,省去从零搭框架的时间。接下来按实际拆解路径走:先讲SSM架构和数据库设计,再拆核心业务代码,最后落到部署和排错。
2. SSM三层架构与MySQL表设计:从配置链路到九张核心表
2.1 为什么选SSM框架:三层架构的职责边界
SSM是老牌组合,Spring负责Bean管理,SpringMVC负责Web层请求分发,MyBatis负责数据持久化。它的核心优势在于职责边界清晰:控制层(Controller)只做参数接收和视图跳转,业务层(Service)只处理业务逻辑,数据层(Mapper)只做SQL映射。这种分层方式特别适合酒店客房预定这种业务模型相对固定的系统,每张表的增删改查都能对应到Mapper接口,扩展新功能时不会牵一发动全身。
对比Spring Boot的单体服务风格,SSM虽然配置繁琐,但每个配置文件都是显式声明,适合理解Java Web的请求流转过程。常见做法是Spring负责Service层Bean管理,SpringMVC负责Controller层,MyBatis负责Mapper层与数据库的连接管理。这个系统里三者协作路径是:浏览器请求 → DispatcherServlet → Controller → Service → Mapper → MySQL,返回路径反向走,最终由视图解析器渲染JSP页面。
2.2 数据库表设计:预定、开房、退房三张业务表的状态关联
从项目源码里的实体类可以反推出表结构。系统核心角色分管理员和普通用户,围绕客房产生预定、开房、退房三条业务链路。表的设计遵循第三范式,把用户表、客房表、类型表拆分独立,通过外键关联。需要重点理解的是预定、开房记录、退房结账三张表的关系:预定表存客户预定意向,开房记录表存实际入住信息,退房结账表算费用。这三张表通过客房ID和用户ID串联起来。
-- 客房类型表 CREATE TABLE room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT '类型名称,如标准间/大床房', price DECIMAL(10,2) NOT NULL COMMENT '单价,按晚计价', remark VARCHAR(255) COMMENT '备注' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 酒店客房表 CREATE TABLE hotel_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL COMMENT '房号,如8801', room_type_id INT NOT NULL COMMENT '外键:客房类型ID', status TINYINT DEFAULT 0 COMMENT '状态:0空闲 1已预定 2已入住', floor INT COMMENT '楼层', FOREIGN KEY (room_type_id) REFERENCES room_type(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 客房预定表 CREATE TABLE room_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '外键:预定客户ID', room_id INT NOT NULL COMMENT '外键:客房ID', order_no VARCHAR(32) NOT NULL COMMENT '预定单号', check_in_date DATE NOT NULL COMMENT '计划入住日期', check_out_date DATE NOT NULL COMMENT '计划退房日期', order_status TINYINT DEFAULT 0 COMMENT '预定状态:0有效 1已取消 2已入住', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES sys_user(id), FOREIGN KEY (room_id) REFERENCES hotel_room(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;参数说明:room_type_id设置外键约束,保证客房必须归属一个有效类型;status用TINYINT存状态码而不是字符串,查询效率更高,后续加状态只需扩展数字含义。room_order表里order_no用32位VARCHAR存唯一单号,方便订单追踪,实际项目里单号一般用时间戳加随机数拼接生成。check_in_date和check_out_date用DATE类型,避免DATETIME带来的时分秒干扰,日期间隔计算直接做日期减法即可。
这里有个设计细节值得注意:预定表和开房记录表没有合并成一张表。刚开始做容易犯的错是把预定、入住、退房全塞进一张订单表,用状态字段区分。拆开的好处是——预定是客户行为,可能有取消操作;开房记录是酒店行为,涉及房间分配和押金;退房结账是财务行为,涉及实收金额。三张表各自记录生命周期,报表统计时用JOIN关联即可。
2.3 配置文件链路:web.xml、spring-mvc.xml、mybatis-config.xml的协作方式
SSM项目最容易翻车的地方在配置文件,这个项目从源码里能看到完整的配置链。web.xml负责启动Spring容器和SpringMVC前端控制器,spring-mvc.xml负责扫描Controller和配置视图解析器,applicationContext.xml扫描Service和Mapper,mybatis-config.xml配置MyBatis全局参数。配置文件之间通过contextConfigLocation和classpath引用串联。
<!-- web.xml 核心片段 --> <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>springmvc</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>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping><!-- spring-mvc.xml 核心配置 --> <context:component-scan base-package="com.hotel.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>逻辑说明:web.xml里DispatcherServlet拦截/路径,所有请求进入SpringMVC前端控制器;spring-mvc.xml只扫描controller包,避免把Service层重复纳入Web容器管理。视图解析器配置prefix和suffix后,Controller返回字符串"room/list"会自动拼接成/WEB-INF/views/room/list.jsp。这种配置方式下,JSP页面放在WEB-INF目录里,用户不能直接通过URL访问,强制走Controller,安全性更好。
MyBatis部分的配置核心在mapper.xml里,系统源码里每张表对应一个Mapper接口加一个XML文件,比如RoomMapper.xml里写SQL,RoomMapper.java里定义方法。项目里SQL写法和MyBatis-Plus不同,更接近原生SQL,适合理解数据访问层的工作原理。
3. 核心业务实现:登录鉴权、客房预定与退房结算的代码拆解
3.1 登录鉴权:拦截器与Session的角色权限控制
用户权限管理在系统里通过SpringMVC拦截器实现,分管理员和普通用户两种角色。拦截器统一处理Session校验,避免每个Controller重复写判断逻辑。项目里的登录流程是:前端表单提交用户名密码,Controller调用UserService查询MySQL,密码匹配后把用户信息放入Session,跳转对应首页。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { // 未登录:重定向到登录页 response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员路径校验:要求session中的role字段为1 String uri = request.getRequestURI(); if (uri.contains("/admin/")) { Object role = session.getAttribute("userRole"); if (role == null || !"1".equals(role.toString())) { response.sendRedirect(request.getContextPath() + "/index"); return false; } } return true; } }参数说明:preHandle返回false表示请求被拦截,不再进入Controller。userRole字段在登录成功后写入Session,值为1代表管理员,0代表普通用户。这里做的是一个粗粒度权限控制——限制普通用户不能访问/admin/前缀的路径,细粒度的按钮级权限在JSP用c:if标签判断。实际开发中如果权限层级复杂,建议引入角色表、菜单表、角色菜单关联表,但在这个规模的项目里,Session存角色字段足够了,改动成本低。
3.2 客房预定业务:状态机流转与并发控制
客房预定是整个系统的核心业务,涉及客房状态变更、订单生成、类型价格读取三个动作。系统把预定定义为:客户选客房、填入住日期、生成订单、修改客房status为1(已预定)。这里有前后端联动的逻辑,前端通过Ajax传参数,Controller接收后开始事务。一个容易被忽略的点是状态判断:预定前必须确认客房status是0(空闲),否则不能下单。
@Transactional public boolean createOrder(RoomOrder order, Integer roomId) { // 1. 锁定客房并检查状态 HotelRoom room = roomMapper.selectByIdForUpdate(roomId); if (room == null || room.getStatus() != 0) { throw new RuntimeException("客房不存在或不可预定"); } // 2. 生成订单号:时间戳 + 随机数 String orderNo = generateOrderNo(); order.setOrderNo(orderNo); order.setOrderStatus(0); // 3. 更新客房状态为已预定 int update = roomMapper.updateStatus(roomId, 1); if (update != 1) { throw new RuntimeException("客房状态更新失败"); } // 4. 保存订单 return orderMapper.insert(order) > 0; }参数说明:@Transactional保证三个阶段要么全部成功要么全部回滚。selectByIdForUpdate是悲观锁写法,MySQL执行SELECT ... FOR UPDATE,锁定该行直到事务结束,防止两个用户同时预定同一间客房。generateOrderNo里常见做法是System.currentTimeMillis()加三位随机数拼成32位字符串,保证并发下不重复。这种用锁保证一致性的方案适合小规模系统,缺点是并发量上去后锁竞争明显,但毕设项目这个量级完全够用。
预定状态流转的完整链路是:空闲(0)→ 已预定(1)→ 已入住(2)→ 空闲(0)。用户取消预定,状态从1回到0;用户办理入住,状态从1变2;退房结账后,状态从2回到0。这个状态机在Service层通过if判断控制,没有引入状态机框架,逻辑直观,便于维护。
3.3 开房记录与退房结账:金额计算的边界条件
开房记录和退房结账是两个紧邻的操作,开房时把预定订单状态改为已入住,同时创建一条开房记录;退房时根据入住天数和客房类型单价计算总费用,生成结账记录。金额计算最大的坑在跨天计算——早上6点入住和下午6点入住,退房时的计费方式不同,这个逻辑在源码里的实现方式是查入住时间,按自然日差值计算。
public BigDecimal settleAccounts(Integer recordId) { // 1. 查询开房记录,拿到入住时间和退房时间 CheckInRecord record = checkInRecordMapper.selectById(recordId); // 2. 计算入住天数:按日历日计算,不足一天按一天计 int days = calculateDays(record.getCheckInTime(), record.getCheckOutTime()); // 3. 读取客房类型单价 RoomType roomType = roomTypeMapper.selectById(record.getRoomTypeId()); BigDecimal total = roomType.getPrice().multiply(BigDecimal.valueOf(days)); // 4. 生成退房结账记录 CheckoutRecord checkout = new CheckoutRecord(); checkout.setRecordId(recordId); checkout.setTotalAmount(total); checkout.setCreateTime(new Date()); return checkoutRecordMapper.insert(checkout) > 0 ? total : null; } private int calculateDays(Date in, Date out) { long diffMs = out.getTime() - in.getTime(); long diffDays = diffMs / (1000 * 60 * 60 * 24); long remainder = diffMs % (1000 * 60 * 60 * 24); return remainder > 0 ? (int) diffDays + 1 : (int) diffDays; }参数说明:BigDecimal用于金额计算,避免double精度丢失——这是财务相关的硬性要求,总价计算不能出现0.999999这种结果。calculateDays用毫秒差值取整,余数大于0就加一天,模拟的是"住了3小时也算一天"的酒店规则。这个方法是退房结账最容易出bug的地方,很多毕设项目在这里直接做天数减法,没考虑时分秒带来的误差,导致结账金额和预期不符。
如果项目需要升级,推荐把计费规则做进数据库配置表,比如设置一个cutoffTime字段,规定下午2点前退房按当天计算,之后退房加收半天。这种规则用配置驱动比写死在Java代码里更灵活,酒店调价或调整规则时不用重新部署。
4. 部署流程与避坑记录:从源码导入到稳定运行的四个高频故障
4.1 环境版本匹配:JDK、Tomcat、MySQL的兼容性组合
这个项目是JSP加SSM架构,环境版本选择比Spring Boot更讲究。从项目配置可以推断,源码基于JDK 1.8编译,Tomcat建议用8.5版本,MySQL不要装8.0以上版本。MyBatis 3.x搭配MySQL 5.7是经典组合,如果硬上MySQL 8.0,数据库驱动需要换成mysql-connector-java 8.x,且URL要加serverTimezone参数,否则报错时区问题。
<!-- pom.xml 关键依赖版本 --> <properties> <spring.version>4.3.25.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> </properties>注意:SSM项目不用Spring Boot,依赖版本由pom.xml或lib目录管理。Spring 4.x对应JDK 1.8完全兼容,MyBatis 3.4.6支持Mapper XML里的#{}预编译。如果项目中pom.xml缺失,说明用的是Eclipse Dynamic Web Project导入本地jar包的方式,这时要检查lib目录下的jar包里是否有mysql-connector-java-5.1.x和jstl相关的两个jar。JSTL缺失会导致JSP页面里的c:forEach标签直接报错,页面白屏且控制台有ClassNotFoundException。
4.2 部署步骤:从导入到启动的完整流程
拿到源码后,推荐用IDEA导入Maven项目,如果没有pom.xml则用Eclipse的Import Existing Projects。部署路线分五步走:创建数据库、修改数据源配置、配置Tomcat、部署war包、启动验证。
# 1. 创建数据库并导入项目自带的SQL脚本 mysql -u root -p mysql> CREATE DATABASE hotel_room DEFAULT CHARSET utf8; mysql> USE hotel_room; mysql> SOURCE /path/to/hotel.sql; # 2. 修改数据库连接配置(jdbc.properties) jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_room?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=your_password # 3. 启动Tomcat并部署 mvn clean package cp target/hotel-room.war /path/to/tomcat/webapps/ sh /path/to/tomcat/bin/startup.sh参数说明:数据库编码必须用utf8,否则中文字段在查询时出现乱码。jdbc.url里characterEncoding=utf8控制JDBC连接层面的字符集,整个链路(JSP页面、Tomcat、JDBC、MySQL)任何一环编码不一致,都会导致存入数据库的中文字符变成问号。如果项目自带SQL文件,文件名里会有hotel或room字样;没有SQL文件的情况,可以从源码的Mapper映射里逆向推导建表语句。
部署完成后的验证路径:启动Tomcat后访问http://localhost:8080/hotel-room/,如果看到登录页,用管理员账号登录后能进入用户管理页面,说明核心链路已经打通。常见默认账号是admin/admin123,具体以项目文档为准。
4.3 高频故障排查:四个踩坑记录
故障一:Tomcat能启动但页面404 现象:访问首页返回404,控制台没有任何报错。 原因:war包部署路径和context-path不匹配,或访问URL少写了项目名。 解决:确认war包名字是hotel-room.war,访问地址必须带/hotel-room前缀。如果项目用ROOT.war部署,则不需要加前缀。
故障二:数据库连接报Communications link failure 现象:程序启动时报无法连接MySQL,错误码是CommunicationsException。 原因:MySQL 5.7默认只监听localhost,如果jdbc.url里写成127.0.0.1,部分版本会因host匹配问题拒绝连接。 解决:检查数据库端口是否被占用,再用命令行工具验证账号权限——执行GRANT ALL PRIVILEGES ON hotel_room.* TO 'root'@'localhost' IDENTIFIED BY 'password'; 刷新权限后重试。
故障三:JSP页面中文全部乱码 现象:页面上中文显示为??,但数据库里数据正常。 原因:JSP页面本身编码不是UTF-8。项目里如果index.jsp.bak这类备份文件,说明原文件被改过,页面可能没有设置contentType。 解决:在所有JSP页面头部检查pageEncoding属性,统一为UTF-8。同时web.xml里配置CharacterEncodingFilter,强制所有请求和响应走UTF-8编码,这是SSM项目最标准的做法。
故障四:启动报BeanCreationException,提示找不到Mapper 现象:Spring容器初始化失败,报NoSuchBeanDefinitionException,指向某个Mapper接口。 原因:applicationContext.xml里没有扫描到Mapper接口包,或者SqlSessionFactoryBean的mapperLocations路径不对。 解决:检查spring配置里base-package是否覆盖了Mapper所在的包路径(项目中通常是com.hotel.mapper),再确认mapperLocations的classpath路径和实际mapper.xml存放位置一致。SSM项目里这是最常见的配置错误,十个项目里有七个栽在这上面。
5. 进阶验证:用Postman跑通预定接口并扩展报表统计
5.1 用Postman验证核心接口的完整链路
部署成功后,很多人直接浏览器点页面,发现功能都是通的,就不再做接口层验证。实际上,接口验证比页面点击更能暴露问题——特别是异常参数、权限拦截这类场景。推荐用Postman按下面流程跑一遍预定接口,确认后端逻辑健壮性。
# 步骤一:模拟用户登录,拿Session POST http://localhost:8080/hotel-room/user/login Content-Type: application/x-www-form-urlencoded username=admin&password=admin123 # 步骤二:查询空闲客房列表 GET http://localhost:8080/hotel-room/room/list?status=0 # 步骤三:创建预定订单 POST http://localhost:8080/hotel-room/order/create Content-Type: application/json { "roomId": 1, "userId": 1, "checkInDate": "2025-06-01", "checkOutDate": "2025-06-03" }参数说明:Postman建议开一个Collection保存这组请求,登录接口返回后自带Cookie,后续请求会自动携带JSessionId。验证时要特别关注两点:一是创建预定时返回的JSON里orderNo是否唯一,连续提交两次如果生成重复单号,说明生成算法需要修复;二是重复预定同一间客房时,服务端是否返回友好错误提示而不是500异常。正常的项目应该返回code:500和message:"客房不存在或不可预定",这是前面@Transactional加的运行时异常被全局异常处理器捕获后的结果。
如果Postman里接口返回正常,但页面上Ajax调用失败,优先检查F12控制台看请求路径——浏览器会自动给相对路径加上当前域名和项目名,如果JS里写了全路径,部署环境变化后就容易404。
5.2 扩展思路:报表统计与数据展示的改造方向
摘要描述里提到报表统计功能,包括入住率报表和客房类型统计。看源码后会发现这类功能通常做得比较基础,一般就是Controller里查一次COUNT,把结果塞进ModelAndView。想扩展成带图表的统计页,有个性价比很高的方案:把统计SQL单独拆出来,用JavaScript图表库渲染。
-- 按客房类型统计入住率 SELECT rt.type_name, COUNT(hf.id) AS total_rooms, SUM(CASE WHEN hr.status = 2 THEN 1 ELSE 0 END) AS checked_in_rooms, CONCAT(ROUND(SUM(CASE WHEN hr.status = 2 THEN 1 ELSE 0 END) / COUNT(hf.id) * 100, 2), '%') AS occupancy_rate FROM room_type rt LEFT JOIN hotel_room hr ON rt.id = hr.room_type_id LEFT JOIN check_in_record hf ON hr.id = hf.room_id AND hf.check_out_time IS NULL GROUP BY rt.id, rt.type_name;这段SQL会返回每个客房类型的总房间数、当前在住房间数和入住率。常见做法是在Service层写一个统计方法,返回List<Map<String, Object>>,Controller转换后以JSON格式传给前端图表库(比如ECharts)渲染。要注意LEFT JOIN的用法——用内连接会漏掉当前没有入住记录的房间类型,统计结果不完整。统计查询对性能不敏感,因为数据量小,但如果入住记录表增长很快,建议在check_out_time字段上建索引,避免全表扫描拖慢页面。
扩展报表功能时有个隐性需求要处理好:报表页面的访问权限。项目经理或财务人员才能看统计,普通前台不应该看到入住率。这个系统已经有了管理员和普通用户的角色区分,统计页面统一放在/admin/statistics路径下,复用LoginInterceptor里对/admin/前缀的拦截即可,不需要额外开发。从那以后,我每次拿到这类SSM毕设项目源码,都会强制走一遍Postman接口验证——浏览器点页面的操作可以被前端脚本掩盖后端错误,接口层的裸调用才能暴露真实问题。希望帮到你。
本文还有配套的精品资源,点击获取