ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SpringBoot+Vue的学生求职就业系统实战项目详解

基于SpringBoot+Vue的学生求职就业系统实战项目详解 最近后台不少同学私信问有没有适合毕业设计或者找工作时拿来写进简历的实战项目而且点名要前后端分离、主流技术栈的。我翻了翻之前整理过的轮子把一套学生求职就业系统重新捋了一遍补了完整文档和数据库脚本今天拿出来单独聊聊。先说这套东西是什么一个基于SpringBoot Vue的学生求职就业系统面向学生、企业、管理员三种角色覆盖学生从浏览职位、投递简历到企业筛选邀约再到管理员做数据审核的完整闭环。它解决的是传统招聘流程里信息分散、进度不透明、简历重复投递无人跟进的问题核心是把求职和招聘搬到线上并且让每一环的状态都可查可追溯。适合拿来当毕业设计、课程设计也适合想系统走一遍前后端分离项目从零到一的同学做练手。之所以值得拿出来单独写一篇是因为它虽然叫“系统”但麻雀虽小五脏俱全权限控制、文件上传、状态流转、数据看板这些真实业务里绕不开的东西全都覆盖了。这篇我会把项目从设计思路、技术选型、数据库建模到核心接口实现、本地部署、常见坑一次性讲透源码和数据库脚本大家拿到之后能直接跑起来改。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot Vue而不是别的组合这个项目在选型上没有整花活SpringBoot Vue的搭配在当下国内中小型项目里属于绝对的主流原因很实在。后端用SpringBoot核心价值在于自动配置和起步依赖。对比传统的SSH或者裸Spring MVCSpringBoot把大量繁琐的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 本地环境准备与版本搭配很多人项目跑不起来八成是环境版本不匹配。这里直接给出我实测的版本组合照着装基本不会踩坑JDK1.8。不要用17虽然SpringBoot 2.7能跑但部分老依赖在17下会出幺蛾子。Maven3.6.3或3.8.x。不要用最新的3.9没发现明显问题但没必要冒险。Node16.20.x14也行。Vite 4要求Node 14.18以上Vue3生态完全支持。MySQL8.0注意安装时选utf8mb4字符集并在连接串中加serverTimezoneAsia/Shanghai。IDEA2022版本直接支持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?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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这里有两个容易被忽略的点。第一allowPublicKeyRetrievaltrue这个参数MySQL 8.0连接时会因为公钥检索问题报错加上就能解决。第二map-underscore-to-camel-casetrue必须开否则数据库的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 ServiceImplJobApplicationMapper, JobApplication implements JobApplicationService { Override public ResultVOInteger submitApplication(Integer jobId, Integer studentId) { // 1. 校验职位是否存在且已发布 JobPosition position jobPositionMapper.selectById(jobId); if (position null || position.getStatus() ! 1) { return ResultVO.error(职位不存在或已下线); } // 2. 校验是否重复投递 LambdaQueryWrapperJobApplication 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 ResultVOPageJobPosition 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) { LambdaQueryWrapperJobPosition 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:8080Windows上用netstat -ano | findstr 8080找到占用进程PID后结束进程或者直接在application.yml里改端口。5.2 前端npm install报错与页面白屏前端最大的坑在依赖安装。npm install的时候如果报ERR! code ERESOLVE通常是因为npm版本太高与某些依赖的peerDependencies冲突。最简单的解决方案是在项目根目录创建.npmrc文件写入registryhttps://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导出的完整代码先用着现在的版本把基础打牢有问题在评论区沟通。
返回列表