基于SpringBoot+Vue的学生求职就业系统实战项目详解
2026/9/9 19:43:07 网站建设 项目流程

最近后台不少同学私信问有没有适合毕业设计或者找工作时拿来写进简历的实战项目,而且点名要前后端分离、主流技术栈的。我翻了翻之前整理过的轮子,把一套学生求职就业系统重新捋了一遍补了完整文档和数据库脚本,今天拿出来单独聊聊。

先说这套东西是什么:一个基于SpringBoot + Vue的学生求职就业系统,面向学生、企业、管理员三种角色,覆盖学生从浏览职位、投递简历到企业筛选邀约,再到管理员做数据审核的完整闭环。它解决的是传统招聘流程里信息分散、进度不透明、简历重复投递无人跟进的问题,核心是把求职和招聘搬到线上,并且让每一环的状态都可查可追溯。适合拿来当毕业设计、课程设计,也适合想系统走一遍前后端分离项目从零到一的同学做练手。

之所以值得拿出来单独写一篇,是因为它虽然叫“系统”,但麻雀虽小五脏俱全:权限控制、文件上传、状态流转、数据看板这些真实业务里绕不开的东西全都覆盖了。这篇我会把项目从设计思路、技术选型、数据库建模到核心接口实现、本地部署、常见坑一次性讲透,源码和数据库脚本大家拿到之后能直接跑起来改。

1. 项目整体设计与技术选型思路

1.1 为什么是SpringBoot + Vue,而不是别的组合

这个项目在选型上没有整花活,SpringBoot + Vue的搭配在当下国内中小型项目里属于绝对的主流,原因很实在。

后端用SpringBoot,核心价值在于自动配置和起步依赖。对比传统的SSH或者裸Spring MVC,SpringBoot把大量繁琐的XML配置直接干掉了。比如数据源、事务管理器、Jackson序列化这些,引入对应starter之后基本开箱即用。对于学生求职这种业务逻辑不算特别复杂的场景,SpringBoot能让你把精力集中在业务接口上而不是配置地狱里。网上大量吐槽的“SpringBoot版本太高导致配置项变化”“starter版本不兼容”问题,只要选一个稳定版本,比如2.7.x,基本一年内不用操心。

前端选Vue,理由同样直接:响应式数据绑定让表单、列表这类的交互开发效率极高,组件化拆分适合多人协作,而且Vue在国内社区活跃度极高,遇到问题基本搜一下就能找到答案。特别要提的是Vue 2和Vue 3的选择,这套项目用的是Vue 3 + Element Plus,原因在于Vue 3的组合式API写业务逻辑更顺手,配合Vite启动速度也快,毕业设计答辩时老师问起来也更有说头。

1.2 系统角色与核心业务流转

这套系统我最初设计时就定了三个角色:学生、企业、管理员。一个项目里能清楚区分三种权限体系并让数据相互流转,在简历里是很好的亮点。

学生的核心路径是:注册登录 -> 完善个人信息和简历 -> 浏览职位 -> 投递简历 -> 查看投递状态 -> 收到面试邀约。企业的核心路径是:注册并完善公司资料 -> 发布职位 -> 查看收到的简历 -> 筛选并更新投递状态 -> 发送面试邀约。管理员的核心路径是:登录后台 -> 审核企业信息 -> 审核职位发布 -> 查看全站数据统计。

这三条线不是孤立的,学生投递之后,企业端立即能看到对应简历;企业更新状态之后,学生端实时同步。整个状态流转我用一个投递状态字段控制,数据表里冗余存储状态值和状态说明,前端根据状态展示不同标签和按钮,这是项目里最核心的业务闭环。

1.3 技术栈清单与版本选择

这里把整个项目的技术栈列一下,版本均经过实测稳定:

技术分类具体选型版本建议说明
后端框架SpringBoot2.7.14稳定且资料多,避免3.x兼容性问题
持久层MyBatis-Plus3.5.3单表CRUD不用写SQL,复杂查询用Wrapper
数据库MySQL8.0 / 5.7均可推荐8.0,时区设置简单
权限认证JWT + Spring Security2.7.x对应版本前后端分离下最简单的鉴权方案
前端框架Vue 33.4+组合式API,逻辑复用方便
UI组件库Element Plus2.4+表格、表单、弹窗开箱即用
构建工具Vite4.x / 5.x开发环境秒启动
文档工具Knife4j4.3.0接口调试和后端文档生成

