
简介一套基于 SpringBoot、Thymeleaf 与 MySQL 开发的旅游景点酒店预订网站完整毕业设计源码包面向计算机专业学生及需要完整项目案例的开发者可快速解决毕业设计选题、项目搭建与功能实现难题。网站整体模拟电商系统包含用户登录注册、景点列表展示、相册墙、景点购买、订单评论、酒店管理以及一整套后台管理系统同时集成了短信注册验证、邮箱找回密码、阿里云 OSS 图片存储、RabbitMQ 消息队列、Redis 缓存等技术模块后端采用 Restful 风格设计并通过 JSON 与前端交互页面基于 Layui 与 BootStrap 构建具备较好的分层结构与扩展性。资源包含 487 个文件压缩后约 15.13MB覆盖 Java 源码、SQL 数据库脚本、HTML/CSS/JS 前端资源、图片素材与项目配置文件目录结构完整可导入开发环境后结合数据库脚本直接运行。目前已有 175 人学习适合用于毕业设计参考、项目实战练习以及技术方案复现尤其适合需要快速落地一套可演示、可扩展的 Web 应用系统的场景。1. 拿到“旅游景点酒店预订网站”源码包之后先别急着点运行“毕业设计基于SpringbootThymeleafMySql开发的旅游景点酒店预订网站”这个标题在每年的毕设季都会出现SpringBoot做后端骨架Thymeleaf做页面渲染MySQL存业务数据附带的Java源码和数据库脚本让你觉得“下载就能跑”。但大多数人实际遇到的情况是改完数据库密码点运行要么端口起不来要么登录页一片白要么跑起来之后根本不知道答辩时该从哪行代码开始讲。这篇笔记想把这条链路拆成几个能独立拿走的落地块项目配置怎么搭、预订下单为什么必须加事务、SQL脚本怎么写才能一次导入成功、以及最常见的五个翻车点。适合正在消化这套源码的人也适合准备把别人的作品改成自己能讲清楚的作品的人。2. 为什么是 SpringBoot Thymeleaf MySQL这套组合在毕设里到底帮你扛住了什么2.1 SpringBoot 在毕业设计里扛下的是什么SpringBoot 解决的最大问题不是性能而是“让一个人能独立把项目跑起来”。它内置了 Tomcat不需要在部署时单独装 Servlet 容器它用 starter 依赖把 Spring MVC、Jackson、参数校验这些常规模块打成一个包你不用像 SSM 时代那样去写一堆 XML 配置。在这类旅游景点酒店预订网站里SpringBoot 主要负责四件事接收浏览器发来的请求、把请求交给对应的 Service 逻辑、返回页面或数据、管理事务。对毕业设计而言还有第五件事——让答辩老师能顺着 Controller 到 Mapper 的路径把代码看完而不卡壳。实际拿到源码时第一个要看的就是 pom.xml。很多老项目卡在版本上Spring Boot 2.7.x 用 javax 包名3.x 换成了 jakarta如果你下载的源码是 2.x 写的硬配上 JDK17 和 Spring Boot 3.x 依赖编译期就会到处报“找不到符号”。我的习惯是一律先用 Spring Boot 2.7.x 配 JDK8 跑通再决定要不要升级。毕设项目里稳定压倒一切。2.2 Thymeleaf评审老师一眼能看懂的服务端渲染Thymeleaf 是这套技术栈里最被低估的一层。它属于服务端模板引擎页面本质是 HTML只是在标签里加了th:text、th:each、{...}这类属性。浏览器直接打开模板文件时看到的是静态占位内容后端把数据放进 Model 后Thymeleaf 会在服务端把占位内容替换成真实数据再返回给浏览器。这个机制对毕业设计和答辩非常友好。整个请求链路是“浏览器请求地址 → Controller 接收参数 → Service 处理业务 → Mapper 查询数据库 → Thymeleaf 渲染页面”老师按 F12 看到的 HTML 就是最终结果不用像前后端分离那样还要解释跨域、Token、接口文档。相比 Vue SpringBoot 的前后端分离方案Thymeleaf 省掉了 Node 环境也不需要单独启动前端服务。对一个人的毕设来说这就少了一半的环境变量和版本冲突。唯一要注意的是开发时把模板缓存关掉否则你改了 HTML 刷新页面没反应这个问题几乎每届都有同学踩中。2.3 数据访问层MyBatis 是主流搭配但核心在业务闭环标题里没写持久层框架但这套源码最常见的组合是 MyBatis。与 Spring Data JPA 相比MyBatis 的 SQL 是手写的答辩论据更直观你能直接指着一句update room set stock stock - 1 where room_id ? and stock 0告诉老师“这里就是防超卖”。如果你拿到的源码用的是 JPA 或者 JDBC Template思路完全一致只是把 Mapper 接口换成 Repository。我建议不要为了换框架而换框架那会引入大量新代码答辩时反而讲不熟。真正该花时间的是把“用户下单、扣库存、生成订单”这条业务闭环讲清楚数据访问层只要稳定就别动它。2.4 跑起来的第一步pom 与 application.yml 的关键配置先把 pom.xml 里的核心依赖立住。下面这段是这类项目的最小可运行骨架parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 启动器内置 Tomcat包含 Spring MVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 模板引擎适配器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis 与 SpringBoot 的桥接包 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- MySQL 驱动。8.x 版本 groupId 已从 mysql:mysql-connector-java 改为 com.mysql:mysql-connector-j -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies这段依赖的核心点有两个spring-boot-starter-parent统一管理版本号避免你自己猜驱动版本MySQL 驱动选com.mysql:mysql-connector-j而不是老的mysql:mysql-connector-java否则连 MySQL 8 会报认证插件不兼容。如果你不想用 MyBatis把mybatis-spring-boot-starter换成spring-boot-starter-data-jpa即可但这篇文章后面按 MyBatis 讲。然后是application.yml这是每次拿到新项目第一个要改的文件server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplallowPublicKeyRetrievaltrue和useSSLfalse是 MySQL 8 的惯用配置前者解决 caching_sha2_password 认证时的公钥获取问题后者避免本地连接的 SSL 握手警告。cache: false是开发期必须关掉的 Thymeleaf 缓存。map-underscore-to-camel-case让数据库的order_no自动映射成实体类的orderNo少写一堆Results注解。日志输出StdOutImpl会在控制台打印每条 SQL排错时能直接看到 MyBatis 实际执行了什么语句。这段配置跑通之后项目就能启动到“访问首页不报 500”的状态。下一步才是业务代码。3. 核心功能落地把预订流程从页面到数据库完整走通3.1 项目分层让答辩老师愿意往下看的代码组织方式旅游景点酒店预订网站的业务量不大但代码结构一定不能省。常见的分层方式是controller、service、mapper、entity、dto加上common放拦截器和全局异常处理。src/main/java/com/example/travel/ ├── TravelApplication.java // 启动类标注 SpringBootApplication ├── common/ │ ├── BizException.java // 自定义业务异常 │ └── GlobalExceptionHandler.java ├── controller/ │ ├── IndexController.java // 首页与页面跳转 │ ├── HotelController.java // 酒店搜索与详情 │ ├── BookingController.java // 下单与订单列表 │ └── UserController.java // 登录注册 ├── service/ │ ├── BookingService.java │ └── impl/ │ └── BookingServiceImpl.java ├── mapper/ │ ├── HotelMapper.java │ ├── RoomMapper.java │ └── BookingMapper.java ├── entity/ │ ├── User.java │ ├── Hotel.java │ ├── Room.java │ └── Booking.java └── dto/ └── BookingDTO.java // 前端传参对象分包逻辑是entity和数据库表一一对应dto只承载页面提交的参数controller不写业务只做参数接收、调用 Service、返回视图名。老师问“项目怎么分层”时你能直接对着目录说清楚这比代码本身更能加分。3.2 预订与下单一个用事务和行锁撑住的核心 Service这个项目的业务核心是“用户选一个房型、选入住离店日期、下单”。最容易答不上来的问题是两个用户同时订最后一间房怎么办。写一个带事务的 Service 就能把这个问题讲透。Service public class BookingServiceImpl implements BookingService { Autowired private RoomMapper roomMapper; Autowired private BookingMapper bookingMapper; Override Transactional(rollbackFor Exception.class) public String createBooking(BookingDTO dto) { // 1. 对房型行加锁防止并发下单时读到同一个剩余库存 Room room roomMapper.selectByIdForUpdate(dto.getRoomId()); if (room null || room.getStock() 0) { throw new BizException(该房型已满房); } // 2. 计算入住天数校验日期合法性 long days ChronoUnit.DAYS.between( dto.getCheckInDate(), dto.getCheckOutDate()); if (days 0) { throw new BizException(离店日期必须晚于入住日期); } // 3. 总价 房型单价 × 晚数。用 BigDecimal避免 double 精度丢失 BigDecimal totalPrice room.getPrice() .multiply(BigDecimal.valueOf(days)); // 4. 生成订单号BK 时间戳 4位随机数简单且不容易撞 String orderNo BK System.currentTimeMillis() RandomStringUtils.randomNumeric(4); // 5. 先扣库存受影响行数为 0 说明库存已被抢走 int affected roomMapper.deductStock(dto.getRoomId()); if (affected 0) { throw new BizException(手慢了库存刚刚被抢完); } // 6. 保存订单 Booking booking new Booking(); booking.setOrderNo(orderNo); booking.setUserId(dto.getUserId()); booking.setRoomId(dto.getRoomId()); booking.setCheckInDate(dto.getCheckInDate()); booking.setCheckOutDate(dto.getCheckOutDate()); booking.setTotalPrice(totalPrice); booking.setContactName(dto.getContactName()); booking.setContactPhone(dto.getContactPhone()); booking.setStatus(0); // 0待支付 booking.setCreateTime(new Date()); bookingMapper.insert(booking); return orderNo; } }这里有两个参数层面的细节。Transactional(rollbackFor Exception.class)表示只要方法里抛出任何异常扣库存和插订单这两步就会一起回滚否则会出现“订单没生成但库存少了”的不一致状态。selectByIdForUpdate是悲观锁对应的 SQL 是select * from room where room_id ? for update它会让并发请求在行上排队一个事务提交后另一个才能读到最新库存。毕设场景并发量很低悲观锁最好讲也最好维护。如果你想讲乐观锁可以在room表加version字段用update ... set stock stock - 1, version version 1 where room_id ? and version ?判断影响行数。但我建议别在答辩时给自己挖坑悲观锁一句话就能解释清楚乐观锁还要解释重试机制。对应的RoomMapper.xml里这两个 SQL 是关键select idselectByIdForUpdate resultTypecom.example.travel.entity.Room select * from room where room_id #{roomId} for update /select update iddeductStock update room set stock stock - 1 where room_id #{roomId} and stock 0 /updatefor update只在事务内生效所以这条查询必须和扣库存放在同一个Transactional方法里。deductStock的where stock 0是第二道保险即使前面的行锁没生效数据库层面也不会把库存扣成负数。3.3 查询与分页Controller 侧的参数处理和 PageHelper 用法酒店列表页和订单列表页都需要分页。MyBatis 生态里最常用的分页组件是 PageHelper用法是在 Mapper 查询之前调用它的静态方法拦截器会为紧接着的第一条 SQL 自动拼接limit。Controller RequestMapping(/hotel) public class HotelController { Autowired private HotelMapper hotelMapper; GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 6) Integer pageSize, RequestParam(required false) String keyword, Model model) { PageHelper.startPage(pageNum, pageSize); ListHotel hotelList hotelMapper.search(keyword); PageInfoHotel pageInfo new PageInfo(hotelList); model.addAttribute(page, pageInfo); model.addAttribute(keyword, keyword); return hotel/list; } }PageHelper.startPage(pageNum, pageSize)必须写在 Mapper 查询的紧邻前面中间不能穿插其他查询语句否则拦截器会作用到错误的 SQL 上。PageInfo里已经帮你算好了total、pages、hasPreviousPage、hasNextPage页面直接取这些属性就能渲染分页按钮。这里有一个容易忽略的参数问题pageNum如果被传成 0 或负数PageHelper 会把它当成 1 处理但最好还是自己校验一次。HotelMapper.search对应的模糊查询要写成select idsearch resultTypecom.example.travel.entity.Hotel select * from hotel where if testkeyword ! null and keyword ! name like concat(%, #{keyword}, %) or address like concat(%, #{keyword}, %) /if /where order by hotel_id desc /select用where标签而不是直接写where 11能让 SQL 在关键字为空时不带条件日志里看起来更干净。concat(%, #{keyword}, %)是为了防止关键字里带%或_时出现通配符注入比直接拼字符串安全。3.4 Thymeleaf 页面渲染列表展示与表单回显页面层直接用 Thymeleaf 的标签属性渲染数据。下面是酒店列表页的核心片段table tr th:eachhotel : ${page.list} td th:text${hotel.name}酒店名称/td td th:text${hotel.city}城市/td td th:text${hotel.starLevel} 星星级/td td th:text${hotel.rating}评分/td td a th:href{/hotel/detail/{id}(id${hotel.hotelId})} classbtn btn-primary查看房型/a /td /tr /table div span th:if${page.hasPreviousPage} a th:href{/hotel/list(pageNum${page.prePage},pageSize${page.pageSize},keyword${keyword})}上一页/a /span span th:text第 ${page.pageNum} / ${page.pages} 页第 1 / 1 页/span span th:if${page.hasNextPage} a th:href{/hotel/list(pageNum${page.nextPage},pageSize${page.pageSize},keyword${keyword})}下一页/a /span /divth:each会遍历model里的page.list集合每行生成一个tr。{/hotel/detail/{id}(id${hotel.hotelId})}是 Thymeleaf 的 URL 模板语法会生成/hotel/detail/3这样的地址。分页链接里必须把keyword也带回去否则第二页开始搜索条件就丢了。表单回显是另一个高频细节。当用户搜索后翻页或搜索失败时输入框里的关键字应该在刷新后保留input typetext namekeyword th:value${keyword} placeholder输入酒店名或城市注意th:value里拿到的是 Controller 放进去的keyword属性如果属性名和input的name不一致页面刷新后回显会失败。如果你在浏览器里看到${keyword}这个字符串原样打在页面上基本可以确定是模板渲染没有生效——不是缓存问题就是 Controller 返回的视图名没对应到 templates 目录下的文件。4. 数据库脚本一份能直接演示的 SQL 该写成什么样4.1 表结构设计五张核心表和它们的外键关系很多同学交付的数据库脚本只有建表语句没有初始化数据导致答辩现场导入后页面空空如也。一份合格的脚本要包含表结构、索引和演示数据三部分。这个项目核心就五张表用户表、酒店表、房型表、景点表、订单表。下面是建表 SQL 的关键片段create database if not exists travel default character set utf8mb4; use travel; create table user ( user_id int primary key auto_increment, username varchar(32) not null, password varchar(64) not null, nickname varchar(32) default , phone varchar(20) default , create_time datetime default current_timestamp ) engineInnoDB default charsetutf8mb4; create table hotel ( hotel_id int primary key auto_increment, name varchar(64) not null, address varchar(255) default , city varchar(32) not null, star_level tinyint default 3, rating decimal(2,1) default 5.0, description text, image_url varchar(255) default ) engineInnoDB default charsetutf8mb4; create table room ( room_id int primary key auto_increment, hotel_id int not null, room_type varchar(32) not null, price decimal(10,2) not null, stock int default 0, bed_type varchar(32) default 大床, area int default 20, image_url varchar(255) default , key idx_hotel_id (hotel_id) ) engineInnoDB default charsetutf8mb4; create table booking ( booking_id int primary key auto_increment, order_no varchar(32) not null, user_id int not null, room_id int not null, check_in_date date not null, check_out_date date not null, total_price decimal(10,2) not null, contact_name varchar(32) not null, contact_phone varchar(20) not null, status tinyint default 0 comment 0待支付 1已支付 2已取消 3已完成, create_time datetime default current_timestamp, key idx_user_id (user_id), key idx_order_no (order_no) ) engineInnoDB default charsetutf8mb4;字段设计上有三个踩坑点。金额一律用decimal(10,2)不要用float或double否则结算价会出现 19.99 变成 20.0000001 的问题。订单号要加唯一索引idx_order_no虽然代码里已经拼了时间戳和随机数数据库层面再加一道约束更稳妥。room表的hotel_id建普通索引就好不需要建外键约束——毕设里外键会在导入数据和删除数据时带来顺序麻烦索引足够支撑按酒店查房型。4.2 初始化数据不是随便填要能支撑你演示完整个流程初始化数据比表结构更影响答辩效果。你的演示路径一般是搜索城市 → 进入酒店详情 → 选房型 → 下单 → 查看订单。所以数据必须覆盖这个完整链路每个环节都有内容可点。insert into hotel (name, address, city, star_level, rating, description) values (杭州西湖大酒店, 西湖区湖滨路1号, 杭州, 5, 4.8, 坐落在西湖景区旁), (丽江古城客栈, 古城区五一街, 丽江, 3, 4.5, 纳西风格小院); insert into room (hotel_id, room_type, price, stock, bed_type, area) values (1, 豪华湖景大床房, 688.00, 5, 大床, 45), (1, 标准双床房, 388.00, 8, 双床, 30), (2, 庭院大床房, 268.00, 3, 大床, 25); insert into user (username, password, nickname, phone) values (admin, md5(123456), 管理员, 13800000000), (test, md5(123456), 测试用户, 13900000000);注意这里的md5(123456)是 MySQL 内置函数直接在 SQL 里把密码转成了摘要。登录校验时对用户输入的密码套同样的MD5函数再比对避免明文存密码。虽然 MD5 不算安全的加密方式但毕业设计里比明文强一个档次而且不需要额外写加密工具类。房型库存设成 3 到 8 之间的数是有讲究的。库存太大会让你演示“满房异常”时得手动改数据库库存太小又容易被你自己反复测试耗尽。3 到 5 是刚刚好能演示、又能通过修改 SQL 快速重置的量。4.3 IDEA 导出 sql 脚本交作业和二次导入的正确姿势数据库脚本最终要交到指导老师手里常见做法是让老师新建一个空库再执行你的脚本。这里有两个导出路径。第一个是命令行用mysqldump导出mysqldump -u root -p travel --default-character-setutf8mb4 travel.sql这里travel是数据库名。加--default-character-setutf8mb4是为了确保脚本里的中文不会在导入时变成问号。如果不加Windows 下默认字符集可能是 latin1导出的脚本在另一台机器上导入就乱码。第二个是 IDEA 的图形界面导出。打开右侧 Database 面板展开你的数据源右键数据库名选择 “Export with mysqldump” 或 “Dump data to file”不同版本菜单名略有差异。导出时注意勾选包含CREATE DATABASE和USE语句这样对方导入时不用手动建库。导入时直接在 IDEA 里打开 sql 文件点击运行按钮选择你的数据源即可。如果脚本里有drop table if exists语句重复导入不会报错如果没有第二次导入会卡在“表已存在”的错误上。我一般习惯在脚本开头加这一段drop table if exists booking; drop table if exists room; drop table if exists hotel; drop table if exists scenic; drop table if exists user;这相当于给数据库吃了一颗后悔药。每次需要重置演示环境时直接重新执行整个脚本所有数据回到初始状态不用手动去点删除。5. 避坑这套毕设跑起来最容易翻车的 5 个问题5.1 页面改了不生效F5 也没用现象修改了templates/hotel/list.html刷新浏览器页面完全没有变化甚至删掉一行代码页面还是旧的。原因Thymeleaf 默认开启模板缓存第一次访问后就把渲染结果缓存起来浏览器侧可能还叠加了本地缓存。解决把application.yml里的spring.thymeleaf.cache设为false然后重启应用。开发期间还可以在 IDEA 里修改模板后按Ctrl F9重新编译或直接Ctrl Shift F9热更新当前文件。如果还不行浏览器按Ctrl F5强制刷新绕开浏览器缓存。改完页面不生效这件事90% 是缓存没关10% 是改错了文件目录。5.2 MySQL 8 连接报 Public Key Retrieval 异常现象应用启动或首次访问数据库时报错Public Key Retrieval is not allowed。原因MySQL 8 默认使用caching_sha2_password认证插件客户端第一次连接时需要从服务端获取公钥来加密密码而连接串默认不允许这个行为。解决在 JDBC 连接串上追加allowPublicKeyRetrievaltrueuseSSLfalse。如果用的驱动是 5.x 老版本建议换成com.mysql:mysql-connector-j8.0 系列。顺带提一句改完连接串后一定要重启应用数据源连接池不会自动重新读取配置。另一个思路是把用户密码改回mysql_native_password方式但那是绕开问题而不是解决问题换驱动加参数才是正路。5.3 SpringBoot 版本太高javax 变成 jakarta 导致编译失败现象下载的源码里的 import 是javax.servlet.http.HttpServletRequest你的环境里 Spring Boot 3.x 编译报红提示包不存在。原因Spring Boot 3.0 开始所有 javax 命名空间迁移到 jakarta。老源码没有做对应修改在 3.x 下自然编译不过。解决这种项目直接调整为一个已知稳定组合——Spring Boot 2.7.x JDK8同时把mysql-connector-j换成 8.0.x。如果指导老师要求必须用新版本那就全局替换javax.为jakarta.但要注意 Thymeleaf 的spring-boot-starter-thymeleaf版本要跟着 Boot 3.x 走别拿 2.x 的依赖配 3.x 的父工程。我的血泪经验是不要在毕设阶段追新版本能用就行。5.4 数据库脚本导入乱码或外键校验失败现象导入 SQL 后页面上酒店名称变成???或者导入时报错Cannot add foreign key constraint。原因乱码通常是 SQL 文件本身不是 UTF-8 编码或者连接客户端的字符集没指定外键报错则是创建表的顺序不对子表在父表之前创建。解决在 SQL 文件第一行写SET NAMES utf8mb4;并在导入前用编辑器把文件编码转成 UTF-8。IDEA 右下角能直接查看和切换文件编码Windows 下新建的 txt 默认可能是 GBK。外键问题更简单——脚本里尽量别建物理外键用普通索引替代如果必须有把父表hotel、user放在前面建子表room、booking对应房间表在最后建。用主从顺序建表后再用SET FOREIGN_KEY_CHECKS0;和SET FOREIGN_KEY_CHECKS1;包住整个脚本就能避免导入时校验顺序。5.5 登录拦截器把 css/js 全部拦掉页面只剩纯文本现象登录成功后页面能跳转但完全没有样式浏览器控制台一堆 404全都是/css/**、/js/**的资源请求。原因你写了一个登录拦截器在预处理里把没登录的请求重定向到登录页但没有放行静态资源路径。Spring Boot 的静态资源默认放在classpath:/static/下访问路径是/css/xxx.css拦路由拦了它页面自然裸奔。解决在注册拦截器的 WebMvc 配置里把静态资源路径和公开页面排除掉Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /css/**, /js/**, /images/**, /static/**, /, /index, /hotel/list, /hotel/detail/**, /user/login, /user/register ); } }这里addPathPatterns(/**)是拦截所有路径excludePathPatterns指定不拦的路径。放行时注意把hotel下面的查询和详情页也放掉否则用户没登录就没法浏览内容整个网站的使用逻辑就不成立了。还有一个隐蔽点如果你用了自定义的登录页面页面里的验证码请求地址也要加进白名单否则每次刷新验证码都会被重定向。6. 答辩加分把普通增删改查讲出业务感的三个小技巧6.1 用拦截器把“权限控制”讲清楚登录拦截器不只是一个过滤器它是答辩时最容易展开讲的点。准备一张“未登录访问订单页 → 拦截器重定向到登录页”的截图老师问你权限怎么做时直接打开LoginInterceptorpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object userId request.getSession().getAttribute(userId); if (userId null) { response.sendRedirect(/user/login); return false; } return true; } }说明逻辑就三句话Session 里有userId就放行没有就重定向到登录页返回false终止后续执行。再结合上一章提到的静态资源排除就能把“如何保护需要登录的接口”和“为什么静态资源不拦截”都串起来。6.2 给下单接口准备一张 JMeter 压测截图老师的常见追问是“你这个系统并发怎么样”。你可以提前用 JMeter 对/booking/create接口做一次小规模压测线程组设为 50 个线程、循环 10 次聚合报告截图放进毕设文档里。关键是截图上要能看到“异常率 0%”平均响应时间在几百毫秒以内。压测前先跑一次单线程的请求确保接口本身不报错压测时如果出现连接超时多半是本地环境或连接池配置问题先排查日志别拿一张全是红色错误的图去答辩。本地压测的数据虽然不代表生产性能但能证明你做过性能验证这一项会明显拉开与其他只写了 CRUD 的同学的差距。6.3 把 Banner 和日志打点变成你的个人痕迹最后给一个小细节在src/main/resources下放一个banner.txt启动 SpringBoot 时控制台会打印你自定义的 ASCII 图案网上有在线生成器能直接做。再加上在预订成功的日志里打一行订单号log.info(预订成功 orderNo{}, userId{}, orderNo, dto.getUserId());这两件事成本很低但会让老师觉得你是真的在长期使用这个项目而不是为了交差临时拼出来的。我当年答辩时被问到“你怎么证明订单没超卖”我没有背概念直接把那条for update的 SQL 和事务日志调出来解释了一遍。能亲手讲清楚一个细节比十张架构图更有说服力。希望这篇笔记能帮你在答辩前把项目变成真正属于自己的作品。本文还有配套的精品资源点击获取