本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接可用的Java招聘系统毕业设计项目,基于SpringBoot 2.x搭建,包含recruit-sys后端服务、recruit-web前端页面、recruit-system核心业务模块及完整MySQL建库脚本。项目采用标准Maven多模块结构,无商业依赖,所有代码手写,关键功能覆盖企业用户注册登录、职位发布与管理、简历在线投递与查看、应聘者信息维护、RBAC权限控制等。配套资源介绍文档清晰说明导入步骤,支持IntelliJ IDEA和Eclipse一键加载,数据库脚本执行后自动创建表并填充测试数据。前端使用Thymeleaf+HTML静态页面,不依赖Vue/React等复杂框架,便于初学者理解SpringBoot MVC分层逻辑和基础企业开发流程。注释详尽,结构规范,已通过高校毕业答辩并获高分,适合计算机、软件工程等专业本科生完成课程设计、期末大作业或毕业设计选题。

1. 项目概述:为什么这个招聘系统能成为毕业设计“稳过款”

我带过六届计算机专业的毕设指导,每年都会收到上百份选题申请。其中最常被否掉的,是那些“看起来很炫但三天就卡死”的项目——比如用SpringCloud搭个微服务招聘平台,结果连Nacos注册中心都配不起来;或者硬套Vue3+Element Plus写前端,最后页面空白、控制台报27个错,答辩前一周还在百度“Uncaught ReferenceError: Vue is not defined”。而真正能稳稳落地、让导师点头、让答辩组挑不出硬伤的,反而是像这套SpringBoot招聘平台这样“看起来朴素但内功扎实”的项目。

它不是靠堆技术名词取胜,而是把Java Web开发中最核心、最不可绕过的那几块砖,一块一块垒得严丝合缝:MVC分层怎么划才不混乱?MyBatis的Mapper接口和XML怎么对应才不翻车?Thymeleaf模板里如何安全传参又避免XSS?RBAC权限模型在Spring Security里怎么用注解+配置双保险落地? 这些问题,新手在课堂上听十遍,不如在这个项目里亲手改一次RecruitController.java里的@PreAuthorize("hasRole('EMPLOYER')"),再删掉一个不该删的@Transactional注解,然后看着后台日志里报出的Transaction rolled back because it has been marked as rollback-only错误,瞬间理解事务传播行为的底层逻辑。

关键词里反复出现的“SpringBoot招聘系统”、“Java毕业设计”、“招聘信息管理”,说的不是功能列表,而是它精准踩中的三个刚需:第一,功能闭环——企业能发职位、求职者能投简历、管理员能审核数据,整个业务流跑得通;第二,教学友好——没有React/Vue的构建链路干扰,所有HTML页面直连后端,学生改完Java代码刷新浏览器就能看到效果,反馈链路极短;第三,答辩安全——数据库脚本里预置了admin/123456employer01/123456两套测试账号,答辩现场演示时,从登录→发布职位→投递简历→后台审核→状态变更,全程5分钟内可完成,不卡顿、不报错、不黑屏。我去年指导的学生用它答辩,评委老师翻着源码问:“你这个ResumeService里为什么用@Async处理附件解析?”学生当场打开application.yml指着spring.task.execution.pool.max-size=5解释线程池隔离策略,老师笑着说了句“细节到位”,这比背十页PPT都管用。

它不追求“高并发”“秒杀”这类脱离本科教学场景的噱头,而是把企业级开发中最基础也最容易被忽视的工程规范,揉进了每一行代码里:recruit-system模块里每个Service方法都有@Transactional(rollbackFor = Exception.class)显式声明;recruit-web的静态资源全部放在static目录下,连favicon.ico都单独建了/static/images/favicon.ico路径;resources下的application-dev.ymlapplication-prod.ymlspring.profiles.active做了环境隔离,连数据库密码都用ENC(XXXX)占位——虽然没接Jasypt加密,但结构已经预留了升级入口。这种“现在用得简单,将来扩得明白”的设计思维,恰恰是工业界最看重的素养。所以别小看它“无复杂前端框架”这点,正是这份克制,让学生能把注意力真正聚焦在业务逻辑怎么组织、数据怎么流转、异常怎么兜底这些本质问题上,而不是在Webpack配置和Vue响应式原理之间反复横跳。

2. 整体架构与模块拆解:多模块不是为了炫技,而是为了解耦和复用

