简介:这是一套基于 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 到前后端分离的选型对比
毕业生招聘平台这一类系统的技术选型,说白了就在下面几条路线里选:纯 JSP+Servlet、SSM(Spring 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 里的 serverTimezone=Asia/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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';这里三个细节值得抄。第一,role 用数字不用字符串,后续枚举判断方便,也暗示了这类字段是可扩展的。第二,unique key 加在 username 上,演示时反复注册同一账号不会给你现场报重复数据。第三,create_time 和 update_time 自动填充,答辩时展示“用户注册时间、信息更新时间线”是很自然的素材。
resume 表我建议用 user_id 做外键并加 unique 约束,保证一个毕业生只能有一条主简历。字段就是姓名、性别、毕业院校、专业、学历、工作经历、项目经历、附件路径这些,别贪多。简历在招聘平台里是“用户的扩展信息”,不是独立实体,这个认知能让你的 E-R 图画得干净。
3.2 投递状态状态机与冗余字段:application 表的三个关键设计
投递表是整个系统的业务灵魂。很多毕设把投递表做成 job_id + user_id + 时间,三个字段草草了事,结果答辩时老师一句“投递之后流程怎么走”就卡住了。一张能撑起故事的投递表至少要这样设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键自增 |
| student_id | BIGINT | 毕业生用户ID |
| job_id | BIGINT | 岗位ID |
| company_id | BIGINT | 冗余企业ID,方便企业端查列表 |
| status | TINYINT | 0 已投递 1 已查看 2 面试邀约 3 已通过 4 已结束 |
| remark | VARCHAR(255) | 企业回填的反馈或面试时间 |
| create_time | DATETIME | 投递时间 |
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, 'admin@example.com'), ('company01', 'e10adc3949ba59abbe56e057f20f883e', 2, 'hr@company01.com'), ('stu01', 'e10adc3949ba59abbe56e057f20f883e', 1, 'stu01@example.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> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</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 位,太短会抛 WeakKeyException;expire 我设 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 Page<Job> searchJobs(Integer pageNo, Integer pageSize, String keyword, Long companyId, Integer status) { Page<Job> page = new Page<>(pageNo, pageSize); LambdaQueryWrapper<Job> 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 Result<Page<Job>> 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 默认 1,pageSize 默认 10,keyword 可以不传。这里我传的 status 是 1,表示毕业生端只能搜索到“发布中”的岗位,企业下架的岗位不会出现在搜索结果里,这是业务规则在接口层的体现。
4.3 简历附件上传与访问:上传路径和静态资源映射的正确姿势
毕业生端的附件上传,最常见做法是本地磁盘存储,别往项目根目录存,更别考虑放到数据库里存文件本身。本地磁盘存储的上传代码大概是:
@PostMapping("/resume/upload") public Result<String> 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 里补上 serverTimezone=Asia/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,完全看不出分页意图。
解决办法是加一个配置类,注册 MybatisPlusInterceptor:
@Configuration 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”,中间多了个 T
java.util.Date 序列化成 JSON 时默认输出 ISO 格式,中间那个 T 既不好看,在某些前端组件里还会解析失败。这在简历的“毕业时间”和投递记录的“投递时间”展示时经常碰到。
在 application.yml 里配一个统一的日期格式,比在实体类每个字段加注解要省事:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghaidate-format 作用于 java.util.Date,time-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。这三项是血泪经验换来的,每一条我都亲自翻过车。
这些年我带毕设养成的习惯是:演示前一天用一台“干净电脑”跑一遍完整流程,专挑破坏性操作测试系统反应。这个习惯让我躲过了很多次现场尴尬,也希望帮到你。
本文还有配套的精品资源,点击获取