SSM酒店预订系统这个题目我带了这么多届毕业设计几乎每年都会遇到几个学生选它。说实话这题目虽然老但确实经典到值得认真聊一聊。它不像电商、社交那种大而全的系统业务边界清晰功能复杂度适中用SSM这套组合来做刚好能把Java Web开发中最重要的一条链路完整走通——Spring管对象、SpringMVC接请求、MyBatis碰数据库。把这个项目吃透你就能回答清楚“一个Web应用从浏览器请求到数据库返回结果中间到底发生了什么”这个问题而这不正是毕业设计答辩时老师最想听到的吗。这篇文章我不打算给你贴一堆源码而是想把SSM酒店预订系统从选题思路、数据库设计、SSM核心机制、环境搭建到常见问题排查从头到尾拆一遍。内容偏向实操经验和避坑心得适合正在做毕设、或者想系统复习SSM开发的同学参考。1. 内容整体设计与思路拆解1.1 为什么SSM是毕业设计的黄金组合先聊一个经常被学生问的问题都什么年代了为什么还要用SSM做毕设我的答案是毕业设计的核心目的从来不是追新而是证明你掌握了软件开发的基本功。SSM是Spring、SpringMVC、MyBatis三个框架的组合它呈现出非常清晰的分层架构思想——表现层、业务层、持久层各司其职每一层都能单独拿出来讲原理。这种分工明确的技术栈最适合用来展示你对分层架构的理解。对比Spring Boot后者虽然配置简单、开发效率高但很多学生做完项目连“自动配置到底配了什么”都说不清楚。答辩时被追问一句“你的数据库连接是怎么初始化的”就卡住了。SSM则恰好相反所有配置都要你自己写一遍每个Bean怎么创建、事务怎么切进去、SQL怎么映射都是透明的。这就像学车先学手动挡离合、换挡逻辑弄明白了以后开自动挡才能判断它到底在干什么。1.2 酒店预订系统的业务边界与模块划分酒店预订系统之所以适合当毕设题目是因为它的业务流程足够贴近真实世界又不会复杂到一个人做不完。最核心的一条主链路是用户注册登录、浏览房型、选择入住日期、提交订单、完成支付或模拟支付、到店入住。围绕这条链路系统需要两个基本角色普通用户和管理员。用户端负责看房、订房、查订单管理员端负责维护房型信息、管理订单状态、统计基本营业数据。很多同学一开始容易把系统设计得过大加了一堆“轮播图管理”“友情链接”之类的冗余功能。我的建议是先守住主线再考虑扩展。你只需要抓住一个核心问题我设计这个系统是要帮助一家酒店完成线上预订这件事。从这个目标反推首页房型展示、房型详情、下单流程、个人订单中心、后台房型管理、后台订单管理这六个模块就是不可删减的骨架。1.3 技术选型背后的三个关键决策选SSM之后还有三个具体决策需要提前想清楚因为这决定了你后面写代码的难易程度。第一Spring版本选择。我建议用Spring 5.x配JDK 1.8不要贪新用Spring 6。Spring 6强制要求JDK 17很多学校机房和老电脑的环境跑不动而且网上资料量锐减出了问题不好查。第二前端页面方案。建议用JSP加上一点原生JavaScript或者直接用简单的HTML加AJAX。不要一上来就引入Vue全家桶SSM项目的重点是后端逻辑前端越复杂答辩时被追问前端细节的风险就越高。第三数据库用MySQL 5.7。它和SSM的配合经过千锤百炼网上踩坑记录最全InnoDB引擎的事务和行级锁对订单类业务也最合适。2. 核心功能模块与数据库设计思路2.1 从业务流倒推表结构数据库设计是所有毕设最见功底的地方也是答辩老师最喜欢问的部分。我见过太多学生直接拿网上的SQL跑一遍问他“订单表里为什么不直接存房型名称而要存一个type_id”就答不上来。这里分享一个我给学生用的方法画出业务流程中的每一个名词再把名词之间的数量关系标出来表就出来了。酒店预订业务涉及的名词有用户、房型、房间、订单、入住人。其中用户和订单是一对多关系房型和房间是一对多关系订单与房间、房型在“下单那一刻”形成关联。于是核心表结构就确定了用户表、房型表、房间表、订单表。如果你要做评分或者收藏功能再额外考虑关联表但主线四张表是跑不掉的。2.2 四张核心表的设计建议以我实际做过的一个酒店预订系统为例这四张表的字段设计可以这样安排用户表t_user主要字段user_id主键自增、username唯一、password存MD5加盐后的密文、nickname、phone、role区分管理员和普通用户、create_time。这里特别注意密码一定不能明文存储。虽然毕设不会真的遭到攻击但答辩时你如果能说一句“我用加盐MD5存储用户密码避免数据库泄露后密码直接暴露”绝对是加分项。房型表t_room_type主要字段type_id、type_name、price挂牌价、bed_type床型、area面积、max_people可住人数、status上下架状态、description、photo。房型表要跟房间表分开是因为同一房型可以有多间客房比如“标准大床房”下有101、102、103三间房。价格挂在房型上入住时按房型计价这也是现实酒店的做法。房间表t_room主要字段room_id、room_number、type_id外键关联房型、floor楼层、room_status0空闲1占用。房间表的作用是精确管理每一间房的可用状态下单时先从房间表找到空闲房间再写入订单否则会出现“同一间房被两个人同时预订”的并发问题。订单表t_order主要字段order_id、order_no业务单号、user_id、type_id、type_name快照字段、room_id、check_in_date、check_out_date、nights、total_price、status、create_time。这里有个关键设计点下单时除了存type_id还要冗余一个type_name和一个当时的价格字段。原因后面展开讲。2.3 状态机设计与字段冗余背后的“为什么”上面我两次提到“为什么”这两个点搞懂了数据库设计这块答辩基本就稳了。为什么要做字段冗余因为房型名称和价格是可能变化的。假设你在1月1日以388元的价格预订了1月15日的大床房到了1月5日酒店把价格调成了488元。如果订单表里只存type_id下单时再去房型表查名称和价格你的订单就会显示成488元这显然不合理。所以下单那一刻必须把名称和价格作为快照复制进订单表。这就叫“历史数据快照”是任何订单系统都躲不开的设计。为什么要用状态字段而不是直接删除数据无论订单还是房型都牵涉业务历史。用户取消订单不应该把订单记录删掉而是要把它标记为“已取消”状态方便日后统计、追溯。房型下线也是一样用status字段从1改成0而不是DELETE掉否则历史订单关联的数据就断了。订单状态我通常会定义成0待支付、1已支付待入住、2已入住、3已完成、4已取消用整数存储解释成本低索引效率也高。3. SSM框架核心知识点与源码级理解3.1 SSM三驾马车各自管什么想要消化这套系统的源码先得把三个框架的职责边界划清楚。Spring是“管家”负责创建对象、管理对象之间的依赖关系还顺带管事务SpringMVC是“前台”负责接收浏览器的HTTP请求把请求转发给对应的Controller方法再把返回结果翻译成JSON或页面MyBatis是“仓库管理员”负责把Java方法调用翻译成SQL语句执行后把数据库结果集映射成Java对象。用生活化的比喻来记一家餐厅里Spring是老板决定谁当厨师、谁当服务员谁依赖谁都由他统一调配SpringMVC是前台服务员客人点菜先告诉他他再喊后厨做MyBatis是采购员后厨需要什么食材他拿着清单去仓库取。三个角色各干各的互不越界这就是分层的好处每一层都可以独立修改不影响其他层。3.2 从DispatcherServlet到Controller的请求全链路SpringMVC的核心是一个叫DispatcherServlet的前置控制器它在web.xml里配置后所有请求都会先到达这里。我给学生讲这个流程时习惯用“快递分拣”做类比DispatcherServlet是快递中转站HandlerMapping是分拣员它根据URL上的信息告诉你“这个包裹应该送到哪个柜口”也就是找到对应的Controller方法。找到后HandlerAdapter负责真正把包裹交到柜口Controller业务方法被执行。方法执行完如果是ResponseBody接口会在HandlerAdapter里把返回值交给Jackson转换器序列化成JSON字符串写回浏览器。这条链路里最值得反复咀嚼的是HandlerMapping。你可能觉得写一个RequestMapping(/login)然后浏览器访问/login方法就调用了这不是理所当然吗其实底层是Spring启动时扫描所有Controller把映射关系注册到一个Map里等于建了本“通讯录”。请求来了DispatcherServlet查通讯录查到就调查不到就报404。理解了这个机制你以后遇到“明明写了Controller但请求404”的情况就知道优先检查映射有没有注册上。3.3 SSM常用注解速查与必知必会做这个项目下面这些注解是每天都打交道的我按“出现在哪个层”给你整理了一份速查表注解所属层核心作用易错点Controller表现层标记类是控制器忘了配ResponseBody会返回视图名RestController表现层Controller ResponseBody合体返回JSON时推荐直接用这个RequestMapping表现层绑定URL与方法支持类上和方法上两级拼接ResponseBody表现层把返回值转成JSON写入响应体依赖Jackson相关依赖Service业务层标记业务类纳入Spring容器接口和实现类注意命名Repository持久层标记DAO接口实现类与Mapper注解作用类似Autowired任意层按类型注入依赖同类型多个Bean时要配QualifierTransactional业务层声明式事务控制默认只回滚RuntimeExceptionParam持久层给Mapper方法参数命名多参数时必须显式指定PathVariable表现层从URL路径取参数路径参数用{}占位RequestBody表现层把请求体JSON转成Java对象前端必须设置Content-TypeDateTimeFormat表现层把字符串日期转成Date类型格式不匹配会直接400其中Transactional的易错点很多同学会踩。记住Spring的声明式事务默认只在遇到RuntimeException时回滚如果方法抛出的是受检异常比如IOException事务是不会自动回滚的。所以严谨的写法是Transactional(rollbackFor Exception.class)告诉Spring“只要是异常就回滚”。3.4 Mapper代理机制与XML映射的绑定规则MyBatis这块核心要理解的是Mapper接口和XML文件怎么对应上。规则说起来很简单接口的全限定名要跟XML的namespace完全一致接口方法名要跟XML里statement的id一致参数类型和返回类型要匹配。很多同学的Mapper报BindingException九成都是这三者不一致导致的。MyBatis真正优雅的地方在于你不需要写接口实现类它通过JDK动态代理在运行时帮你生成了一个代理对象。你调用userMapper.selectByUsername(username)代理对象会去解析这个方法的绑定SQL通过SqlSession执行再把ResultSet映射成User对象。所以当你看到代码里只是声明了一个接口就能直接注入使用别觉得神奇底层是MyBatis的工厂类在启动时扫描包路径为每个Mapper接口创建代理实例。实际开发里还有两个细节要提醒。第一如果接口方法需要传多个参数一定要用Param注解起名字否则XML里只能用arg0、arg1这类隐晦的名字。第二XML里SQL语句的参数用#{}而不是${}。#{}是预编译占位符能防SQL注入且会对传参自动加引号处理${}是字符串拼接只在动态排序列名、表名等场景使用严禁直接拼接用户输入。4. 环境搭建与项目启动全过程4.1 开发环境选型与版本搭配环境搭建是很多同学的第一步拦路虎。我先给一套验证过无数次的版本组合照着装基本不会出兼容性问题JDK1.8Java 8不要装17或21Maven3.6.x配阿里云镜像加速Tomcat8.5或9.0二者择一MySQL5.7装完记得设root密码IDEACommunity版足够不用破解专业版数据库可视化工具Navicat或DataGrip为什么坚持JDK 1.8因为SSM框架的经典配置都是在Java 8时代成熟的Spring 5官方文档的核心示例也基于Java 8。而且你做的毕设不需要函数式编程、模块化这些新语法Java 8足够还省去新版本JDK在模块化访问方面给Tomcat带来的启动兼容问题。这种“不追新求稳”的思路在毕设环境里永远是对的。4.2 Maven依赖清单与关键配置Maven是管理依赖的但很多学生只是盲目把pom.xml复制进去从没认真看过每一条依赖是干什么的。以下是最小可运行依赖集dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.18/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.18/version /dependency !-- Spring JDBC与事务 -- dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.18/version /dependency !-- MyBatis核心库 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.10/version /dependency !-- MyBatis与Spring的整合桥接器 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- 数据库连接池推荐Druid或C3P0 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency !-- JSON序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.4/version /dependency !-- JSP标准标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- Servlet与JSP依赖Tomcat编译需要 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies注意最后一条javax.servlet-api的scope是provided意思是打包时不需要带进去因为Tomcat自己就有。如果把scope去掉打包后的war里会包含一份Servlet API跟Tomcat自带的版本冲突大概率启动抛NoSuchMethodError。4.3 Spring与MyBatis的核心配置文件SSM项目有三个配置文件缺一不可web.xml、applicationContext.xml、spring-mvc.xml。有时候还会拆一个mybatis-config.xml但其实大部分设置可以合并到applicationContext.xml里。web.xml里最核心的配置是这两件事第一配置DispatcherServlet并指定spring-mvc.xml的位置第二配置ContextLoaderListener加载applicationContext.xml。前者管控制器层的请求分发后者管Spring根容器里那些服务类和DAO的初始化。用代码看更直观servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingapplicationContext.xml里连接池、SqlSessionFactory、Mapper扫描、事务管理这四件事是必须配好的。连接池配置用Druid为例bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/hotel_db?useSSLfalseamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value你的密码/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.dao/ /bean配置文件里我特别标注了autoCommit这个知识点事务管理器默认不自动提交需要配合Spring的Transactional手动声明哪些方法“要么一起成功、要么一起失败”。别把autoCommittrue加上去否则事务注解形同虚设。数据库连接URL里useSSLfalse和characterEncodingutf8这两个参数建议固定写上。前者避免MySQL 8的SSL警告刷屏后者保证插入中文不乱码。如果你实际连的是MySQL 8驱动类名要改成com.mysql.cj.jdbc.Driver并且URL里加serverTimezoneAsia/Shanghai否则时间字段会全部偏8小时。4.4 从数据库脚本到Tomcat启动的完整流程环境配好以后启动流程其实就那么几大步但每一步都可能出问题。建议按照我下面这个顺序操作在MySQL里执行数据库脚本建库建表并确认t_user里插了一条管理员账号数据。用IDEA打开项目等待Maven依赖下载完成检查pom无红线报错。配置Tomcat在IDEA里新增Tomcat Server选择你本机Tomcat目录Deployment里添加war exploded方式Application context设为/hotel。启动前检查applicationContext.xml中数据库密码是否和本机一致。点击Debug启动观察Tomcat日志。看到Started ServletEngine和Spring Framework的启动日志无异常再用浏览器访问http://localhost:8080/hotel。这套流程里我一直强调用war exploded而不是war。原因是exploded模式直接以解压目录运行修改了Java代码或配置文件后重新编译刷新一下就能生效调试效率高得多。用war包方式每次改动都要重新打包部署纯粹浪费时间。5. 实操过程与核心环节实现5.1 登录与Session会话管理实战登录模块是几乎所有SSM毕设的第一个功能也是面试和答辩里最容易开场的提问点。实现逻辑不复杂用户提交用户名和密码Service层调用Mapper查询用户记录比对密码成功后把用户信息放入Session。但很多同学的实现经不起追问最关键的问题是三个密码比对Session为什么要存退出登录怎么清。先看Controller代码RestController RequestMapping(/user) public class UserController { Autowired private IUserService userService; PostMapping(/login) public Result login(RequestBody User loginUser, HttpSession session) { User user userService.login(loginUser.getUsername(), loginUser.getPassword()); if (user null) { return Result.error(用户名或密码错误); } session.setAttribute(loginUser, user); return Result.success(user); } PostMapping(/logout) public Result logout(HttpSession session) { session.invalidate(); return Result.success(退出成功); } }Service层的登录方法注意密码比对这部分Override public User login(String username, String rawPassword) { User user userDao.selectByUsername(username); if (user null) { return null; } // 用MD5加密用户输入的密码再与数据库存储的密文比对 String encoded DigestUtils.md5DigestAsHex((salt rawPassword).getBytes()); if (!encoded.equals(user.getPassword())) { return null; } return user; }为什么登录状态用Session而不是每请求传ID因为HTTP是无状态的。Session能帮你把“用户已登录”这个状态保存在服务器端浏览器只需要带一个SessionID的Cookie即可。现实中Session还可以设置过期时间比如30分钟无操作自动失效学生如果能提到这个机制也算加分。5.2 房型列表与分页查询的完整链路房型列表是酒店预订系统的门面也是我让学生必须亲手从零写一遍的功能。它串起了“用户请求后端、后端查数据库、数据返回前端、前端渲染列表”整条链非常适合作为练习。技术选型上分页我建议用PageHelper插件做物理分页不要自己手写limit偏移量。原因很简单手写分页容易在计算total时多查一次count而PageHelper能自动拦截SQL完成count与分页减少一处容易出错的地方。分页示例GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 8) Integer pageSize) { PageHelper.startPage(pageNum, pageSize); ListRoomType roomTypes roomTypeDao.selectOnSaleList(); PageInfoRoomType pageInfo new PageInfo(roomTypes); return Result.success(pageInfo); }这里必须强调一个PageHelper 的坑PageHelper.startPage()方法必须紧接着调用查询方法中间不能再执行其他SQL否则分页会作用到不相干的查询上。这也是MyBatis拦截器机制的特点它是基于当前线程上下文在执行查询前自动拼接limit语句。如果分页没生效先排查你的调用顺序。5.3 下单流程、并发控制与事务的落地实现下单是整个系统最核心的业务环节涉及查询房间是否空闲、锁定房间、生成订单三个步骤中间任何一步失败都不能留下脏数据。我把下单Service层的关键代码拆出来你感受一下事务是怎么“兜底”的Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto, Integer userId) { // 根据房型ID和日期查询空闲房间这里要用悲观锁或乐观锁防止并发重复下单 Room room roomDao.selectFreeRoomByTypeAndDate(dto.getTypeId(), dto.getCheckInDate(), dto.getCheckOutDate()); if (room null) { throw new BusinessException(该房型在所选日期内已满房); } // 计算入住天数与总额 int nights (int) ((dto.getCheckOutDate().getTime() - dto.getCheckInDate().getTime()) / (1000 * 60 * 60 * 24)); BigDecimal totalPrice roomTypeDao.selectById(dto.getTypeId()).getPrice() .multiply(new BigDecimal(nights)); // 生成订单号 String orderNo generateOrderNo(); // 更新房间状态为占用 roomDao.updateStatus(room.getRoomId(), 1); // 插入订单 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setRoomId(room.getRoomId()); order.setTypeId(dto.getTypeId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setNights(nights); order.setTotalPrice(totalPrice); order.setStatus(0); orderDao.insert(order); return order; }这段代码展示的是一次典型的“先锁后写”流程。关于并发控制那间房在用户下单的瞬间可能被另一个人抢先预订因此查空闲房间时不要只查状态而是要加上数据库层面的限制。简单有效的方式是执行SELECT ... FOR UPDATE锁定这行直到事务提交或回滚或者用乐观锁在房间表加version字段更新时带上旧版本号更新失败就重试。两种策略写清楚一种答辩就很能说明问题了。事务为什么能“兜底”因为Transactional让这整个方法运行在同一个数据库连接里。如果第四步房间状态更新成功但第五步订单插入失败Spring会检测到异常并调用连接的回滚操作把房间状态改回空闲。当你在答辩现场说出“我用Spring声明式事务保证了数据一致性遇到任何异常都会回滚到操作前状态”老师基本就点头了。5.4 前端对接Fetch的JSON交互后端接口写好之后前端页面用fetch调用是当前比较主流也最轻量的方式。以房间预订提交为例前端可以这样写fetch(/hotel/user/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: admin, password: 123456 }) }) .then(res res.json()) .then(data { if (data.code 200) { alert(登录成功); } else { alert(data.msg); } });这里有个很重要的易错点如果后端拿不到RequestBody里的数据先检查请求头里Content-Type是不是application/json。很多同学在axios里默认设置的请求头会把JSON转成表单格式后端接收不到就报参数解析失败。前后端联调时打开浏览器F12的Network面板看Payload一切就都一目了然了。6. 常见问题与排查技巧实录6.1 启动失败与依赖异常排查我根据带毕业设计的经验把最高频的问题整理成一张速查表你启动遇到问题先对照查一遍现象大概率原因处理方式Tomcat启动直接报端口被占用8080端口被其他进程占用排查占用进程或改Tomcat端口启动时ClassNotFoundException: DispatcherServletspring-webmvc依赖未正确引入或没打包检查pom依赖右键Maven重新导入数据库连接失败Access denied for user数据库密码不对或用户无权限核对db.properties里的账号密码连接时间超时MySQL服务没启动或URL配置错误确认MySQL服务已启动检查驱动类Mapper绑定异常BindingExceptionMapper接口与XML namespace不匹配核对namespace是接口全限定名中文乱码数据库连接没配characterEncodingURL加characterEncodingutf8页面请求404映射路径写错或Controller没扫描到检查RequestMapping拼接后的最终路径页面请求500且日志是SQL语法错XML里SQL拼错了复制SQL到Navicat手动执行验证6.2 最常见的500错误Mapper参数绑定失败在所有运行期报错里我见到最多的就是org.apache.ibatis.binding.BindingException: Parameter xxx not found。这个问题90%是因为多参数方法没有加Param。比如这样一个Mapper方法ListRoom selectFreeRoom(Param(typeId) Integer typeId, Param(date) String date);XML里对应的SQL就是select idselectFreeRoom resultTypecom.example.entity.Room SELECT * FROM t_room WHERE type_id #{typeId} AND room_status 0 /select如果你去掉ParamMyBatis只认识arg0、arg1、param1、param2你在XML里写#{typeId}它当然找不到报错就来了。治本的思路是以后看到“Parameter not found”第一反应就是检查Mapper接口参数注解而不是怀疑SQL写错。6.3 答辩高频三问的快答思路做完了系统答辩这一关也要提前准备。根据我旁听答辩的经验老师最爱问这三类问题现在把快答思路给你列出来第一问Spring IOC是什么不要背概念用一句话加一个例子回答。“IOC就是对象创建与依赖关系的管理反过来交给Spring容器。比如UserController里用了IUserService我不自己new而是加Autowired声明让Spring容器在启动时帮我注入一个实例。好处是代码解耦、替换实现方便测试时还可以注入Mock对象。”第二问AOP在项目里用在哪你完全可以说事务。“比如我的下单方法加了TransactionalSpring会在方法执行前开启事务执行后提交异常时回滚这些逻辑被织入到我的业务方法中不需要我手写try-catch管理事务这就是AOP在项目中实际的体现。”第三问如果用户并发订同一间房怎么办对应前面5.3的代码你答“SELECT FOR UPDATE加行锁锁定房间记录其他事务必须等待防止超卖”然后补充一句“这是悲观锁方案如果想减少锁竞争也可以用版本号乐观锁”。主动权就完全在你手里了。6.4 一个必须养成的排查习惯最后分享一个我带学生时反复强调的习惯不要盯着报错堆栈的第一行发呆从日志的最后一行往前找“Caused by”。Tomcat抛出的异常往往是一长串嵌套的结构真正的根因经常藏在最下面的“Caused by”里。第一次出现Exception的堆栈行往往只是表象比如一个NullPointerException它的根因可能是上面SQL查询结果为空、对象没初始化或者依赖注入失败。用IDEA的搜索框直接定位“Caused by”对症下药排查效率能提升一倍以上。我个人在这些年的实际带教中体会最深的反而是“别急着改源码”这件事。很多学生拿到一套酒店预订系统源码第一反应是换个登录页Logo、改个系统名称就交差。但毕业设计答辩的核心是你对项目的理解和解释能力。你只有自己亲手把数据库建一遍、把配置文件敲一遍、把下单的事务逻辑走通一遍才能在被问到“为什么订单表要存快照字段”“为什么房间状态要先锁后改”时给出真正有底气的回答。这套技术栈虽然年头久但恰恰因为久资料多、坑少、原理清晰如果你能借这个项目把Java Web最核心的那条链路彻底打通收获的远远不止一个毕设成绩。建议你在做完基础功能后再尝试把原来的JSP接口改造成RESTful风格把订单查询改成带筛选条件的多条件分页这些扩展会让你的系统和你的能力都比同组人高一截。