简介一套采用SSM框架与MySQL数据库的酒店管理系统完整项目包含项目代码和数据库脚本面向毕业设计、期末大作业和课程设计等场景也适合正在学习JavaWeb分层开发的读者。zip压缩包共112个文件其中45个Java源文件对应控制层与业务逻辑15个JSP页面负责前端展示15个XML文件承载Spring和MyBatis相关配置另有SQL脚本用于初始化数据库表辅以CSS、JavaScript及图片等静态资源整体大小仅5.94MB结构简洁便于本地部署。目前已有193人学习下载是经过他人验证的参考素材。项目代码注释详细作者自述为98分高分项目并获得导师认可下载后简单配置即可运行可对照查看登录、用户管理、房间预订、订单处理、评论反馈等核心模块的完整实现对理解SSM整合流程和数据库设计思路很有帮助。1. 先把“基于SSMMySQL的酒店管理系统”这个毕业设计项目拆明白“基于SSMMySQL的酒店管理系统”这个题目说白了就是让你用Spring管业务对象、SpringMVC接页面请求、MyBatis写SQL再配一个MySQL数据库脚本来完成客房的查询、预订、入住、退房这一整套流程。很多同学从网上下到一个项目代码包后卡在第一步数据库导不好、Maven依赖下不动、Tomcat起不来最后对着源码干瞪眼。这份落地笔记不是给你堆代码而是把从拿到项目到能在答辩现场流畅演示的每一步讲清楚包括环境版本怎么配、核心代码怎么读、数据库有哪些坑、以及怎么向评委证明“这真的是我写的”。适合正在做SSM毕设的学生也适合想快速跑通一个Java Web项目的初级开发者。2. 环境与数据库准备SSM框架和MySQL的版本搭配与初始化2.1 为什么选SSMMySQL而不直接Spring BootSSM是Spring SpringMVC MyBatis的缩写是Java Web开发里非常经典的一套组合。Spring的IOC容器负责管理Service、Mapper这些BeanSpringMVC负责把前端请求分发给ControllerMyBatis把Mapper接口和你写的SQL映射到数据库操作。MySQL负责把房间、客户、订单这些数据落盘。三者在项目代码里的分工非常清楚Controller层只处理参数和返回结果Service层承载业务规则Mapper层只跟SQL打交道。毕业设计选SSM而不是Spring Boot通常有三个现实原因第一很多学校的大纲和任务书里明确写了“基于SSM框架”如果你擅自改用Spring Boot即使功能做得更漂亮也可能被评委扣“没按任务书实现”的分。第二SSM的配置是显式的Spring配置文件、SpringMVC配置文件、MyBatis配置文件分开答辩时能够逐层展示解释起来比Spring Boot的自动配置更直观。第三SSM项目的代码结构天然是三层正适合课程设计要求的“体现分层思想”。所以哪怕你自己已经会用Spring Boot这个题目下也建议老老实实按SSM来。我一般会建议新手用下面这组版本搭配避开很多“毕业设计专用坑”组件建议版本理由JDK1.8兼容性最好Tomcat和Maven生态都成熟Maven3.6.3稳定阿里云镜像也支持Tomcat8.5.x支持Servlet 3.1和Spring 5搭配顺手Spring / SpringMVC5.2.x与JDK8配合良好安全漏洞也少MyBatis3.5.x和Spring 5无版本冲突MyBatis-Spring2.0.x官方提供的整合包MySQL5.7 或 8.05.7最稳8.0要注意驱动和时区MySQL驱动8.0.x或对应5.1.x驱动版本必须和服务端版本匹配版本不是越高越好。就拿MySQL驱动来说如果你用MySQL 5.7却配了mysql-connector-java 8.0.x一般也能用但连接URL里的serverTimezone必须写否则会报时区错误。反过来MySQL 8.0用了旧驱动可能直接提示“Public Key Retrieval is not allowed”。2.2 用SQL文件初始化数据库建库建表与测试数据的几条关键SQL常见的酒店管理系统项目包里数据库部分会提供一个hotel.sql文件。这个文件一般包含数据库创建、表结构、测试数据和必要的存储过程。拿到手以后不要直接双击用图形化工具导入先用命令行把数据库建好再执行脚本能避免很多编码问题。第一步用账号登录MySQLmysql -u root -p输入密码后执行下面的SQL-- 创建数据库时指定utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS hotel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hotel; -- 用户表 CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, real_name varchar(50) DEFAULT NULL, role tinyint(4) DEFAULT 1 COMMENT 1前台 2管理员, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的逻辑是先制定字符集再建表。utf8mb4比utf8多支持表情符号而且能完全覆盖中文。COLLATE utf8mb4_general_ci是排序规则ci表示大小写不敏感适合用户名这种字段。表名我见过很多版本有的叫sys_user有的叫t_admin不影响整体思路但t_user更贴近“前台和管理员统一登录”的场景。接着插入一条测试管理员数据INSERT INTO t_user (username, password, real_name, role) VALUES (admin, MD5(123456), 系统管理员, 2);这里用MD5(123456)在SQL里直接生成密文适合演示。但要注意正式项目不应该在SQL里写死密码应该由Service层加密入库。如果你拿到的项目里登录模块已经用了加盐加密就按项目的来不要为了省事改成MD5。初始化完用户表还要把房间表跑起来。酒店管理系统最核心的资源就是房间房间表至少要包含房间号、类型、价格和状态四个字段。下面这段是我常用的最小表结构CREATE TABLE t_room ( id int(11) NOT NULL AUTO_INCREMENT, room_no varchar(10) NOT NULL, type varchar(20) NOT NULL, price decimal(10,2) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0空闲 1已预订 2已入住 3维修, PRIMARY KEY (id), UNIQUE KEY uk_room_no (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段用tinyint而不是varchar是我一直坚持的做法。理由很简单状态是一个有限集合用数字表示Java代码里写常量比如RoomStatus.FREE0既省空间又便于写WHERE status 0这种查询。如果直接用字符串空闲万一项目里有人写了空闲 带空格数据就出问题。2.3 配置db.properties和mybatis-config.xml让项目连上你的MySQL数据库建好后下一步是让Java项目连接上MySQL。SSM项目里连接信息通常放在db.propertiesMyBatis的全局配置放在mybatis-config.xmlSpring配置文件负责把两者装配起来。典型的db.properties长这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456这里的参数每个都值得说清楚。驱动名com.mysql.cj.jdbc.Driver是MySQL 8.0的驱动类如果你用的是5.1.x驱动要换成com.mysql.jdbc.Driver。useUnicodetruecharacterEncodingutf8保证Java到MySQL之间的中文不变成问号。serverTimezoneAsia/Shanghai专门解决MySQL 8.0的时区报错如果你在上海就写Asia/Shanghai如果你在香港就写Asia/Hong_Kong。allowPublicKeyRetrievaltrue是MySQL 8.0在JDBC首次连接时为了加密认证需要加的很多项目卡在“Public Key Retrieval is not allowed”就是少了这个参数。然后是mybatis-config.xml?xml version1.0 encodingUTF-8? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings typeAliases package namecom.hotel.entity/ /typeAliases /configurationmapUnderscoreToCamelCase的值设为true后MyBatis会自动把数据库字段room_no映射成Java属性的roomNo这个映射关系能省掉你一大半resultMap配置。logImpl设为STDOUT_LOGGING跑起来时控制台会打印SQL语句排错时非常有用。typeAliases里配置实体包路径这样Mapper.xml里写resultTypeRoom就可以不用写全限定类名。最后在Spring配置里声明数据源和SqlSessionFactory。这里以Druid连接池为例bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations list valueclasspath:mapper/*.xml/value /list /property /beaninitialSize是启动时建立的连接数maxActive是最大连接数。毕设项目并发压力很小5和20完全够用不要故意调大。配置到这里数据库和连接层已经可以跑通了。3. 核心代码落地从登录鉴权到客房管理的SSM三层实现3.1 实体、Mapper与Service层怎么拆SSM项目的三层结构是固定套路。前端请求到达ControllerController不写业务逻辑而是调用ServiceService里面写“先检查房间状态再下单”这类的规则Service调用Mapper接口Mapper接口对应XML里的SQL。这样拆的好处是哪一层出了问题你能立刻定位。先从实体类开始。房间表t_room对应的Java类Room.javapackage com.hotel.entity; import java.math.BigDecimal; public class Room { private Integer id; private String roomNo; private String type; private BigDecimal price; private Integer status; // 0空闲 1已预订 2已入住 3维修 public Integer getId() { return id; } public void setId(Integer id) { this.id id; } // 其他getter/setter按同样的方式生成这里省略 }注意这里的roomNo对应数据库的room_no。如果你在MyBatis配置里开了mapUnderscoreToCamelCase可以不用写resultMap如果没有开就得在Mapper.xml里单独映射。我见过不少同学图省事关闭了驼峰映射结果每条查询都要写一堆result columnroom_no propertyroomNo/真是得不偿失。Mapper接口是MyBatis的第一层入口package com.hotel.mapper; import com.hotel.entity.Room; import org.apache.ibatis.annotations.Param; import java.util.List; public interface RoomMapper { ListRoom selectRoomPage(Param(offset) int offset, Param(limit) int limit); int countRooms(); }对应的RoomMapper.xml?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.hotel.mapper.RoomMapper select idselectRoomPage resultTypeRoom SELECT id, room_no, type, price, status FROM t_room ORDER BY id LIMIT #{offset}, #{limit} /select select idcountRooms resultTypeint SELECT COUNT(*) FROM t_room /select /mapper这里为什么用Param因为MyBatis在处理多参数方法时如果不写ParamXML里只能通过param1、param2这种序号引用一旦参数顺序变了SQL就跟着错。写上Param之后XML里#{offset}和#{limit}就跟你定义的名称一致可读性和稳定性都更好。Service层则是业务封装的地方。一个最简单的分页查询可以这样实现package com.hotel.service; import com.hotel.entity.Room; import com.hotel.mapper.RoomMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; Service public class RoomService { Autowired private RoomMapper roomMapper; public MapString, Object getRooms(int pageNum, int pageSize) { int offset (pageNum - 1) * pageSize; ListRoom list roomMapper.selectRoomPage(offset, pageSize); int total roomMapper.countRooms(); MapString, Object result new HashMap(); result.put(list, list); result.put(total, total); return result; } }这个Service方法把“偏移量计算”放在Java层做SQL层只负责接收offset和limit。pageNum是页码从1开始pageSize是每页条数。如果pageNum传1offset就是0正好取第一页。3.2 用SpringMVC写一个带拦截器的登录接口登录是酒店管理系统的门面。它的本质是接收用户名和密码校验是否匹配把用户对象放进Session后续请求通过拦截器判断是否登录。Controller里的代码package com.hotel.controller; import com.hotel.entity.User; import com.hotel.service.UserService; import com.hotel.util.MD5Utils; import com.hotel.util.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpSession; Controller public class LoginController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) ResponseBody public Result login(String username, String password, HttpSession session) { // 演示项目把密码做MD5后与数据库密文比对 User user userService.login(username, MD5Utils.md5(password)); if (user ! null) { session.setAttribute(loginUser, user); return Result.success(登录成功); } return Result.error(用户名或密码错误); } }注意ResponseBody它让返回值直接以JSON格式写回浏览器而不是走视图解析器。MD5Utils.md5()是一个工具方法内部调用MessageDigest实现MD5摘要。项目代码里如果自带工具类优先用它别自己再写一套。要拦住未登录的请求需要配置拦截器。在spring-mvc.xml里mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.hotel.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors拦截器类实现HandlerInterceptor接口在preHandle里判断Sessionpackage com.hotel.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { // 未登录则重定向到登录页 response.sendRedirect(request.getContextPath() /loginPage); return false; } return true; } }这里的逻辑很直白拦截器在进入Controller之前执行发现Session里没有用户就重定向。exclude-mapping里的/static/**必须写否则CSS、JS、图片全部被拦页面会变成纯HTML。3.3 分页查询客房列表PageHelper参数和前端渲染项目里如果已经集成了PageHelper分页代码可以更简洁。PageHelper是MyBatis的分页插件原理是在执行你的查询之前拦截SQL并自动拼接LIMIT。用法是import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; public MapString, Object getRoomsWithPageHelper(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListRoom list roomMapper.selectRoomList(); PageInfoRoom pageInfo new PageInfo(list); MapString, Object result new HashMap(); result.put(list, pageInfo.getList()); result.put(total, pageInfo.getTotal()); return result; }注意PageHelper.startPage()只对“下一条”Mapper查询生效。如果你在它和查询之间又调用了其他Mapper方法分页就会串到别的地方这是一个经常翻车的地方。另外查询语句本身不要手写LIMIT否则插件会根据分页参数再拼接一次导致SQL语法错误。前端如果用的是JSP列表渲染通常是循环% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table tr th房间号/th th类型/th th价格/th th状态/th /tr c:forEach items${page.list} varroom tr td${room.roomNo}/td td${room.type}/td td${room.price}/td td${room.status 0 ? 空闲 : room.status 1 ? 已预订 : 已入住}/td /tr /c:forEach /table这里注意价格字段是BigDecimal类型JSP直接输出不会有问题但如果要做金额计算不要用double或float否则会出现0.1 0.2 0.30000000000000004这种尴尬结果。金额计算全流程都用BigDecimal这是写酒店系统的基本素质。4. 数据库设计与订单闭环房间状态、价格计算与并发防超卖4.1 表结构设计room、order、customer的关系和字段约束酒店管理系统的数据核心是“房、客、单”三张表。房间可以单独管理客户可以单独管理订单则把它们关联起来。毕设项目里我的建议是至少建五张表用户表、房间表、客户表、订单表、订单明细表。如果只是纯房间预订没有餐饮消费订单明细可以省略。但哪怕没有明细表订单表也必须有外键指向房间和客户。下面这段SQL是从我常用模板里精简出来的CREATE TABLE t_customer ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, id_card varchar(18) NOT NULL, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, room_id int(11) NOT NULL, customer_id int(11) NOT NULL, order_date date NOT NULL, days int(11) NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已入住 3已退房 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_order_room (room_id), KEY idx_order_customer (customer_id), CONSTRAINT fk_order_room FOREIGN KEY (room_id) REFERENCES t_room(id), CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES t_customer(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型上的细节值得说清楚。id_card用varchar(18)因为身份证号以0开头也不受影响且根本不做算术运算如果自作主张用bigint前导0会丢失这是非常经典的翻车点。total_amount用decimal(10,2)而不是float原因和前面说的一样金额必须精确到分。order_no是人工生成的业务单号不能依赖自增ID因为自增ID在导出报表时会暴露订单量所以用时间戳加随机数生成。外键在这里的作用是保证数据一致性你不可能插入一个room_id999的订单因为房间表中没有999。某些生产系统为了性能会去掉外键但毕业设计恰恰相反保留外键能体现你懂参照完整性。4.2 订房操作的事务控制Transactional在什么时候真正生效订房不是一个单表操作。在用户点击“下单”后系统至少要完成三件事插入订单记录、把房间状态改成“已预订”、如果是老客户还要判断是否要更新客户信息。这三件事必须同时成功或同时失败否则就会出现“订单存在但房间还是空闲”的数据不一致。Spring的解决方式是Transactional注解。在Service方法上加这个注解后Spring会为该方法创建一个数据库事务方法内任何一步抛出异常整个事务回滚。import org.springframework.transaction.annotation.Transactional; Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private RoomMapper roomMapper; Transactional(rollbackFor Exception.class) public boolean createOrder(Integer roomId, Integer customerId, Date orderDate, int days, BigDecimal price) { // 1. 查询房间当前状态 Room room roomMapper.selectRoomById(roomId); if (room null || room.getStatus() ! 0) { throw new RuntimeException(房间不可预订); } // 2. 生成订单号并插入订单 String orderNo H System.currentTimeMillis(); Order order new Order(); order.setOrderNo(orderNo); order.setRoomId(roomId); order.setCustomerId(customerId); order.setOrderDate(orderDate); order.setDays(days); order.setTotalAmount(price.multiply(BigDecimal.valueOf(days))); order.setStatus(0); orderMapper.insertOrder(order); // 3. 更新房间状态为已预订 int rows roomMapper.updateRoomStatus(roomId, 0, 1); if (rows 0) { throw new RuntimeException(房间状态更新失败); } return true; } }这段代码的逻辑是插入订单后立刻更新房间状态如果更新失败插入操作也一起回滚。rollbackFor Exception.class的意思是无论捕获到检查异常还是运行时异常都回滚如果不写这个参数Spring默认只对运行时异常回滚受检异常不会回滚。很多同学在这个坑上翻车方法里抛了个IOException受检异常事务没有回滚数据就半截入库了。Transactional不是万能的它有几个典型失效场景在同一个类内部通过this调用另一个Transactional方法事务不会生效方法是private或finalSpring无法生成代理对象事务也不会生效方法里把异常用try-catch吞掉Spring感知不到异常同样不会回滚。4.3 状态机从“已预订”到“已入住”再到“已退房”酒店的客房状态是一个典型的状态机。我用一个简单的数字约定0空闲、1已预订、2已入住、3维修。订房时从0变成1客人到店办入住时从1变成2退房时从2变成0并且订单状态变成3。状态迁移不能乱跳比如“已入住”不能直接切到“空闲”必须经过“退房”操作。这里有一个高并发场景同一间房两个人同时下单。如果你的代码先“查询状态0”再“更新状态1”中间大概率产生并发问题。解决办法是用“条件更新”代替“先查后改”UPDATE t_room SET status 1 WHERE id #{roomId} AND status 0在Mapper接口里是这样写的int updateRoomStatus(Param(roomId) Integer roomId, Param(expectStatus) Integer expectStatus, Param(newStatus) Integer newStatus);SQLupdate idupdateRoomStatus UPDATE t_room SET status #{newStatus} WHERE id #{roomId} AND status #{expectStatus} /updateupdateRoomStatus返回的是受影响的行数。如果返回0说明当前房间状态已经不是期望的值这时候就要放弃下单或者提示用户“房间已被预订”。这种方式叫乐观锁它没有用数据库的SELECT ... FOR UPDATE但因为更新条件里带了版本状态并发下也只有一个请求能把状态改过来。退房操作同理UPDATE t_order SET status 3, actual_checkout_time NOW() WHERE order_no #{orderNo} AND status 2; UPDATE t_room SET status 0 WHERE id #{roomId} AND status 2;这里先改订单再改房间一个事务提交。如果先改房间再改订单万一订单更新失败房间已经变成空闲但订单还是“入住中”结账时就会对不上账。所以“订单状态”和“房间状态”的迁移顺序也是你在答辩时需要重点解释的业务闭环。5. 毕业设计避坑指南SSMMySQL项目常见问题与排查下面这五条是我在给毕业生排查SSMMySQL项目时遇到频率最高的问题。每条都按现象、原因、解决的顺序写你可以直接对着日志搜关键词。5.1 数据库中文乱码插入和查询都显示问号现象运行项目后在页面上输入中文保存到MySQL里变成??或者从数据库读出来也是问号。原因多半是数据库连接URL里没有指定characterEncodingutf8。也有可能是建库时没有指定utf8mb4或者MySQL服务端默认字符集不是utf8。JSP页面本身没有设置pageEncoding也会参与捣乱。解决按顺序检查三步。第一步在db.properties连接URL里加上useUnicodetruecharacterEncodingutf8。第二步确认建库语句是DEFAULT CHARACTER SET utf8mb4如果数据库已经建了就执行ALTER DATABASE hotel CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。第三步JSP页面头部加一行% page contentTypetext/html;charsetUTF-8 languagejava %如果是SSM的JSON接口返回乱码还要在spring-mvc.xml里配置消息转换器或者使用RequestMapping(producesapplication/json;charsetUTF-8)。排查时先用命令行查SHOW VARIABLES LIKE character_set%;确认服务端、连接、数据库都是utf8mb4然后再排查Java端。如果你是在IDEA里直接运行SQL脚本出现的乱码还要检查File - Settings - File Encodings把Global Encoding和Project Encoding都设为UTF-8。5.2 Maven依赖下载失败或jar包冲突现象IDEA导入项目后右下方一直转圈maven-resources-plugin报错或者启动时提示ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet。原因多数是Maven默认中央仓库在国外下载不完整或者项目里同时引了不同版本的Spring、MyBatis-Spring导致依赖树冲突。解决在Maven的settings.xml里配置阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirror配置后在IDEA里执行mvn clean然后mvn dependency:tree查看依赖树。如果发现两个版本重复用exclusions把传递依赖排除掉。例如项目里既想用PageHelper又不想让它带着旧MyBatisdependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.2/version exclusions exclusion groupIdorg.mybatis/groupId artifactIdmybatis/artifactId /exclusion /exclusions /dependency排除依赖后要重新加载Maven项目否则IDEA的缓存里还是旧依赖。如果依赖一直下载不完可以在IDEA的Maven面板里点Reload All Projects或者执行mvn -U强制更新快照。5.3 连接池报错时区问题和Access denied现象启动Tomcat时日志里出现Cannot create PoolableConnectionFactory或The server time zone value йʱ is unrecognized。原因MySQL 8.0的时区设置和驱动的默认时区不一致导致连接被拒绝。另一个常见的Access denied for user rootlocalhost则纯粹是用户名或密码错了但很多人会忽略连接URL和db.properties是否真的被加载。解决先确认MySQL账户能登录。命令行执行mysql -u root -p如果登录失败是数据库用户密码问题在MySQL里重置密码或用正确的密码更新db.properties。如果登录成功但项目连不上重点检查连接URL里的serverTimezoneAsia/Shanghai。同时记得MySQL驱动版本要和数据库版本匹配8.0驱动配5.7数据库通常需要在URL里额外加useSSLfalseallowPublicKeyRetrievaltrue。最后确认db.properties里的密码没有前后空格这个错误特别隐蔽有时复制粘贴会带上一个空格眼睛看半天发现不了。Windows下先确认MySQL服务是否启动运行net start mysqlLinux下用systemctl status mysqld查看服务状态。5.4 Tomcat部署后静态资源404现象页面能访问但CSS、JS、图片全部加载不出来F12看到一堆404。原因SpringMVC的拦截器配置了/**把静态资源请求也拦截了或者页面引用了WEB-INF下的静态资源而WEB-INF本身不对浏览器开放。解决第一在spring-mvc.xml里配置资源映射mvc:resources mapping/static/** location/static//第二页面里用${pageContext.request.contextPath}拼资源路径例如link relstylesheet href${pageContext.request.contextPath}/static/css/main.css第三千万别把CSS和JS放在WEB-INF目录下那是安全区浏览器无法直接访问必须通过Servlet转发。很多旧项目习惯把整站页面放在WEB-INF下这时要在拦截器里放行静态资源并且把静态资源放到webapp/static下。如果已经配了mvc:annotation-driven也要确保它和mvc:resources同时存在否则控制器注解可能不生效。5.5 JDK版本不匹配导致ClassNotFound或UnsupportedClassVersionError现象项目跑起来报UnsupportedClassVersionError提示class file has wrong version或者Tomcat启动就闪退。原因本地JDK版本和项目编译的Java字节码版本不一致。比如项目用JDK 11编译而你运行环境的Tomcat是JDK 8加载不了高版本的class文件。另一种情况是IDEA的Project Structure里SDK是17但Maven的pom.xml里source/target还是1.8导致编译混乱。解决统一三处版本。第一检查Java环境java -version。第二在pom.xml里明确指定properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties第三在IDEA的File - Project Structure - Project里把SDK设为1.8Settings - Build - Compiler - Java Compiler里把字节码版本设为1.8。改完后执行mvn clean删除target目录再重启。如果Tomcat启动日志的Caused by后面还有一长串类名多半是依赖版本没对齐优先排查pom.xml里Spring和MyBatis的版本。6. 如何在答辩时把项目讲出亮点验证数据与代码审查技巧6.1 准备一组可复现的演示数据答辩现场最怕的就是“一紧张不知道点什么”。我习惯提前准备三个场景登录、查空闲房、订房后退房。每个场景都有固定的按键路径和预期结果。为了让每一步都能被看清楚我会在数据库里准备一组“一眼能看出状态变化”的数据比如10间房房号从101到110类型和价格都不同。不要用刚插入的默认数据因为状态全是0看不出业务逻辑。推荐在答辩前把整个流程完整走一遍然后用Navicat或命令行把hotel数据库导出成SQL文件再删掉数据库重新导入一次。如果能顺利重来一遍说明你的数据库脚本和代码是真正可复现的这是对“项目代码数据库”这个任务书最有力的回应。6.2 代码审查用自己的话解释三个核心方法评委不问“这个功能怎么实现”而是会问“为什么这么写”。你要能脱稿讲清楚三个方法第一个是登录流程讲讲Session和拦截器的配合第二个是订房事务讲讲Transactional回滚了哪几步第三个是分页查询讲讲PageHelper为什么不能跨查询生效。回答时尽量用自己的话比如“房间里有一个状态位下单时先插订单然后带条件更新房间状态如果更新失败就抛异常让整个事务回滚这样不会出现订单和房间对不上的情况”。6.3 用日志和断点证明你的事务“真的回滚了”如果你想让答辩更有说服力可以在演示时故意展示一次回滚。在createOrder方法里的updateRoomStatus后面抛一个RuntimeException观察控制台打印的SQL先出现了insert into t_order再出现update t_room然后是异常堆栈最后看到Rolling back或去数据库查订单表没有新增记录。这就是“用事实说话”。做这个实验前先备份数据库避免把测试数据搞乱。我个人的习惯是指导老师验收前固定用一份演示数据库做一次全流程回归确认完再清空敏感测试数据把数据库重新导出成干净的初始化脚本。这样交上去的“项目代码数据库”才算完整。希望帮到你。本文还有配套的精品资源点击获取