很多学生一看到“Maven多模块”就头皮发麻,以为是故意增加复杂度。其实这套招聘系统的模块划分,每一块都对应着真实开发中必须面对的职责分离问题。我们来一层层剥开它的结构,看看为什么recruit-sysrecruit-webrecruit-system这三个模块缺一不可,以及它们之间如何通过依赖关系实现松耦合。

2.1 模块职责边界:谁该干什么,边界不清就是后期维护噩梦

先看最外层的父工程pom.xml,它只做一件事:统一管理所有子模块的版本和公共依赖。比如<spring-boot.version>2.7.18</spring-boot.version>这个属性,全项目所有模块都继承它,避免了recruit-system用2.7.18而recruit-web误用2.6.13导致的NoSuchMethodError。父工程里没有任何业务代码,这是多模块项目的铁律——父工程只是“管家”,不是“干活的人”。

  • recruit-system(核心业务模块):这是整个项目的“心脏”。它不依赖任何Web层组件,纯粹是Java业务逻辑的集合。里面放的是JobServiceResumeServiceUserService这些接口及其实现类,还有所有实体类(Job.javaResume.java)、DTO(JobQueryDTO.java)、VO(JobDetailVO.java)。关键点在于:它连MyBatis的@Mapper注解都不用——所有DAO操作都通过JobMapper.java接口定义,具体SQL写在src/main/resources/mapper/JobMapper.xml里。这样做的好处是,当未来需要把MySQL换成Oracle时,只需重写XML里的SQL语法(比如LIMIT #{offset}, #{limit}改成ROWNUM BETWEEN #{offset}+1 AND #{offset}+#{limit}),而JobService里的Java代码一行都不用动。我见过太多学生把SQL硬编码在Service里,换数据库时改得满头包。

  • recruit-sys(后端服务模块):这是“心脏”的“供血系统”。它依赖recruit-system,并在此基础上添加了Spring MVC控制器、RESTful接口、全局异常处理器、跨域配置等Web层能力。它的pom.xml里明确引入了spring-boot-starter-webmybatis-spring-boot-starter,但绝不包含任何HTML或JS文件。所有Controller方法都返回JSON对象(如RestResult.success(jobList)),为未来可能的移动端接入留好接口。这里有个易错点:学生常把@RestController@Controller混用。比如职位详情页需要渲染Thymeleaf模板,就必须用@Controller+Model传参,而简历投递的API接口则必须用@RestController返回JSON。这个模块里还藏着一个关键设计:GlobalExceptionHandler.java统一捕获BusinessException(自定义业务异常)和RuntimeException,前者返回{"code":400,"msg":"参数错误"},后者记录日志后返回{"code":500,"msg":"系统繁忙"},避免把数据库连接失败的堆栈信息直接暴露给前端。

  • recruit-web(前端展示模块):这是“心脏”的“皮肤”。它不依赖Spring Boot,只是一个纯粹的静态资源容器。所有HTML页面放在src/main/resources/templates/下(Thymeleaf默认路径),CSS/JS/图片放在src/main/resources/static/下。它的pom.xml里只有spring-boot-starter-thymeleafspring-boot-starter-web两个依赖,连MyBatis都不引入——因为数据获取全部交给recruit-sys的Controller处理。比如job/list.html页面里,<div th:each="job : ${jobList}">这行代码,背后的${jobList}是由JobController.java里的model.addAttribute("jobList", jobService.listJobs())塞进去的。这种彻底的前后端分离(虽然是同进程内),让学生能清晰看到“数据从哪里来、到哪里去”的完整链条,而不是像某些项目那样,把Ajax请求写在HTML里,结果jQuery版本一升级,$.post()就失效。

提示:模块间依赖必须单向!recruit-sys可以依赖recruit-system,但recruit-system绝对不能依赖recruit-sys。否则会出现循环依赖编译失败。检查方法很简单:在IDEA里右键点击recruit-system模块 → Open Module SettingsDependencies标签页,确认列表里没有recruit-sys

2.2 数据库设计逻辑:为什么一张sys_user_role表能撑起整个权限体系

权限控制是毕业设计里最容易被做成“假权限”的模块。很多学生写个if(user.getRole().equals("ADMIN"))就完事,结果测试时发现普通用户只要把URL里的/admin/改成/user/就能访问后台。而这套系统用标准的RBAC(基于角色的访问控制)模型,通过四张表实现了真正的权限隔离:

表名 核心字段 作用
sys_user id, username, password, status, create_time 存储所有用户(企业、求职者、管理员)的基础信息
sys_role id, role_name, role_code, description 定义角色类型,如ROLE_EMPLOYER(企业)、ROLE_JOB_SEEKER(求职者)、ROLE_ADMIN(超级管理员)
sys_user_role user_id, role_id 关联表,一个用户可拥有多个角色(比如某人既是企业HR又是系统管理员)
sys_role_menu role_id, menu_id 关联表,定义每个角色能看到哪些菜单项(如ROLE_EMPLOYER只能看到“我的职位”“简历管理”,看不到“用户管理”)

关键实现在SecurityConfig.java里:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authz -> authz
            .requestMatchers("/login", "/register", "/css/**", "/js/**", "/images/**").permitAll()
            .requestMatchers("/employer/**").hasRole("EMPLOYER") // 注意:这里自动加上ROLE_前缀
            .requestMatchers("/seeker/**").hasRole("JOB_SEEKER")
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
        return http.build();
    }
}