这套组合最大的好处是,即使你没有实际部署经验,照着官方文档搭环境也不会卡太久。我遇到过很多同学上来直接上SpringBoot 3.x,结果因为JDK版本不对或者jakarta包名迁移问题,卡在环境阶段好几天,没有必要。

2. 核心功能拆解与数据库设计

2.1 数据库设计心法:五张核心表怎么串起来

数据库设计是整个系统的地基。我刚拿到需求时没有急着建表,先画了一遍业务流转图,确认核心实体只有四类:用户(学生/企业共用)、公司信息、职位、简历投递记录。具体表结构如下:

  • sys_user:系统用户表。保存用户名、密码(BCrypt加密)、角色类型(student/company/admin)、手机号、邮箱、状态。
  • company_info:企业信息表。保存企业名称、统一社会信用代码、简介、行业、规模、地址、资质状态。
  • job_position:职位表。保存职位名称、所属企业ID、职位类型、薪资范围、学历要求、工作城市、职位描述、发布状态。
  • student_profile:学生信息表。保存姓名、学校、学历、专业、毕业时间、技能标签、个人简介。
  • resume:简历表。保存学生ID、简历文件名、附件路径、创建时间、更新时间。
  • job_application:投递记录表。保存学生ID、职位ID、投递状态、投递时间、企业备注。

核心关联关系是:sys_user表通过角色字段区分身份,company_info通过user_id关联到企业账号,job_position通过company_id关联到企业,job_application通过student_id和job_id分别关联到学生和职位。

2.2 状态字段设计:少用中间表,多用冗余字段

在投递状态设计上,我没有做复杂的流程表,而是用了一个int类型的status字段加上一个varchar类型的status_text字段。具体定义如下:

  • 0:已投递
  • 1:已查看
  • 2:已邀约面试
  • 3:已录用
  • 4:已拒绝

这里有个细节:为什么要在存一个status_text?因为前端列表页需要直接展示“已投递”“已邀约面试”这样的中文标签。如果只存status,前端每次都要做一次枚举映射;如果加一个冗余状态文本字段,查询结果直接返回,减少一次转换。代价是更新状态时Java代码需要同时维护两个字段,这个通过封装统一的updateStatus方法即可解决,不会漏。

2.3 简历文件怎么存:本地存储方案,别一上来就上OSS

不少同学一做到文件上传就想着接阿里云OSS或者七牛云,其实完全没必要。这个系统里简历文件量级不会很大,我把简历存储在服务器的uploads目录下,数据库只保存相对路径。上传接口接收MultipartFile,校验格式(仅支持pdf、doc、docx),限制大小5MB内,然后按日期目录归档存放。

存储路径的生成规则是:uploadPath + "/" + userId + "/" + dateFormat + "/" + UUID + originalFilename。加上用户ID和日期前缀,好处是有序、好排查,也避免重名覆盖。

