Spring Boot招聘平台毕业设计:从需求到答辩全流程 简介这是一套基于 Spring Boot 的毕业生信息招聘平台毕业设计项目采用 Java 与 MySQL 实现面向计算机相关专业学生及需要快速搭建招聘类系统的开发者。系统涵盖管理员后台、企业端、毕业生端与前台首页功能包括招聘岗位管理、个人简历、岗位应聘、空中宣讲会、线上面试、论坛管理、考试管理等完整业务模块并配有系统详细设计与测试章节便于理解整体架构与实现思路。压缩包约 180.77MB包含系统源码、数据库文件、毕业设计论文LW、答辩PPT以及演示视频等其中文档侧重第5章系统详细设计与第6章系统测试可对照源码学习环境搭建与功能调试。已有136人学习下载内容受到一定关注。通过该项目可获取完整毕业设计方案、关键模块的实现代码、数据库表结构、论文写作框架以及演示视频既能用于毕业设计参考也能为后续二次开发或求职项目积累实战经验。1. 毕业设计选 Spring Boot 做招聘平台先想清楚这四个问题再动手每年毕业季Java 方向的选题总会撞车十个里有六七个是“XX管理系统”但真正能在答辩现场讲清楚业务闭环的不多。毕业生信息招聘平台之所以值得做是因为它天然带着双边角色毕业生要上传简历、搜索岗位、跟进投递状态企业要发布岗位、筛选简历、发起面试邀请。这个双向交互意味着数据库不能只有一张表接口也不只是教材里的“增删改查”演示起来有层次。基于 springboot 落地是这几年 Java 毕业设计里最稳的选型框架成熟、资料多、公司实际用的也是这套。这篇文章按我带项目的顺序来讲——需求、数据库、接口、避坑、答辩演示每一步都给你能直接抄的配置和代码目标是让你拿到交付物后能看懂、能改、能现场复现。2. 技术选型与工程结构Spring Boot 版本、依赖和目录怎么定2.1 为什么 Spring Boot 是毕业设计的“安全牌”从 SSM 到前后端分离的选型对比毕业生招聘平台这一类系统的技术选型说白了就在下面几条路线里选纯 JSPServlet、SSMSpring MVC MyBatis、Spring Boot MyBatis-Plus顶多再拆一层 Vue。纯 JSP 方案在近两年的毕设里越来越不吃香。页面和 Java 代码混在一起调试时要同时盯着 HTML、JSTL、Servlet 转发改一个列表样式能引发一堆无关报错。答辩老师问“前后端怎么交互”只能答 form 提交这在近几年显得单薄。SSM 虽然把三层分开了但配置负担很重数据源、事务、Mapper 扫描、Spring MVC 视图解析器每一样都要写 XML 或注解配置。很多同学不是不会写业务是被这些配置折磨得没心思讲亮点。Spring Boot 解决的核心问题是约定优于配置。拉一个 spring-boot-starter-web内嵌 Tomcat、JSON 序列化、静态资源映射这些底层能力就自动就位你可以把精力放到业务逻辑和答辩 PPT 的讲故事上。而且这套技术栈在公司实际开发里是主流答辩时“技术选型贴近工业界”本身就是加分项。我一般给出的选型组合是这样的分层选型为什么这么选基础框架Spring Boot 2.7.x稳定、中文教程多3.x 需要 JDK 17部分学校电脑没装持久层MyBatis-Plus 3.5.x单表增删改查不用写 SQL分页和条件构造器刚好覆盖搜索投递场景数据库MySQL 8.0默认 utf8mb4存简历附件路径和中文内容不会乱码鉴权JWT 拦截器无状态、好讲解答辩能单独讲 token 机制前端Thymeleaf 或 Vue 二选一Thymeleaf 省事Vue 能讲前后端分离按你的余量选这里要提醒一句版本观Spring Boot 3.x 刚普及没几年但很多毕业设计电脑上的 JDK 还是 8直接上 3.x 会碰到 javax 包名改成 jakarta 这类迁移问题。用 2.7.x 加 JDK 8几乎不会在环境上卡壳。数据库连接驱动也对应选 mysql-connector-java 8.x别拿 5.x 的驱动硬凑 8.0 的 MySQL。2.2 工程结构拆解与 pom.xml从依赖到包结构的落地方式先给一份最小可运行的 pom.xml。这份依赖清单不求全但每一个都是后面章节要用的基础少了任何一个都会在某个环节突然报错。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段配置里有两个值得说的参数。第一个是 java.version 设成 8配合 2.7.18 的父工程能避开绝大多数本地 JDK 版本坑。第二个是 mybatis-plus-boot-starter 的版本3.5.3.1 是兼容 Spring Boot 2.7 的常用版本网上很多旧教程用 3.4.x启动时看似正常但分页插件注册那一步容易失灵后面避坑章节会专门展开。包结构我习惯按模块拆而不是按 controller/service 一层一层铺开。这样答辩讲“毕业生模块”的时候代码跳转路径是连贯的评审老师不用在几十个类里找入口。com.example.recruit ├── RecruitApplication.java ├── common │ ├── Result.java │ ├── JwtUtil.java │ └── LoginInterceptor.java ├── user │ ├── UserController.java │ ├── UserService.java │ └── UserMapper.java ├── job │ ├── JobController.java │ ├── JobService.java │ └── JobMapper.java ├── resume └── applicationcommon 里放所有模块共用的东西统一返回体 Result、JWT 工具类、登录拦截器。user/job/resume/application 各自独立互不引用对方的 Service模块之间有业务协作就在 Controller 层编排。这个约定既控制了你写代码时的耦合度也让论文里的“系统架构图”特别好画。2.3 application.yml 里最容易被忽略的三个参数数据源、日志、上传大小很多同学第一次跑这个项目启动直接红字八成不是代码问题是 application.yml 里的数据源配置不对。给一份能直接用的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/recruit?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto第一处是 url 里的 serverTimezoneAsia/Shanghai。MySQL 8.0 在没指定时区时会报“The server time zone value”错误这一串必须带上。第二处是 map-underscore-to-camel-case数据库 create_time 字段要正确映射到 Java 里的 createTime 属性靠的就是它少了这行你查询出来的时间字段全是 null而且是那种连报错都不给的“安静翻车”。第三处是 multipart 的 max-file-size招聘平台必然有简历附件上传默认 1MB 完全不够调大到 20MB 是应届简历 PDF 的合理上限。mybatis-plus 的 id-type 用 auto 走数据库自增比默认的雪花 ID 在演示时更直观投递记录 ID 123、124 这样递增答辩讲起来不用解释大整数。日志配置 log-impl 打开后控制台能直接看到 MyBatis 生成的 SQL这就是你后面排查问题的眼睛答辩时演示“我能看到后台 SQL”也很加分。3. 数据库设计与核心表结构用户、简历、岗位、投递四张表的关系3.1 从业务需求倒推数据表毕业生、企业、管理员三类账号怎么用一张表装下招聘平台最忌讳一上来就画十几张表。毕业设计的时间有限先保证四条核心链路能跑通毕业生注册并填简历、企业注册并发岗位、毕业生搜岗位并投递、企业查看投递并更新状态。对应到数据库就是 user、resume、job、application 四张表。很多同学会把毕业生和企业拆成两张用户表这会让登录逻辑复杂一倍而且违背了“两类人本质都是账号”的事实。正确做法是用一张 user 表加个 role 字段区分。我给的建表语句是CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码演示环境存MD5, role TINYINT NOT NULL DEFAULT 1 COMMENT 1-毕业生 2-企业 0-管理员, email VARCHAR(100) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里三个细节值得抄。第一role 用数字不用字符串后续枚举判断方便也暗示了这类字段是可扩展的。第二unique key 加在 username 上演示时反复注册同一账号不会给你现场报重复数据。第三create_time 和 update_time 自动填充答辩时展示“用户注册时间、信息更新时间线”是很自然的素材。resume 表我建议用 user_id 做外键并加 unique 约束保证一个毕业生只能有一条主简历。字段就是姓名、性别、毕业院校、专业、学历、工作经历、项目经历、附件路径这些别贪多。简历在招聘平台里是“用户的扩展信息”不是独立实体这个认知能让你的 E-R 图画得干净。3.2 投递状态状态机与冗余字段application 表的三个关键设计投递表是整个系统的业务灵魂。很多毕设把投递表做成 job_id user_id 时间三个字段草草了事结果答辩时老师一句“投递之后流程怎么走”就卡住了。一张能撑起故事的投递表至少要这样设计字段名类型说明idBIGINT主键自增student_idBIGINT毕业生用户IDjob_idBIGINT岗位IDcompany_idBIGINT冗余企业ID方便企业端查列表statusTINYINT0 已投递 1 已查看 2 面试邀约 3 已通过 4 已结束remarkVARCHAR(255)企业回填的反馈或面试时间create_timeDATETIME投递时间status 字段为什么从 0 到 4 这么设因为它把整个招聘流程压缩成了一个可枚举的状态机毕业生投递后是“已投递”企业点开简历变成“已查看”接着企业发起面试变成“面试邀约”毕业生在企业端操作下被动前进。答辩时你可以画一条横向的流程线从投递到终止每一步都能讲出系统的行为逻辑。这里最重要的设计是 company_id 冗余字段因为企业端要按“我收到的投递”来查列表没有这个字段就得 join 两张表毕业设计里能少一次 join 就少一次。索引也别忘了。student_id 和 job_id 各建一个普通索引再加一个 company_id status 的联合索引投递列表页的查询就是索引覆盖。不要小看这一步答辩老师问“数据库优化”时你能说出“通过联合索引避免了文件排序”这就是实打实的加分点。字段注释写全工具生成数据库设计文档时不用返工。3.3 初始化数据脚本三种角色的账号和演示数据怎么造光有建表语句不够你要用一份初始化脚本把演示数据预置好不然答辩前手忙脚乱地现场注册账号、发岗位场面非常狼狈。我习惯把 init.sql 分成三段每段用注释分隔。-- 1. 预置账号admin 管理员 / company01 企业 / stu01 毕业生 INSERT INTO user (username, password, role, email) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 0, adminexample.com), (company01, e10adc3949ba59abbe56e057f20f883e, 2, hrcompany01.com), (stu01, e10adc3949ba59abbe56e057f20f883e, 1, stu01example.com); -- 2. 预置岗位数据company01 发布 3 个岗位 INSERT INTO job (company_id, title, description, salary_range, status) VALUES (2, Java实习生, 负责公司内部管理系统的后端开发, 150-200元/天, 1), (2, 前端开发助理, 参与招聘平台前端页面的维护, 120-180元/天, 1), (2, 产品助理, 协助整理需求文档和原型图, 100-150元/天, 1); -- 3. 预置投递记录让企业端一打开就有内容 INSERT INTO application (student_id, job_id, company_id, status, remark) VALUES (3, 1, 2, 1, 简历已查看等待筛选);密码字段我在这里用的是“123456”的 MD5实际交付时你可以换成 BCrypt写代码的登录校验处对应改一下算法即可。演示账号统一密码是为了答辩现场任何一台电脑都能快速登录不用和评委一起回忆密码。岗位数据里的公司 id 是 2对应企业表的自增 id投递记录里 student_id 是 3这套 id 依赖关系你得在注释里写明白不然复制到别人库就插错了。岗位数据里的 salary_range 用字符串区间其实是有意的因为毕业设计阶段的岗位薪资往往是“面议”或区间描述做成数字反而别扭。status 字段预置为 1 表示发布中0 表示下架演示时可以现场演示“把岗位下架后毕业生就搜不到了”这是很好的业务串联素材。4. 后端接口与关键代码从登录鉴权到投递流程的实现4.1 基于 JWT 的登录鉴权拦截器与注解的最小实现用户体系有了第一个要写的后端模块就是登录。springboot 生态里做鉴权有很多方案但毕业设计我建议用 JWT理由有两个一是无状态服务器不需要存 session接口写好就能在前端任意地方调用二是答辩时“JWT 由三部分组成Header、Payload、Signature”能作为独立的知识点讲上两分钟。先加依赖。在 pom.xml 的 dependencies 里补上 jjwt 的两个包版本用 0.11.5这是和 JDK 8 配合最稳的组合dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency然后是 JwtUtil 工具类。里面的核心逻辑就两个方法生成 token 和解析 token。写一个最小可用的版本Component public class JwtUtil { // 演示用固定密钥生产环境要从配置中心读取 private final String secret recruit-demo-secret-key-please-change; private final long expire 24 * 60 * 60 * 1000L; public String createToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .build() .parseClaimsJws(token) .getBody(); } }这个类的参数有三个关键secret 密钥长度至少要 256 位太短会抛 WeakKeyExceptionexpire 我设 24 小时一整天答辩演示不用重新登录claim 里塞 userId 和 role后续接口做权限判断直接从 token 里取不用再查数据库。signWith 指定了 HS256 算法这是 JJWT 默认且兼容性最好的。接下来要有个拦截器让受保护的接口自动校验 token而不是在每个 Controller 写重复代码Component public class LoginInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; public LoginInterceptor(JwtUtil jwtUtil) { this.jwtUtil jwtUtil; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { response.setStatus(401); return false; } try { Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }拦截器里的 StringUtils.hasText 是 springframework 自带的工具能同时判断 null 和空串。解析成功后把 userId 写进 request 属性后续 Controller 直接用 RequestAttribute 取这就避免了每个接口都重复解析 token。要注意拦截器只处理“登录后才能访问”的接口像登录接口本身和注册接口要放行配置方式在后面的避坑章节会专门讲。4.2 岗位搜索与分页查询MyBatis-Plus 的条件构造器怎么写岗位列表是毕业生端最核心的页面。搜索条件通常有三个关键词、企业、状态。用 MyBatis-Plus 的 LambdaQueryWrapper 写这个查询比手写 SQL 要直观得多而且天然防 SQL 注入。核心代码是这样的Override public PageJob searchJobs(Integer pageNo, Integer pageSize, String keyword, Long companyId, Integer status) { PageJob page new Page(pageNo, pageSize); LambdaQueryWrapperJob wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Job::getTitle, keyword) .eq(companyId ! null, Job::getCompanyId, companyId) .eq(status ! null, Job::getStatus, status) .orderByDesc(Job::getCreateTime); return jobMapper.selectPage(page, wrapper); }注意看这里的方法参数写法like 和 eq 的第一个参数是一个 boolean 条件当条件为 false 时MyBatis-Plus 会直接忽略这条查询条件。这意味着前端不传 keyword 时wrappersql 里压根不会出现 like 语句传了才拼进去。这套机制叫条件动态拼接是 MyBatis-Plus 的灵魂。companyId 用 eq 是因为企业端要看自己的岗位列表毕业生端就传 null 忽略它。selectPage 返回的 Page 对象自带 total、pages、records 属性直接能传给前端做分页组件。控制器里这样暴露接口GetMapping(/job/search) public ResultPageJob search( RequestParam(defaultValue 1) Integer pageNo, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword) { return Result.success(jobService.searchJobs(pageNo, pageSize, keyword, null, 1)); }defaultValue 和 required 这两个参数要合理设置pageNo 默认 1pageSize 默认 10keyword 可以不传。这里我传的 status 是 1表示毕业生端只能搜索到“发布中”的岗位企业下架的岗位不会出现在搜索结果里这是业务规则在接口层的体现。4.3 简历附件上传与访问上传路径和静态资源映射的正确姿势毕业生端的附件上传最常见做法是本地磁盘存储别往项目根目录存更别考虑放到数据库里存文件本身。本地磁盘存储的上传代码大概是PostMapping(/resume/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String fileName System.currentTimeMillis() _ UUID.randomUUID().toString(8) ext; File dest new File(uploadDir, fileName); file.transferTo(dest); return Result.success(fileName); }这里有两个刻意的设计。文件名用“时间戳 UUID 前八位”重组是为了避免用户上传的文件名冲突也防止中文文件名在 URL 回显时乱码。上传目录 uploadDir 是配置在 yml 里的绝对路径比如 D:/recruit/files这样项目打成 jar 包运行时也能正常读写。文件上传之后还要能被浏览器访问到Spring Boot 默认的静态资源目录是 classpath:/static不会指向你的磁盘目录所以必须手动映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadDir); } }addResourceHandler 里 /files/** 是访问路径addResourceLocations 里 file: 前缀表示文件系统绝对路径。这样用户上传后前端拼出“http://localhost:8080/files/xxx.pdf”就能直接打开预览。这个配置最容易犯的错是忘写 file: 前缀只传目录字符串那 Spring 会当作 classpath 路径去解析结果就是上传成功但前端永远 404这属于静态资源映射里很典型的一种“日志全绿但页面就是打不开”。跨域问题也在这里顺手处理掉。如果前端是 Vue 单独起的服务要在 WebMvcConfig 里加 CorsRegistry允许 8080 以外的端口访问后端接口Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); }allowedOriginPatterns 用星号配 allowCredentials(true)能兼容前端携带 token 的跨域请求。如果这里只配 allowedOrigins(*)某些浏览器会拦截带凭证的请求返回“CORS error”这也是常见坑。5. 避坑与常见问题启动失败、分页失效、文件 404 怎么快速定位5.1 现象yml 数据源配置没问题启动却报“Server returns invalid timezone”原因是 MySQL 8.0 的驱动要求必须显式指定时区。你本地 MySQL 服务器没设置 default-time-zone 参数的话驱动握手阶段直接拒绝连接。解决办法是在 jdbc url 里补上 serverTimezoneAsia/Shanghai完整配置在 2.3 节已经给过。如果补了还报错检查是不是驱动版本太低、mysql-connector-java 还是 5.1.x 的老驱动。5.2 现象Lombok 编译不过Data 注解的类 getter/setter 找不到这是 IDEA 环境下最经典的掉坑现场代码看着没问题一编译报几十个“找不到符号 getXxx”。原因是 IDEA 里 Lombok 注解处理器没有被启用或者项目用的 JDK 版本和 Lombok 版本不匹配。打开 IDEA 的 Settings → Build → Compiler → Annotation Processors勾选 Enable annotation processing。如果是新装的环境再检查 Lombok 依赖是不是在 pom 里写成了 optional true这是规范用法但有些旧的 IDEA 版本在 optional 依赖下不触发处理器的识别改成不带 optional 重新 import 一次 Maven 工程即可。5.3 现象上传文件 404控制台没有任何报错日志全绿上传的日志显示文件写到了磁盘前端拿到的文件名也是对的但浏览器访问 /files/xxx.pdf 就是 404。这个基本可以确定不是代码逻辑问题而是你上传目录的物理路径和静态资源映射不一致。我踩过最隐蔽的一次是WebMvcConfig 里 addResourceLocations 写的是 uploadDir 加上反斜杠Windows 下反斜杠在 Spring 的路径解析里会被吞掉。规范做法是配置里写绝对路径时统一用正斜杠比如 D:/recruit/files/代码里拼 File 路径用 Paths.get 而不是手动加分隔符。映射配置里还要保证路径末尾有斜杠file:D:/recruit/files/ 才是正确格式。5.4 现象MyBatis-Plus 分页失效selectPage 返回全部数据每页数量被忽略在 2.2 节说了MyBatis-Plus 3.5.x 的分页不是自带能力需要注册分页拦截器。少了这一步selectPage 看起来执行了但 SQL 里没有 LIMIT 语句一次性把所有记录查出来。更迷惑的是控制台打印的 SQL 确实是 SELECT * FROM job完全看不出分页意图。解决办法是加一个配置类注册 MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }DbType.MYSQL 这个参数很关键不同数据库的分页方言不同MySQL 和 PostgreSQL 的 LIMIT 语法不一样写错会在底层拼 SQL 时报错。还有一点分页插件要求传入的 pageNo 不能小于 1否则它会认为传的是 offset行为会变得奇怪。前端传参时注意做最小值校验。5.5 现象前端拿到的时间变成了“2024-06-01T10:00:00”中间多了个 Tjava.util.Date 序列化成 JSON 时默认输出 ISO 格式中间那个 T 既不好看在某些前端组件里还会解析失败。这在简历的“毕业时间”和投递记录的“投递时间”展示时经常碰到。在 application.yml 里配一个统一的日期格式比在实体类每个字段加注解要省事spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghaidate-format 作用于 java.util.Datetime-zone 解决服务器和本地时区不一致导致的时间偏移 8 小时问题。配置完成后重启前端拿到的就是“2024-06-01 10:00:00”这种人类友好格式。6. 答辩前最后一步事务回滚演示与投递顺序的三种加分做法6.1 投递接口加事务用一次故意异常展示回滚最后一个代码技巧是给投递接口加上事务并现场演示回滚。投递操作涉及两步往 application 表插投递记录、检查简历是否存在两步要么都成功要么都失败。加了 Transactional 的代码是这样的Override Transactional(rollbackFor Exception.class) public boolean applyJob(Long studentId, Long jobId) { Application app new Application(); app.setStudentId(studentId); app.setJobId(jobId); app.setStatus(0); applicationMapper.insert(app); Resume resume resumeMapper.selectById(studentId); if (resume null) { throw new RuntimeException(简历不存在投递失败); } return true; }rollbackFor Exception.class 表示任何异常都触发回滚包括 RuntimeException。答辩演示时你可以故意用一个没填简历的测试账号投递接口返回错误信息然后立刻打开数据库查 application 表发现刚刚那次 insert 被回滚掉了没有残留半条脏数据。这一整套动作演示下来事务的原子性就不只是课本上的概念而是你系统里的真实行为。6.2 演示顺序先跑业务主流程再补技术细节答辩演示的黄金顺序是注册一个毕业生账号 → 登录 → 搜索岗位 → 投递 → 切换企业账号 → 查看投递 → 更新状态。先把业务闭环跑通评委看出系统是“能用”的再回头讲技术点。技术细节挑三个讲JWT 鉴权、MyBatis-Plus 分页、Transactional 回滚每个点控制在两分钟以内。源码包里的 PPT 和演示视频建议按这个节奏来组织视频录一遍完整流程PPT 里放一张数据表关系图加一张投递状态机图就够了。6.3 演示前检查这三处避免现场翻车每一处都可能让演示戛然而止确认 MySQL 服务已启动且 init 脚本执行过确认后端启动端口没被占用换端口的话前端请求地址要同步改确认上传目录的磁盘路径存在且有写权限否则首次上传简历附件会直接抛 IOException。这三项是血泪经验换来的每一条我都亲自翻过车。这些年我带毕设养成的习惯是演示前一天用一台“干净电脑”跑一遍完整流程专挑破坏性操作测试系统反应。这个习惯让我躲过了很多次现场尴尬也希望帮到你。本文还有配套的精品资源点击获取