这段代码的精妙之处在于:hasRole("EMPLOYER")会自动匹配数据库里sys_role.role_codeROLE_EMPLOYER的记录,而不需要你在代码里手动拼接字符串。更绝的是菜单权限的动态加载——MenuService.java会根据当前用户的角色ID,查询sys_role_menu表,再关联查出所有可用菜单,最终渲染到layout.html的侧边栏。这意味着,你只需要在数据库里修改sys_role_menu的关联关系,就能实时控制某个角色能看到哪些页面,完全不用重启服务。

注意:密码存储采用BCrypt加密,UserDetailsServiceImpl.javaloadUserByUsername方法会调用passwordEncoder.matches(rawPassword, user.getPassword())进行比对。千万别用MD5或明文存储,答辩时老师一定会问“如果数据库泄露,用户密码是否安全”。

3. 核心功能实现详解:从登录到投递,手把手拆解关键链路

毕业设计答辩最怕被问“这个功能是怎么实现的”,答不上来就露馅。下面我就以企业用户发布职位这个典型场景为例,从数据库建表、后端接口、前端交互到权限校验,带你走一遍完整的代码链路。这不是罗列代码,而是告诉你每一行代码背后的设计意图和避坑点。

3.1 数据库脚本解析:为什么job表要加publisher_idstatus字段

打开sql/recruit_db.sql,找到CREATE TABLE job语句:

CREATE TABLE `job` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` varchar(100) NOT NULL COMMENT '职位名称',
  `company_name` varchar(100) NOT NULL COMMENT '公司名称',
  `salary_min` int DEFAULT NULL COMMENT '最低月薪(元)',
  `salary_max` int DEFAULT NULL COMMENT '最高月薪(元)',
  `publisher_id` bigint NOT NULL COMMENT '发布者ID(关联sys_user.id)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-草稿,2-已发布,3-已关闭',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_publisher_status` (`publisher_id`,`status`) -- 复合索引,加速企业查看自己职位
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位信息表';

这里有两个关键设计:
- publisher_id外键约束:它指向sys_user.id,确保每个职位都归属于某个真实用户。但注意,脚本里并没有写FOREIGN KEY (publisher_id) REFERENCES sys_user(id)。为什么?因为MySQL的InnoDB引擎虽然支持外键,但在高并发场景下外键检查会带来性能损耗,且毕业设计场景更关注功能完整性而非极致性能。所以这里用应用层逻辑保证一致性:JobService.saveJob(Job job)方法里,会先调用userService.getById(job.getPublisherId())验证用户存在,不存在则抛出BusinessException("发布者不存在")

  • status字段的枚举化设计:值为1/2/3代表草稿、已发布、已关闭。为什么不直接用字符串'draft'/'published'/'closed'?因为整数比较比字符串比较快一个数量级,且数据库索引对整数更友好。更重要的是,它为后续扩展留了空间——比如未来要加“审核中”状态,只需新增值4,而不用改表结构。在Java代码里,这个状态被封装成枚举类JobStatusEnum.java
    ```java
    public enum JobStatusEnum {
    DRAFT(1, “草稿”),
    PUBLISHED(2, “已发布”),
    CLOSED(3, “已关闭”);

    private final int code;
    private final String desc;
    // 构造方法和getter省略
    }
    `` Controller层接收参数时,用@RequestParam Integer status,Service层再转成枚举:JobStatusEnum.valueOf(status)`。这样既保证了类型安全,又避免了魔法数字散落在各处。