补充说明,因为前后端分离项目前端和后端可能部署在不同域名或端口,文件访问时就需要前端在接口请求中通过后端代理访问uploads目录。部署时我给SpringBoot配置了静态资源映射,将/upload/**路径映射到本地uploads目录,这样简历附件URL就能直接通过后端域名访问了。

2.4 权限控制的三层设计

权限这块我用了Spring Security + JWT的组合。很多同学一听到Security就头大,觉得配置复杂。其实在前后端分离的项目里,核心就三件事:放开登录接口和静态资源、其他接口统一走JWT过滤器校验、根据角色注解做接口级权限控制。

具体实现上,我定义了一个JwtAuthenticationFilter,继承OncePerRequestFilter,从请求头取Authorization字段,去掉"Bearer "前缀后解析Token,拿到userId和role之后放入SecurityContext。在SecurityConfig里,定义了三条放行路径:/api/auth/、/doc.html、/upload/**。其余接口全部需要认证。角色控制通过在Controller方法上加上@PreAuthorize("hasRole('STUDENT')")来实现,比如投递接口只有学生能访问,发布职位接口只有企业能访问。

3. 实操环节:从配环境到跑通全流程

3.1 本地环境准备与版本搭配

很多人项目跑不起来,八成是环境版本不匹配。这里直接给出我实测的版本组合,照着装基本不会踩坑:

  • JDK:1.8。不要用17,虽然SpringBoot 2.7能跑,但部分老依赖在17下会出幺蛾子。
  • Maven:3.6.3或3.8.x。不要用最新的3.9,没发现明显问题但没必要冒险。
  • Node:16.20.x,14也行。Vite 4要求Node 14.18以上,Vue3生态完全支持。
  • MySQL:8.0,注意安装时选utf8mb4字符集,并在连接串中加serverTimezone=Asia/Shanghai。
  • IDEA:2022+版本直接支持Vue插件,省心。

3.2 后端项目配置详解

拿到源码之后,后端部分需要改的核心配置文件是application.yml,关键配置项如下:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/job_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你自己的密码 servlet: multipart: max-file-size: 5MB max-request-size: 10MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true jwt: secret: 你自己的盐值,建议32位以上随机字符串 expire: 604800

这里有两个容易被忽略的点。第一,allowPublicKeyRetrieval=true这个参数,MySQL 8.0连接时会因为公钥检索问题报错,加上就能解决。第二,map-underscore-to-camel-case=true必须开,否则数据库的create_time字段映射不到Java的createTime属性上,前端拿到的就是null。

数据库初始化直接使用我提供的sql文件即可,里面包含建库建表和基础数据。基础数据里我预置了三个测试账号:学生账号、企业账号、管理员账号,登录之后能看到不同端的不同菜单,方便快速联调。

3.3 前端项目配置与运行

前端部分需要改的地方不多,最重要的是vite.config.js里的代理配置。因为开发环境下前端跑5178端口,后端跑8080端口,跨域是免不了的。Vue3项目里,我配置了Vite的devServer代理:

server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true }, '/upload': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端代码里所有请求都写相对路径即可,例如axios.post('/api/application/submit', data),开发环境由Vite转发,生产环境则由Nginx转发。很多同学在这个地方直接写死http://localhost:8080/api/xxx,结果一到部署环境就要全局搜索替换,非常被动。

另外axios封装时要注意请求拦截器和响应拦截器。请求拦截器负责给每个请求头加上Authorization字段,从localStorage里取Token;响应拦截器统一处理401状态码,遇到Token失效就清空本地登录信息并跳转登录页。这些逻辑属于通用能力,在项目里封装好之后,后续所有页面开发都不用重复写鉴权逻辑。

3.4 核心接口实现实战:投递简历全链路

以投递简历这个核心动作为例,完整走一遍请求链路,你就能理解整个项目是怎么运作的。

前端投递按钮触发后,调用投递接口,传入职位的job_id。后端JobApplicationController接收到请求后,先校验当前登录用户角色是否为STUDENT,再判断该学生是否已经投递过该职位,这些操作封装在JobApplicationService中。

@Service public class JobApplicationServiceImpl extends ServiceImpl<JobApplicationMapper, JobApplication> implements JobApplicationService { @Override public ResultVO<Integer> submitApplication(Integer jobId, Integer studentId) { // 1. 校验职位是否存在且已发布 JobPosition position = jobPositionMapper.selectById(jobId); if (position == null || position.getStatus() != 1) { return ResultVO.error("职位不存在或已下线"); } // 2. 校验是否重复投递 LambdaQueryWrapper<JobApplication> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(JobApplication::getJobId, jobId) .eq(JobApplication::getStudentId, studentId); Long count = this.count(wrapper); if (count > 0) { return ResultVO.error("您已投递过该职位,请勿重复投递"); } // 3. 插入投递记录 JobApplication application = new JobApplication(); application.setJobId(jobId); application.setStudentId(studentId); application.setStatus(0); application.setStatusText("已投递"); this.save(application); // 4. 更新职位投递数量 jobPositionMapper.updateApplyCount(jobId); return ResultVO.success(); } }

这里的第二点值得展开说说:为什么要先校验是否重复投递。实际业务中,用户连续点击两次按钮,前端可能因为网络延迟没有及时禁用按钮,后端如果不做幂等校验就会出现两条重复的投递记录。在毕业设计答辩时能主动讲出“这个接口考虑了幂等性”,本身就是加分项。

企业筛选简历的逻辑同样简单清晰。企业端看到投递记录列表后,可以对某一条记录更新状态。后端提供updateStatus接口,接收applicationId和targetStatus,统一更新status和statusText。这里我用了一个静态方法转换:

public static String getStatusText(Integer status) { switch (status) { case 1: return "已查看"; case 2: return "已邀约面试"; case 3: return "已录用"; case 4: return "已拒绝"; default: return "已投递"; } }

4. 关于前端Vue实现:页面怎么组织更清晰

4.1 路由权限控制与菜单联动

前端这边,我用Vue Router带meta信息的模式实现页面权限控制。每个路由的meta里带roles字段,例如:

{ path: '/student', component: Layout, children: [ { path: 'job-list', name: 'JobList', component: () => import('@/views/student/JobList.vue'), meta: { roles: ['STUDENT'], title: '职位浏览' } } ] }

在全局前置守卫里统一判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { const role = localStorage.getItem('role') if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403') } else { next() } } })

这样可以做到:未登录只能访问登录页,学生访问企业端页面直接跳403。动态生成菜单我用的是遍历路由表筛选出当前角色可访问的菜单项,不引入vue-element-admin那套复杂的动态路由方案,够用且好理解。

4.2 表格页面的通用模式:搜索 + 分页 + 状态标签

这个项目里的列表页,无论是职位列表、投递记录还是企业管理,都遵循同一套模式:搜索栏 + 表格 + 分页。这里我封装了一个分页查询的公共方法,前端传pageNum、pageSize、keyword等参数,后端用MyBatis-Plus的Page对象接收,返回统一格式的数据。

以职位列表为例,后端接口长这样:

@GetMapping("/job/list") public ResultVO<Page<JobPosition>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) String city, @RequestParam(required = false) String jobType) { LambdaQueryWrapper<JobPosition> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(JobPosition::getTitle, keyword) .or().like(JobPosition::getDescription, keyword); } if (StringUtils.hasText(city)) { wrapper.eq(JobPosition::getCity, city); } if (StringUtils.hasText(jobType)) { wrapper.eq(JobPosition::getJobType, jobType); } wrapper.orderByDesc(JobPosition::getCreateTime); return ResultVO.success(jobPositionService.page(new Page<>(pageNum, pageSize), wrapper)); }

这样设计的好处是,前端只关心传参数和渲染返回结果,后端用Wrapper动态拼接查询条件,不需要为每种组合单独写SQL。如果后续加筛选条件,比如“只看一周内的新职位”,只需要在Wrapper里加一个ge条件即可,扩展成本极低。

5. 常见问题与排坑实录

5.1 后端启动失败:端口占用与数据库连接

第一个经典坑是数据库连接报错。默认配置里数据库名是job_system,如果你本地的库是别的名字,启动时就会直接抛出通讯链接失败异常。解决方式是在数据库软件里新创建一个job_system数据库,将sql文件导入,或者修改application.yml中的库名和账号密码。

第二个经典坑是端口占用。8080端口如果被其他程序占用,SpringBoot启动会直接退出。排查方法很简单,mac或Linux上用lsof -i:8080,Windows上用netstat -ano | findstr 8080,找到占用进程PID后结束进程,或者直接在application.yml里改端口。

5.2 前端npm install报错与页面白屏

前端最大的坑在依赖安装。npm install的时候如果报ERR! code ERESOLVE,通常是因为npm版本太高,与某些依赖的peerDependencies冲突。最简单的解决方案是在项目根目录创建.npmrc文件,写入registry=https://registry.npmmirror.com,并降低npm版本或者使用yarn安装。

另一个常见问题是页面白屏但控制台无报错。这种情况多半是路由mode设置问题。Vue Router如果用history模式,部署到静态服务器后,直接访问某个子路由会404。解决方式有两种:开发环境无所谓,生产环境用Nginx做try_files回退;或者干脆把mode改成hash模式,虽然URL带个#号,但部署省心。项目源码里默认用的hash模式,新手可以少踩坑。

5.3 跨域问题:为什么登录之后调接口还是401

如果你改了前端请求路径,没有走代理,而是直接请求http://localhost:8080,那就会出现跨域。虽然我后端配置了CorsFilter,但如果你自己加了拦截器或者Spring Security的配置顺序有误,跨域会先被拦截,导致前端收到401。排查思路是:先确认网络请求是否到达后端;再看后端响应头里有没有Access-Control-Allow-Origin字段;最后检查SecurityConfig中cors配置是否放行。

后端CorsFilter的核心配置如下,这个是最省心的全局跨域方案:

@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }

注意allowCredentials(true)时,allowedOrigin不能配置为"",必须用allowedOriginPattern(""),这是很多老代码在浏览器上报错的原因。

5.4 简历上传成功但前端预览不了

这个问题遇到的概率也很大。简历上传后,数据库存的是相对路径,前端需要拼接后端域名才能访问。如果你本地前端通过Vite代理访问,代理配置里已经包含了/upload路径,那请求/upload/xxx.png应该没问题。但如果直接用了localhost:8080之外的域名部署,就需要检查后端的静态资源映射是否生效。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("/upload/**") .addResourceHandler("/upload/**"); } }

我再补充一句,Upload目录的绝对路径建议配置成可配置项,比如放在application.yml里的custom.upload-path,这样部署在Linux服务器时只需要改一个配置项,不用重新打包。

6. 项目扩展思路与简历加分项

如果这套项目只是用来应付答辩,那现在已经完全够用了。但如果你想让它变成简历上的一个亮点,我建议从下面几个方向做延伸,每个方向都不会太难,但都是真实业务会用到的东西,面试官问起来你能讲出深度。

6.1 增加Redis缓存热点职位列表

目前职位列表每次查询都走MySQL,如果数据量大了,接口性能会明显下降。可以引入Redis,把首页推荐职位、热门职位列表缓存起来,设置5分钟过期。查询时先查缓存,缓存没有再去查数据库并回填。这个改造点虽然技术上不复杂,但体现的是性能优化意识。

6.2 简历导出为PDF

目前简历只是文件上传和预览,可以增加一个导出功能:把学生填写的在线简历信息,用Java后端生成PDF文件。可以引入itextpdf或xhtmlrenderer库,把简历模板用HTML写好后转成PDF,提供下载接口。这个功能很实在,企业在查看简历时可以直接下载归档,也是很多招聘系统的基本需求。

6.3 模拟消息通知

目前学生查看投递状态是靠主动刷新页面。可以引入WebSocket或者使用前端定时轮询,让企业在更新投递状态时,学生端实时收到通知。这个功能建议只要做简单版:前端每隔10秒调用一次查询是否有新通知的接口,有新数据就弹窗提示。因为不是核心流程,用轮询完全够用,而且实现简单、不容易出bug。

6.4 增加招聘数据看板

管理后台的首页目前只有简单的数据统计,可以扩展为带图表的可视化看板,展示职位发布趋势、投递量排行、企业活跃度等。前端可以用ECharts实现,后端提供汇总统计接口就好。这个功能一旦做出来,整体项目的完成度会提升一个档次,也是答辩时视觉效果最好的模块。

根据我个人的实操经验,这套项目最适合的学习路径是:先把数据库跑起来,用Swagger把后端接口一个个测通,然后启动前端,用测试账号登录,把核心流程(学生投递、企业更新状态、管理员审核)完整走一遍。把这条路走通之后,再去看源码里自己不理解的部分,效率是最高的,切忌一开始就埋头读代码,读了一周还停留在登录接口。

最后再分享一个自己在调试时特别实用的小技巧:前端项目里我加了vite-plugin-mock的配置,当你不想依赖后端时,可以用Mock数据先开发页面。但注意mock插件一定不要在build生产时启用,否则打包出来的项目所有接口都返回mock数据,部署上线后一片空白。代码里我已经通过环境变量做了判断,开发环境加载mock,生产环境忽略,大家改的时候不要把这层逻辑动掉。这个项目后续我还会更新一版带消息通知和PDF导出的完整代码,先用着现在的版本把基础打牢,有问题在评论区沟通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询