
简介一份基于Spring Boot的大学生就业需求分析系统毕业设计资源包主要面向计算机相关专业毕业生、课程设计学生及Java初阶开发者。系统围绕求职者与招聘单位双端设计覆盖简历创建、职位搜索、简历投递、面试准备、职位发布、简历筛选和面试安排等完整业务流程适合作为毕业设计、课题参考或项目实训模板。包体内含792个文件以java源码、vue前端页面、js逻辑、css样式、html静态页、sql数据库脚本及说明文档为主附带svg图标、gif演示图与mp4操作演示整体约26.92MB目录结构清晰便于按模块查阅。已有68人学习下载可帮助快速理解Spring Boot项目分层架构与前后端交互方式同时提供可直接导入运行的工程文件和数据库脚本节省从零搭建环境的时间适合用于论文配套演示与实际功能二次开发也有助于深入理解需求分析、数据建模及系统实现全过程。1. 基于SpringBoot的大学生就业需求分析系统拆开之后最该带走什么基于SpringBoot的大学生就业需求分析系统名字里的“分析”两个字容易让人误以为核心是统计报表。真正拆完这套Java毕业设计工程才发现大头工作量在求职者与招聘单位双向的业务闭环上求职者侧要完成简历创建、职位搜索、简历投递、进度查询招聘单位侧要完成职位发布、简历筛选、面试邀约。只有把这条链路完整跑通SpringBoot的配置管理、ORM映射、分页查询、状态流转和登录鉴权才算真正串联起来。这套系统适合两类人。一类是准备Java毕业设计的学生可以用它把基于SpringBoot的工程从骨架搭建到部署上线的完整路径走一遍另一类是负责Java技术带教的人这套表结构和状态机设计可以直接裁剪成企业内部招聘平台的第一版底座。下面按我拆这个项目的顺序把数据库建模、JWT认证、职位搜索、投递状态机和部署脚本逐一展开。2. 数据库建模先行——就业需求分析系统的表设计与索引取舍2.1 三个角色与两条业务主线如何收敛成表关系在写SpringBoot代码之前先把业务用例收敛成数据关系。系统内有三个角色求职者、招聘单位、管理员。求职者与招聘单位之间并不直接建立外键关联而是通过职位表和投递记录表间接联系。求职者维护一份简历招聘单位发布多个职位求职者对职位发起投递投递记录同时挂简历ID和职位ID。这个设计避免了用户表承载过重也方便在答辩时把业务闭环讲清楚。管理员角色在表结构层面不新增业务表只通过 t_user 的 role 字段区分。管理端关注的职位需求量、投递量、各企业发布量等数据全部通过对 t_position 和 t_application 的聚合查询获得。比如统计各行业职位需求分布一条 group by 语句就够统计投递转化率对 t_application 按 status 分组即可。把统计口径建立在真实业务表上比另建一张冗余统计表更扎实数据对得上也经得起追问。2.2 五张核心表的结构定义与字段说明下面是我在类似系统里常用的一组表结构。五张核心表分别是用户表、简历表、职位表、投递记录表和面试安排表。以简历表和职位表为例看字段设计思路CREATE TABLE t_resume ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 归属用户ID, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 联系电话, email varchar(100) DEFAULT NULL COMMENT 邮箱, education varchar(20) DEFAULT NULL COMMENT 最高学历, school varchar(100) DEFAULT NULL COMMENT 毕业院校, major varchar(100) DEFAULT NULL COMMENT 专业, work_years int(11) DEFAULT 0 COMMENT 工作年限, skill_tags text COMMENT 技能标签逗号分隔, self_evaluation text COMMENT 自我评价, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT求职者简历表; CREATE TABLE t_position ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 职位名称, company_name varchar(100) NOT NULL COMMENT 公司名称冗余字段, industry varchar(50) DEFAULT NULL COMMENT 所属行业, salary_low int(11) DEFAULT NULL COMMENT 薪资下限单位K, salary_high int(11) DEFAULT NULL COMMENT 薪资上限单位K, city varchar(50) DEFAULT NULL COMMENT 工作城市, description mediumtext COMMENT 职位描述, requirement mediumtext COMMENT 任职要求, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-下架 1-上架, creator_id bigint(20) NOT NULL COMMENT 发布人ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_create_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT招聘职位表;这段SQL包含两个值得展开的设计点。一是简历表对用户表的 1:1 约束用唯一索引 uk_user_id 保证简历可以反复修改但不会出现同一用户两份简历的脏数据二是职位表冗余了 company_name 字段因为招聘单位发布职位时已经填写了企业信息列表页展示无需二次联表。技能标签用 text 存逗号分隔在小规模系统里 like 匹配足够不必刻意拆标签中间表答辩被问“为什么违反第三范式”时可以给出读取频率和查询路径的理由。其余三张表与核心状态字段汇总如下表名存放内容关键关联字段状态字段设计t_user账号密码、角色、所属企业company_idrole0求职者 1招聘单位 2管理员t_resume简历主体user_id无状态通过是否有记录判断t_position职位主体creator_idstatus0下架 1上架t_application投递行为记录resume_id, position_idstatus0待查看 1已查看 2面试邀约 3已拒绝t_interview面试安排application_id, position_idround轮次status0待确认 1已确认 2已完成投递记录表还需要高频过滤字段的联合索引按两种最常见的查询路径建立ALTER TABLE t_application ADD INDEX idx_rid_pid (resume_id, position_id), ADD INDEX idx_pid_status (position_id, status);这里两个索引对应的场景分别是“我的投递列表”和“职位下的候选人筛选”。根据最左前缀原则idx_rid_pid 能同时覆盖按 resume_id 查询和按 resume_id position_id 查询两种条件idx_pid_status 则是为固定职位下按状态筛选候选人的高频操作准备的。如果后续 t_application 数据量到万级这两个索引能让绝大多数查询保持在索引扫描级别不需要回表扫描全量数据。2.3 部署前先解决的时区与字符集问题常见做法是把处理参数直接写进连接 URL。我在迁移到 MySQL 8 时遇到过通宵排查的问题本地连接正常部署到 Windows 服务器后抛The server time zone value ?й? is unrecognized or represents more than one time zone。这个异常信息在中文环境经常乱码很容易被误判成编码问题实际上是连接串缺了时区参数。spring: datasource: url: jdbc:mysql://localhost:3306/job_analysis?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue注意allowPublicKeyRetrievaltrue这个参数。MySQL 8 默认认证插件是 caching_sha2_password部分 JDBC 驱动在没有 SSL 加密连接时首次认证需要拿服务器公钥不带这个参数会报Public Key Retrieval is not allowed。项目里如果是 MySQL 5.7可以不加8.0 建议保留。这个细节在部署阶段比写业务代码更容易卡住先写进连接串能少踩很多坑。提示如果项目工程里带了 element.min.css、app.37d929b9.css 这类前端静态资源包统一拷贝到src/main/resources/static目录下SpringBoot 会自动映射为根路径静态资源无需额外写一个静态文件控制器。3. SpringBoot工程骨架与JWT认证链路的实现3.1 依赖选型SpringBoot 2.7.x 加 MyBatis-Plus 的取舍针对毕业设计、课程设计和中小型系统我会优先选 Spring Boot 2.7.x 配 MyBatis-Plus 3.5.x。理由有三2.7.x 是 Spring Boot 3 大规模普及前最后一个支持 JDK 8 的主线版本部署环境不用额外折腾JDK版本MyBatis-Plus 提供 LambdaQueryWrapper 和内置分页插件解决单表读写的样板代码能力比原生 MyBatis 强很多社区资料量最大网上排查问题基本能覆盖。Java 8 加 SpringBoot 2.7 的组合对当前大多数毕设和中小项目仍是稳定首选。dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.7.18/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency 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 dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies依赖层面有一个非常容易被忽略的点JJWT 0.11.x 必须同时引入 jjwt-api、jjwt-impl、jjwt-jackson 三个 artifact。网上很多教程只贴 api 和 impl运行时解析 token 会抛ClassNotFoundException: io.jsonwebtoken.lang.Classes。另外Spring Boot 2.7 对应的 mybatis-plus 版本不要误配成mybatis-plus-spring-boot3-starter那个是 Spring Boot 3 专用。依赖版本作用spring-boot-starter-web由 2.7.18 parent 管理Web 容器与 MVCmybatis-plus-boot-starter3.5.5ORM、条件构造器、分页mysql-connector-java8.0.33MySQL 8 驱动jjwt-api / impl / jackson0.11.5JWT 签发与校验三件套3.2 application.yml 里必须写对的三处配置第一个是数据源连接参数刚才已经提到时区与公钥第二个是 MyBatis-Plus 的驼峰映射与日志第三个是连接池参数。HikariCP 是 SpringBoot 默认连接池不需要额外引入依赖但参数必须显式声明。spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置项推荐值说明maximum-pool-size20单机部署够用过大反而增加数据库上下文切换minimum-idle5保留常驻连接避免流量突增时重复建连map-underscore-to-camel-casetrue数据库下划线字段自动映射Java驼峰属性log-implStdOutImpl开发期打印SQL上线前换成 NoLoggingImpl 或注释逻辑删除配置让 applicationMapper 的删除操作自动变成 update对投递记录这种需要留痕的表很有用。分页插件必须有一个配置类显式注册否则selectPage会退化成内存分页Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个类selectPage虽然不报错但会退化成查全表后在内存中手动分页。数据量小的时候看不出问题t_position 里插两万条测试数据之后页面会明显卡顿。答辩演示阶段插数据验证性能时这一步是首要检查项。注意log-impl: StdOutImpl会在控制台逐条打印SQL方便调试但上线前必须关闭否则日志文件一天能涨到几百MB。3.3 JWT Token 校验与登录用户上下文注入登录接口校验账号密码通过后签发一个包含 userId 和 role 的 token。后续请求在拦截器里解 token把用户信息放进 request attribute业务层直接从参数拿当前用户避免每个接口手动接收 token 再解析。Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String secret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } try { SecretKey key Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); Claims claims Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token.substring(7)) .getBody(); request.setAttribute(userId, claims.get(userId, Long.class)); request.setAttribute(role, claims.get(role, Integer.class)); return true; } catch (JwtException | IllegalArgumentException e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token无效或已过期\}); return false; } } }拦截器先放行 OPTIONS 预检请求然后从 Header 中取 Authorization Bearer token解析失败统一返回 401 JSON不让异常抛到全局处理器变成一屏堆栈。Keys.hmacShaKeyFor要求密钥长度不少于 32 字节jwt.secret 建议直接放一个 32 位以上的随机串不要用 abc123否则会直接抛 WeakKeyException。注册拦截器时注意排除登录注册接口否则把自己锁在门外Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/position/list); } }这段配置里职位列表接口排除在鉴权之外因为未登录用户也能浏览职位但投递、简历、面试相关接口全部走拦截器。角色校验在 Controller 层用注解或手动判断 role 值比如发布职位只允许 role1 的账号访问管理员接口只允许 role2逻辑简单且可以精确控制到每个接口。4. 职位搜索、简历投递与面试邀约的实现细节4.1 职位多条件搜索与分页的正确写法职位搜索是求职者端打开频率最高的接口需要支持按行业、城市、薪资区间、关键词的组合查询并带有分页。MyBatis-Plus 的 LambdaQueryWrapper 用条件函数包装查询条件避免手动拼接 SQL 时出现类似where 11的写法。public PagePositionVO searchPosition(PositionQuery query) { PagePosition page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperPosition wrapper new LambdaQueryWrapperPosition() .eq(StringUtils.hasText(query.getIndustry()), Position::getIndustry, query.getIndustry()) .eq(StringUtils.hasText(query.getCity()), Position::getCity, query.getCity()) .ge(query.getMinSalary() ! null, Position::getSalaryLow, query.getMinSalary()) .le(query.getMaxSalary() ! null, Position::getSalaryHigh, query.getMaxSalary()) .like(StringUtils.hasText(query.getKeyword()), Position::getTitle, query.getKeyword()) .eq(Position::getStatus, 1) .orderByDesc(Position::getCreateTime); IPagePosition result positionMapper.selectPage(page, wrapper); return PageResult.from(result); }StringUtils使用 Spring 自带的org.springframework.util.StringUtils.hasText条件为 false 时MyBatis-Plus 会跳过该条件不拼进 SQL。薪资过滤用ge和le分别对应“最低薪资不低于下限”和“最高薪资不高于上限”职位表薪资拆成 low 和 high 两个字段用区间判断比单一字段更符合业务直觉。keyword 用 like 搜 title 即可不要拖上 description 和 requirement 两个大字段避免模糊匹配直接让索引失效。这里有两个隐藏问题。第一薪资单位是 K 时前端传 15 代表 15K后端直接用整数比较不要因为前端传的是字符串而多做一个无意义的类型转换第二orderByDesc(Position::getCreateTime)依赖索引建表时idx_status_create_time已经包含 create_time上架状态加时间排序恰好命中索引。4.2 简历投递状态机与防重复校验投递行为是求职者端最核心的写操作。设计上我用一个 t_application 表记录每次投递状态码从 0 到 3状态流转规则如下状态码状态名触发动作求职者端展示文案0待查看求职者向职位发起投递已投递等待目标企业查看1已查看招聘单位打开投递详情目标企业已查看请留意后续通知2面试邀约招聘单位创建面试安排收到面试邀约请确认时间3已拒绝招聘单位标记不合适暂未匹配当前岗位需求状态机的核心落在投递这个动作上Service 层需要做一层顺序校验Transactional(rollbackFor Exception.class) public Long applyPosition(Long userId, Long positionId) { Resume resume resumeMapper.selectOne(new LambdaQueryWrapperResume() .eq(Resume::getUserId, userId)); if (resume null) { throw new BusinessException(400, 请先完善个人简历); } Long count applicationMapper.selectCount(new LambdaQueryWrapperApplication() .eq(Application::getResumeId, resume.getId()) .eq(Application::getPositionId, positionId)); if (count 0) { throw new BusinessException(400, 您已投递过该职位请勿重复投递); } Position position positionMapper.selectById(positionId); if (position null || position.getStatus() ! 1) { throw new BusinessException(400, 该职位已下架); } Application application new Application(); application.setResumeId(resume.getId()); application.setPositionId(positionId); application.setStatus(0); applicationMapper.insert(application); return application.getId(); }这段代码的关键点有三个。事务注解保证插入失败时前面的校验不产生副作用防重复投递的判断放在 Java 层而不是数据库唯一约束原因是重复投递需要返回可读的业务文案单纯靠唯一索引报 DuplicateKeyException 会让前端很难处理职位下架检查放在计数之后虽然多一次查询但拦截判断的顺序更符合业务直觉。答辩时这段逻辑可以直接展开不需要额外修饰。4.3 面试邀请的业务流转面试模块的粒度到“邀请-确认-完成”即可。招聘单位在投递详情页点击“邀请面试”后端在 t_interview 表插入一条记录同时把 t_application.status 更新为 2求职者端看到待确认的面试邀请确认后状态改为已确认面试结束招聘单位录入结果状态改为已完成。public void inviteInterview(Long applicationId, String address, LocalDateTime interviewTime, String contact) { Application application applicationMapper.selectById(applicationId); if (application null || application.getStatus() 1) { throw new BusinessException(400, 候选人简历未被查看不能发起面试邀约); } Interview interview new Interview(); interview.setApplicationId(applicationId); interview.setPositionId(application.getPositionId()); interview.setAddress(address); interview.setInterviewTime(interviewTime); interview.setContact(contact); interview.setRound(1); interview.setStatus(0); interviewMapper.insert(interview); Application update new Application(); update.setId(applicationId); update.setStatus(2); applicationMapper.updateById(update); }这里的状态校验是为了保证招聘单位不会对一个已拒绝的候选人发起邀约。application.getStatus() 1把状态 0 排除在邀约条件之外同时允许已查看1和已处于面试状态2的记录继续追加邀请比如一面完成后约二面。如果后续需求要支持多轮面试t_interview 的 round 字段从 1 递增面试表与投递表的 1:N 关系也能容纳不需要改表结构。5. 一键部署脚本、上线自检与性能兜底5.1 三个 bat 脚本的分工install / run / build工程解压后常见到1-install.bat、2-run.bat、3-build.bat三个文件这是把初始化、运行、打包分开的三个入口。我一般按下面这个逻辑组织echo off REM 1-install.bat 首次安装初始化数据库并拉取依赖 chcp 65001 echo [1/3] 检测 MySQL 服务... net start | find MySQL nul if errorlevel 1 net start MySQL80 echo [2/3] 创建数据库与表结构... mysql -uroot -proot -e CREATE DATABASE IF NOT EXISTS job_analysis DEFAULT CHARSET utf8mb4; sql/init.sql echo [3/3] 编译并安装本地依赖... call mvn clean install -DskipTests echo 安装完成 pauseecho off REM 2-run.bat 日常启动 chcp 65001 set APP_JARtarget\job-analysis-0.0.1-SNAPSHOT.jar if not exist %APP_JAR% ( echo 未找到JAR包请先执行 1-install.bat pause exit /b 1 ) start job-analysis java -jar %APP_JAR% --server.port8080 echo 服务启动中本地访问 http://localhost:8080echo off REM 3-build.bat 打包产出 chcp 65001 call mvn clean package -DskipTests if errorlevel 1 ( echo 打包失败 pause exit /b 1 ) copy /y target\job-analysis-0.0.1-SNAPSHOT.jar dist\app.jar echo 打包成功产物位于 dist\app.jar pauseinstall 只在第一次部署时执行负责拉起 MySQL、导入初始化 SQL 和编译项目run 是日常启动入口先检查 jar 是否存在再拉起服务避免空跑的窗口一闪而过build 在改完代码要出部署产物时使用。chcp 65001把控制台代码页切到 UTF-8否则 bat 里的中文注释在 Windows 上会乱码。三个脚本边界是“安装一次、运行多次、按需构建”实际部署时把 sql 目录放到工程根目录即可。5.2 上线前必查的连接池与索引参数把前面几章涉及到的连接坑汇总成一张自查表现象优先排查项连接报 Access denied账号密码、host 是否允许远程抛 time zone 异常连接 URL 缺 serverTimezoneAsia/Shanghai中文乱码连接 URL 缺 characterEncodingutf8Public Key Retrieval is not allowed连接 URL 缺 allowPublicKeyRetrievaltrue端口被占用换端口或 netstat -ano 找 PID 关闭进程索引方面建议在两万条测试数据量时自查一遍执行计划EXPLAIN SELECT * FROM t_application WHERE position_id 1 AND status 0;如果type列是 ALL说明索引没有命中需要确认联合索引是否建立、字段顺序是否符合最左前缀原则。position_id, status这个顺序能同时满足按职位过滤和按职位加状态过滤两种查询如果建反成status, position_id单独按 position_id 查询时索引直接失效。这个细节在演示时用两万条数据验证说服力要比口头描述强很多。本文还有配套的精品资源点击获取