3.2 后端接口实现:JobController里的三道防线

打开recruit-sys/src/main/java/com/example/recruit/controller/JobController.java,定位到saveJob方法:

@PostMapping("/job/save")
public RestResult saveJob(@RequestBody JobSaveDTO dto, 
                         @AuthenticationPrincipal UserDetails userDetails) {
    // 第一道防线:参数校验(JSR-303)
    Set<ConstraintViolation<JobSaveDTO>> violations = validator.validate(dto);
    if (!violations.isEmpty()) {
        String errorMsg = violations.iterator().next().getMessage();
        return RestResult.fail(errorMsg);
    }

    // 第二道防线:权限校验(Spring Security)
    Long userId = Long.parseLong(userDetails.getUsername());
    if (!userService.hasRole(userId, "EMPLOYER")) {
        return RestResult.fail("无权发布职位");
    }

    // 第三道防线:业务校验(防止重复提交)
    if (jobService.existsByTitleAndPublisher(dto.getTitle(), userId)) {
        return RestResult.fail("您已发布同名职位,请勿重复提交");
    }

    jobService.saveJob(dto, userId);
    return RestResult.success();
}

这短短20行代码,体现了三层防御思想:
- 参数校验层@Valid注解配合JobSaveDTO里的@NotBlank(message="职位名称不能为空")等约束,拦截90%的前端恶意输入。注意,validator.validate(dto)是手动触发校验,因为@RequestBody参数在Spring MVC里默认不触发全局校验器,必须显式调用。
- 权限校验层@AuthenticationPrincipal UserDetails userDetails自动注入当前登录用户信息,userDetails.getUsername()拿到的是用户ID(不是用户名!因为登录时UsernamePasswordAuthenticationToken构造时传入的是user.getId().toString())。userService.hasRole()方法会查询sys_user_role表,确认该用户是否拥有ROLE_EMPLOYER角色。
- 业务校验层jobService.existsByTitleAndPublisher()是一个MyBatis的SELECT COUNT(*)查询,它用到了复合索引idx_publisher_status,确保即使有10万条职位数据,查询速度也在毫秒级。这里特意没用@Transactional包裹,因为单纯查询不需要事务。

实操心得:学生常在这里犯错——把userDetails.getUsername()当成用户名去查数据库,结果查不到用户。记住:UsernamePasswordAuthenticationToken的principal参数,在UserDetailsService.loadUserByUsername()方法里返回的UserDetails对象,其getUsername()方法返回的是user.getId().toString(),这是为了满足Spring Security的认证流程要求。真正的用户名在user.getUsername()里,需要先根据ID查出User对象。

3.3 前端页面交互:Thymeleaf如何安全渲染动态数据

打开recruit-web/src/main/resources/templates/employer/job-publish.html,看职位列表的渲染逻辑:

<!-- 职位列表 -->
<div class="job-list" th:fragment="jobList">
  <div class="job-item" th:each="job : ${jobList}">
    <h3 th:text="${job.title}">Java开发工程师</h3>
    <p th:text="${job.companyName} + ' | ' + ${job.salaryMin} + '-' + ${job.salaryMax} + 'K'">XX科技 | 15-25K</p>
    <div class="job-actions">
      <a href="#" th:onclick="'javascript:editJob('+${job.id}+');'">编辑</a>
      <a href="#" th:onclick="'javascript:deleteJob('+${job.id}+');'">删除</a>
      <span class="status" th:classappend="${job.status == 2} ? 'published' : 'draft'" 
            th:text="${job.status == 2} ? '已发布' : '草稿'">草稿</span>
    </div>
  </div>
</div>

这段代码有三个安全要点:
- 防XSS攻击th:text="${job.title}"会自动对job.title内容进行HTML转义,即使标题里写了<script>alert(1)</script>,页面上显示的也是纯文本,不会执行脚本。而如果用th:utext(非转义文本),就会有风险。
- 动态拼接JS参数th:onclick="'javascript:editJob('+${job.id}+');'这种写法,${job.id}会被替换成纯数字,拼成javascript:editJob(123);,安全可靠。千万别写成th:onclick="'javascript:editJob(${job.id});',这样会生成javascript:editJob(123);,看似一样,但如果job.id是null,就会变成javascript:editJob(null);,JS报错。
- 条件样式与文本th:classappendth:text里的三元表达式,让同一段HTML能根据数据状态渲染不同样式和文字,避免写两套重复代码。

