简介:本资源为基于SSM框架、MySQL数据库与JSP技术开发的酒店预订管理系统完整项目包,面向计算机相关专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者。系统按角色划分为前台用户与后台管理员两大模块:前台涵盖注册登录、客房列表模糊搜索、在线预订、订单查询、资料维护与密码修改;后台则提供系统设置、用户管理、系统日志、楼层与房型管理、房间管理、客户管理、预订与入住管理以及营业统计等功能,可满足中小型酒店日常运营的信息化需求。压缩包为zip格式,大小约35.29MB,内含项目源码、毕业论文及开发报告等文件,论文部分详细阐述开发背景、需求分析、系统设计与测试结果,报告则梳理操作流程与系统特点。目前已有83人学习下载,适合作为SSM整合实战、JSP页面交互与MySQL数据库设计的综合练习素材,也可为毕业设计撰写提供完整参考。
1. 酒店预订管理系统:从 SSM 到 JSP 的完整落地路径
很多做毕业设计或课程项目的同学,一看到“SSM 框架 + MySQL + JSP”这个组合就头大——不是技术本身有多难,而是不知道一个能跑起来的酒店预订管理系统到底该长什么样。这个标题背后其实是一套非常经典的 Java Web 技术栈:Spring 做 IoC 和事务管理,SpringMVC 处理请求路由和参数绑定,MyBatis 负责 SQL 映射和结果集封装,MySQL 存数据,JSP 做视图渲染。它解决的核心问题是:如何用一套结构清晰、分层合理的代码,把“房间查询、在线预订、订单管理、入住退房”这条业务链路完整跑通。适合谁?适合正在做课程设计、毕业设计,或者想用一个真实业务场景把 SSM 串起来练手的开发者。我见过太多人卡在“环境搭好了但不知道下一步写什么”,这篇就按实际开发顺序,把每一步拆开讲。
2. 环境搭建与项目骨架:别急着写业务代码
2.1 版本选型与依赖清单
SSM 这套东西最怕版本打架。我一般会锁定一组经过验证的版本组合,避免出现“昨天还能跑,今天换个依赖就 404”的玄学问题。下面这张表是我在多个模拟项目中反复用过的配置,直接抄作业就行。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容性最好,别用高版本踩坑 |
| Maven | 3.6+ | 依赖管理,统一构建 |
| Spring | 5.3.x | 核心容器和 MVC |
| MyBatis | 3.5.x | SQL 映射 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动类名变化 |
| Druid | 1.2.x | 连接池,监控方便 |
| JSP | 2.3 | 视图层,配合 JSTL |
| Tomcat | 8.5 或 9.0 | 别用 10,JSP 包名变了 |
提示:MySQL 8.0 的驱动类是
com.mysql.cj.jdbc.Driver,URL 要加serverTimezone=Asia/Shanghai,否则连接直接报错。
2.2 Maven 依赖配置与目录结构
项目骨架用 Maven 的 webapp 模板生成,目录结构按标准分层来:controller、service、service.impl、mapper、entity、util,JSP 放在WEB-INF/jsp下,避免被直接访问。下面这段pom.xml只列核心依赖,完整版按需补全。
<!-- pom.xml 核心依赖片段 --> <dependencies> <!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency> <!-- SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>逻辑说明:Spring 和 SpringMVC 版本必须一致,MyBatis-Spring 负责把 SqlSession 交给 Spring 管理。Druid 连接池不是必须,但它的监控页面在调试 SQL 时非常有用。JSTL 用于 JSP 页面循环和判断,不引入的话<c:forEach>直接报错。
参数说明:mysql-connector-java的版本要和 MySQL 服务端匹配,8.0 的驱动连 5.7 也能用,但反过来会出问题。mybatis-spring的 2.x 版本对应 MyBatis 3.5+,别降到 1.x。
2.3 数据库建表与连接配置
酒店预订的核心表就四张:用户表、房间表、订单表、房间类型表。建表时注意外键和索引,订单表的room_id和user_id一定要加索引,否则数据量上来后查询慢到怀疑人生。
-- 房间表:存储房间基本信息 CREATE TABLE `room` ( `id` int(11) NOT NULL AUTO_INCREMENT, `room_number` varchar(10) NOT NULL COMMENT '房间号', `type_id` int(11) NOT NULL COMMENT '房间类型ID', `price` decimal(10,2) NOT NULL COMMENT '单价', `status` tinyint(1) DEFAULT '0' COMMENT '0空闲 1已预订 2入住中', PRIMARY KEY (`id`), KEY `idx_type` (`type_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:核心业务表 CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `room_id` int(11) NOT NULL, `check_in` date NOT NULL COMMENT '入住日期', `check_out` date NOT NULL COMMENT '退房日期', `total_price` decimal(10,2) DEFAULT NULL, `status` tinyint(1) DEFAULT '0' COMMENT '0待确认 1已确认 2已入住 3已完成 4已取消', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_room` (`room_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:status字段用数字枚举,比字符串省空间且查询快。订单表不设外键约束,用应用层保证一致性,避免级联删除带来的数据丢失风险。check_in和check_out用 date 类型,计算天数时直接用 SQL 的DATEDIFF函数。
参数说明:utf8mb4支持 emoji,虽然酒店系统用不上,但养成习惯没坏处。decimal(10,2)保证金额精度,别用 float,否则 0.1+0.2 的问题会让你在算总价时翻车。
3. 核心业务实现:从登录到订单的完整链路
3.1 SpringMVC 控制器与参数绑定
控制器层只做三件事:接收参数、调用 Service、返回视图或 JSON。酒店系统里最典型的接口是房间列表查询和订单创建。下面这个RoomController展示了分页查询和日期参数绑定的写法。
@Controller @RequestMapping("/room") public class RoomController { @Autowired private RoomService roomService; // 分页查询房间列表,支持按类型和状态筛选 @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(required = false) Integer typeId, @RequestParam(required = false) Integer status, Model model) { PageInfo<Room> pageInfo = roomService.findByPage(page, 10, typeId, status); model.addAttribute("pageInfo", pageInfo); model.addAttribute("typeId", typeId); model.addAttribute("status", status); return "room/list"; // 对应 /WEB-INF/jsp/room/list.jsp } // 创建订单,日期格式由 @DateTimeFormat 自动转换 @RequestMapping("/book") @ResponseBody public Map<String, Object> book(@RequestParam Integer roomId, @DateTimeFormat(pattern = "yyyy-MM-dd") Date checkIn, @DateTimeFormat(pattern = "yyyy-MM-dd") Date checkOut, HttpSession session) { Map<String, Object> result = new HashMap<>(); User user = (User) session.getAttribute("loginUser"); if (user == null) { result.put("code", 401); result.put("msg", "请先登录"); return result; } try { roomService.bookRoom(user.getId(), roomId, checkIn, checkOut); result.put("code", 200); result.put("msg", "预订成功"); } catch (Exception e) { result.put("code", 500); result.put("msg", e.getMessage()); } return result; } }逻辑说明:@RequestParam的defaultValue保证分页参数不传时也有默认值。@DateTimeFormat解决 JSP 表单提交的字符串日期到 Date 的转换,不加这个注解会直接抛 400 错误。@ResponseBody把返回的 Map 自动序列化为 JSON,前端用 Ajax 接收。
参数说明:page默认 1,typeId和status允许为空表示不筛选。book方法里从 Session 取用户,没登录直接返回 401,避免未登录下单。异常捕获后返回错误信息,前端统一提示。
3.2 Service 层事务与订单冲突处理
订单创建是典型的并发场景:两个用户同时预订同一间房,如果不在 Service 层加锁或事务控制,就会出现超卖。我一般用“先查后改”加乐观锁的方式,在房间表加一个version字段,更新时检查版本号。
@Service public class RoomServiceImpl implements RoomService { @Autowired private RoomMapper roomMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) public void bookRoom(Integer userId, Integer roomId, Date checkIn, Date checkOut) { // 1. 查询房间当前状态 Room room = roomMapper.selectById(roomId); if (room == null || room.getStatus() != 0) { throw new RuntimeException("房间不可预订"); } // 2. 检查日期区间是否已被占用 int conflict = orderMapper.countConflict(roomId, checkIn, checkOut); if (conflict > 0) { throw new RuntimeException("该时间段已被预订"); } // 3. 计算总价 long days = (checkOut.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24); BigDecimal total = room.getPrice().multiply(new BigDecimal(days)); // 4. 更新房间状态为已预订 room.setStatus(1); int updated = roomMapper.updateStatusWithVersion(roomId, 1, room.getVersion()); if (updated == 0) { throw new RuntimeException("房间已被他人预订,请刷新重试"); } // 5. 写入订单 Order order = new Order(); order.setUserId(userId); order.setRoomId(roomId); order.setCheckIn(checkIn); order.setCheckOut(checkOut); order.setTotalPrice(total); order.setStatus(0); orderMapper.insert(order); } }逻辑说明:@Transactional保证 4 和 5 两步要么都成功要么都回滚。countConflict用 SQL 的BETWEEN判断日期重叠,避免同一房间同一时段被重复预订。updateStatusWithVersion是乐观锁,版本号不匹配说明有人抢先改了,直接抛异常让用户重试。
参数说明:rollbackFor = Exception.class确保所有异常都触发回滚,默认只回滚运行时异常。days计算用毫秒差除以一天的毫秒数,注意checkOut当天不算入住,所以实际住几晚就是几天。
3.3 MyBatis 映射文件与动态 SQL
MyBatis 的 XML 映射文件是 SSM 里最容易写错的地方。酒店系统里房间列表查询需要根据类型和状态动态拼接条件,订单冲突检查需要日期区间比较。下面这两个 SQL 片段是核心。
<!-- RoomMapper.xml 分页查询 --> <select id="findByPage" resultType="Room"> SELECT * FROM room <where> <if test="typeId != null"> AND type_id = #{typeId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY room_number ASC LIMIT #{offset}, #{limit} </select> <!-- OrderMapper.xml 日期冲突检查 --> <select id="countConflict" resultType="int"> SELECT COUNT(*) FROM orders WHERE room_id = #{roomId} AND status IN (0, 1, 2) AND check_in < #{checkOut} AND check_out > #{checkIn} </select>逻辑说明:<where>标签自动处理第一个AND,避免拼接出WHERE AND的语法错误。日期冲突的判断逻辑是:已有订单的入住日期小于新订单的退房日期,且已有订单的退房日期大于新订单的入住日期,两个条件同时满足才算重叠。status IN (0,1,2)排除已取消和已完成的订单。
参数说明:offset和limit由 Service 层计算,offset = (page - 1) * limit。<和>是 XML 转义,直接写<和>会解析失败。
4. 避坑与排查:那些让我熬夜的瞬间
4.1 JSP 页面中文乱码
现象:表单提交后数据库里存的是问号,或者页面显示乱码。原因:JSP 页面编码、请求编码、数据库编码三者不一致。解决:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" %>,web.xml 里配CharacterEncodingFilter强制 UTF-8,数据库连接 URL 加useUnicode=true&characterEncoding=utf8。三个地方缺一不可。
4.2 Spring 事务不生效
现象:Service 方法抛异常了,但订单还是写进去了。原因:事务方法不是 public,或者同类内部调用绕过了代理。解决:@Transactional只对 public 方法生效,且必须通过代理对象调用。如果 Service 里 A 方法调 B 方法,B 的事务不会生效,要把 B 抽到另一个 Service 或注入自身代理。
4.3 MyBatis 返回 null 但 SQL 能查到
现象:Mapper 接口返回 null,但手动在数据库执行 SQL 有结果。原因:实体类字段名和数据库列名不一致,或者没配mapUnderscoreToCamelCase。解决:在 MyBatis 配置文件里加<setting name="mapUnderscoreToCamelCase" value="true"/>,让room_number自动映射到roomNumber。或者手动写<resultMap>做映射。
4.4 Tomcat 启动报 ClassNotFoundException
现象:明明 Maven 依赖都下了,Tomcat 启动就是找不到类。原因:依赖没有部署到WEB-INF/lib下。解决:IDEA 里检查 Project Structure 的 Artifacts,把 Maven 依赖加到 lib 目录。或者用mvn package打 war 包部署,别用 exploded 方式。
4.5 日期格式转换异常
现象:提交2024-01-01报MethodArgumentTypeMismatchException。原因:SpringMVC 默认不支持字符串到 Date 的转换。解决:加@DateTimeFormat(pattern = "yyyy-MM-dd")注解,或者在 SpringMVC 配置里注册FormattingConversionServiceFactoryBean全局处理。
5. 进阶技巧:让系统更接近真实项目
5.1 用拦截器做登录校验和权限控制
每个 Controller 都写一遍 Session 判断太蠢了。我一般写一个LoginInterceptor,在preHandle里检查 Session,排除登录和注册接口。配置在 SpringMVC 的<mvc:interceptors>里,指定拦截路径和排除路径。这样 Controller 里只关心业务,权限逻辑统一收口。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") == null) { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/user/login"); return false; } return true; } }逻辑说明:preHandle返回 false 会中断请求,不再执行 Controller。sendRedirect要加contextPath,否则部署到非根路径下会 404。排除路径在 XML 里配<mvc:exclude-mapping path="/user/login"/>。
5.2 订单状态机与定时任务
订单状态从待确认到已完成,中间有取消和超时未支付的情况。我一般用状态机思路,每个状态只允许特定操作。比如待确认可以取消,已入住不能取消。再加一个定时任务,扫描超过 30 分钟未确认的订单自动取消,释放房间。
@Component public class OrderTimeoutTask { @Autowired private OrderMapper orderMapper; @Autowired private RoomMapper roomMapper; // 每 10 分钟执行一次 @Scheduled(cron = "0 0/10 * * * ?") @Transactional public void cancelTimeoutOrders() { List<Order> timeoutOrders = orderMapper.findTimeoutOrders(30); for (Order order : timeoutOrders) { order.setStatus(4); // 已取消 orderMapper.updateStatus(order.getId(), 4); roomMapper.updateStatus(order.getRoomId(), 0); // 释放房间 } } }逻辑说明:@Scheduled需要开启<task:annotation-driven/>。findTimeoutOrders查询创建时间超过 30 分钟且状态为待确认的订单。释放房间时把状态改回空闲,让其他人可以预订。
参数说明:cron表达式0 0/10 * * * ?表示每 10 分钟执行一次。30是超时分钟数,按业务调整。
5.3 用 Druid 监控 SQL 性能
Druid 的监控页面能直接看到慢 SQL 和执行次数。配置StatViewServlet后访问/druid/index.html,输入用户名密码就能看。我一般会关注执行时间超过 1 秒的 SQL,多半是缺索引或者写了SELECT *全表扫描。订单表的日期范围查询一定要走索引,否则数据量到几万条时页面直接卡死。
注意:监控页面只在内网或开发环境开,生产环境记得关掉,避免暴露数据库信息。
5.4 毕业论文与报告里的技术描述技巧
写论文时别堆砌“SSM 框架实现了分层解耦”这种空话。我一般会配一张架构图,然后重点写三个地方:一是订单冲突的乐观锁实现,二是事务的传播行为配置,三是 JSP 页面如何用 JSTL 做数据渲染。报告里放一段核心代码加注释,比大段文字描述更有说服力。测试部分用表格列出正常流程和异常流程的用例,比如“同一房间同一时段重复预订”预期返回错误提示,实际结果一致。
这套东西我前后搭过三次,每次都会在日期处理和事务边界上翻车。后来养成习惯:先把数据库建好,再写 Mapper 和 SQL 测试,最后才动 Controller 和 JSP。顺序反了,调试成本翻倍。希望帮到你。
本文还有配套的精品资源,点击获取