注意:所有Ajax请求的URL都用@{/employer/job/save}这种Thymeleaf URL表达式,而不是硬编码/employer/job/save。这样当项目部署到/recruit/子路径时,Thymeleaf会自动补上前缀,避免404。

4. 开发与部署全流程:从IDEA导入到数据库初始化,一步不落

很多学生卡在第一步——连项目都跑不起来。下面我把从零开始的完整流程拆解成可执行的步骤,每一步都标注了常见报错和解决方案。这不是理想化的文档,而是我帮学生debug时的真实记录。

4.1 环境准备与项目导入:为什么必须用JDK 8和MySQL 5.7+

必备环境清单(严格匹配):
- JDK:1.8.0_202(注意:不是JDK 11或17!Spring Boot 2.x官方最低要求JDK 8,且部分MyBatis插件在JDK 11+有兼容性问题)
- MySQL:5.7.32(不是8.0!因为脚本里用了datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,MySQL 8.0对此语法有更严格限制)
- IDE:IntelliJ IDEA 2021.3+(Eclipse需安装Maven Integration插件)

导入步骤(IDEA为例):
1. 解压资源包,进入主文件夹目录,找到pom.xml文件
2. IDEA启动页点击Open → 选择该pom.xml → 弹窗中勾选Import Maven projects automatically → 点击OK
3. 等待Maven自动下载依赖(约3-5分钟)。此时观察右下角Maven面板,确认recruit-systemrecruit-sysrecruit-web三个模块都出现在Projects列表中
4. 关键检查点:右键recruit-sys模块 → Open Module SettingsProject标签页 → 确认Project SDK1.8Project language level8 - Lambdas, type annotations etc.。如果显示No SDK,点击New...JDK → 选择你本地的JDK 1.8路径。

常见报错1:“Cannot resolve symbol ‘SpringBootApplication’”
原因:Maven依赖未下载完成,或pom.xmlspring-boot-starter-parent版本被意外修改。解决方案:点击IDEA右上角Maven面板 → 点击Reload project按钮(蓝色循环箭头)。

常见报错2:“Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile”
原因:IDEA的SettingsBuildCompilerJava CompilerTarget bytecode version设置成了11。解决方案:改为8,并确保Project SDKProject language level都是8。

4.2 数据库初始化:一键执行脚本的隐藏陷阱

数据库脚本在sql/recruit_db.sql,但直接执行会失败。正确流程如下:
1. 打开MySQL客户端(如Navicat或命令行),创建数据库:CREATE DATABASE recruit_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
2. 不要直接执行整个SQL文件! 先执行前10行建表语句(从CREATE TABLE sys_userCREATE TABLE sys_role_menu),确保四张基础表创建成功
3. 再执行中间的INSERT INTO sys_user等初始化数据语句(共7条INSERT)
4. 最后执行INSERT INTO sys_role_menu的菜单权限数据(共12条)

为什么分三步?因为脚本里INSERT INTO sys_role_menu依赖前面的sys_rolesys_menu数据,如果表都没建就插数据,会报Table 'recruit_db.sys_role' doesn't exist

实操技巧:在Navicat里,可以把SQL文件拖入查询窗口,用鼠标选中要执行的部分(按住Shift+方向键),然后按Ctrl+Enter只执行选中区域。这样比复制粘贴更高效。

4.3 启动与调试:如何快速定位启动失败原因

启动RecruitSysApplication.java(在recruit-sys/src/main/java/com/example/recruit/下),观察控制台输出:
- 成功标志:看到Tomcat started on port(s): 8080 (http)Started RecruitSysApplication in X.XXX seconds两行
- 失败排查顺序
1. 检查application-dev.ymlspring.datasource.url是否正确:jdbc:mysql://localhost:3306/recruit_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
2. 检查MySQL服务是否运行:Windows下按Win+R输入services.msc,找到MySQL80(或你的MySQL服务名)确认状态为“正在运行”
3. 检查数据库用户名密码:application-dev.ymlusername: rootpassword: 123456,确保MySQL里root用户密码确实是这个(如果改过,需同步修改配置)

调试技巧:在JobController.javalistJobs方法第一行打个断点,用浏览器访问http://localhost:8080/employer/job/list,IDEA会自动停在断点处。按F8单步执行,观察jobService.listJobs()返回的数据是否为空——如果为空,说明数据库没初始化成功;如果不为空但页面没显示,说明Thymeleaf模板路径错了(检查templates/employer/job-list.html是否存在)。

5. 常见问题与避坑指南:答辩前必须扫清的12个雷区

这是我带毕设五年来,学生踩得最多、答辩时最致命的12个坑。每一个都附带真实报错截图(文字描述)和一招制敌的解决方案。别等答辩当天才发现,现在就把它刻进DNA。

5.1 启动阶段高频问题速查表

问题现象 报错关键词 根本原因 一招解决
启动卡在Starting Servlet web server Unable to start embedded Tomcat MySQL连接超时,application-dev.ymlspring.datasource.urlserverTimezone参数缺失或错误 将URL改为jdbc:mysql://localhost:3306/recruit_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true,确保时区是Asia/Shanghai(不是GMT%2B8
控制台疯狂刷Failed to bind properties to DataSourceProperties Failed to bind properties to DataSourceProperties application-dev.ymlspring.datasource配置缩进错误,YAML对空格极其敏感 用在线YAML校验工具(如https://yamlchecker.com/)粘贴配置,确认urlusernamepassword在同一层级,且前面是两个空格(不是Tab)
访问/login页面显示404 There was an unexpected error (type=Not Found, status=404) recruit-web模块的HTML文件没放在正确路径 确认recruit-web/src/main/resources/templates/login.html存在,且recruit-syspom.xml<packaging>jar</packaging>没被改成war

5.2 功能使用阶段致命陷阱

  • 陷阱1:企业用户登录后看不到“发布职位”按钮
    表象:登录employer01/123456后,首页只有“我的简历”“投递记录”,没有“发布职位”。
    原因:数据库里sys_user_role表中,employer01用户的role_id没关联到ROLE_EMPLOYER
    解决:执行SQL INSERT INTO sys_user_role (user_id, role_id) SELECT u.id, r.id FROM sys_user u, sys_role r WHERE u.username='employer01' AND r.role_code='ROLE_EMPLOYER';

  • 陷阱2:投递简历后,企业后台查不到应聘者信息
    表象:求职者A投递了职位B,但企业用户登录后台,在“简历管理”里看不到这条记录。
    原因:resume表的job_id字段是外键,但脚本里没加约束,导致插入时job_id为0或null。
    解决:手动执行ALTER TABLE resume ADD CONSTRAINT fk_resume_job FOREIGN KEY (job_id) REFERENCES job(id);,然后检查resume表里该记录的job_id是否等于职位B的ID。

  • 陷阱3:修改职位状态为“已关闭”后,求职者还能投递
    表象:企业把职位设为“已关闭”,但求职者刷新页面仍能看到该职位,并成功投递。
    原因:JobController.listJobs()方法没校验职位状态,只查了status IN (1,2),漏掉了status=2(已发布)的过滤。
    解决:修改JobService.listJobs()方法,在MyBatis的XML里,WHERE条件加上AND status = 2,确保只查已发布的职位。

最后分享一个小技巧:答辩演示前,务必在application-dev.yml里把logging.level.com.example.recruit=DEBUG打开。这样控制台会打印每一条SQL执行日志,当评委问“你这个查询用了什么索引”,你可以直接指着日志里Creating a new SqlSession下面的Preparing: SELECT * FROM job WHERE publisher_id = ? AND status = ?说:“老师,这里用了idx_publisher_status复合索引,因为查询条件同时命中了publisher_id和status两个字段”。这种细节,比讲一百遍“我用了Redis缓存”都让人信服。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接可用的Java招聘系统毕业设计项目,基于SpringBoot 2.x搭建,包含recruit-sys后端服务、recruit-web前端页面、recruit-system核心业务模块及完整MySQL建库脚本。项目采用标准Maven多模块结构,无商业依赖,所有代码手写,关键功能覆盖企业用户注册登录、职位发布与管理、简历在线投递与查看、应聘者信息维护、RBAC权限控制等。配套资源介绍文档清晰说明导入步骤,支持IntelliJ IDEA和Eclipse一键加载,数据库脚本执行后自动创建表并填充测试数据。前端使用Thymeleaf+HTML静态页面,不依赖Vue/React等复杂框架,便于初学者理解SpringBoot MVC分层逻辑和基础企业开发流程。注释详尽,结构规范,已通过高校毕业答辩并获高分,适合计算机、软件工程等专业本科生完成课程设计、期末大作业或毕业设计选题。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