尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

大学生就业招聘系统:Java SpringBoot+Vue3前后端分离毕设实战

发布时间:2026/9/26 11:34:09

资讯中心
01
ARTICLE

大学生就业招聘系统:Java SpringBoot+Vue3前后端分离毕设实战

大学生就业招聘系统:Java SpringBoot+Vue3前后端分离毕设实战
最近抽空把之前做的大学生就业招聘系统整理成了完整源码技术栈是 Java SpringBoot Vue3 MyBatis MySQL前后端分离数据库用 MySQL 8.0。这个选题在课程设计、毕业设计里非常常见学生端投简历、企业端发职位、管理员后台审核三条业务线闭环走完几乎覆盖了 Java 全栈面试里最喜欢问的登录鉴权、分页查询、多表关联、文件上传这些点。如果你正在准备课设、毕设或者想找一个能写进简历的实战项目这套东西可以直接拿来改、拿来跑。我写这篇文章不只是贴源码结构和目录而是把从需求分析到数据库设计、从后端接口实现到前端页面联调、再到部署运行遇到的坑完整复盘一遍。所有表结构、核心代码、配置步骤都是我在实际开发中验证过的不是网上那种只给你一个 README 的空壳项目。1. 项目整体设计与需求拆解1.1 从需求文档到功能模块做这类系统最忌讳一上来就建表写代码先把角色和业务用例理清楚后面所有代码都是顺着用例长出来的。大学生就业招聘系统说白了就是三方角色围绕“职位”和“简历”这两条主线转。学生端需要注册登录、完善个人简历基本信息、教育经历、项目经历、技能标签、浏览职位、按关键词和城市筛选、投递简历、查看投递状态、收藏职位、接收企业的面试邀约。企业端需要注册后由管理员审核通过、发布职位、下线职位、查看收到的简历、筛选候选人、发出面试通知。管理员端则是企业账号审核、职位审核、学生账号管理、基础数据统计。这三个角色的功能叠加起来后端接口基本可以归成四类认证接口登录注册、token刷新、学生业务接口简历、投递、收藏、企业业务接口职位管理、简历处理、管理端接口审核、统计。前端就是对应四套页面路由层面用角色字段做访问控制学生访问企业后台就直接重定向到403页面。1.2 为什么选这套技术栈前后端分离的取舍逻辑可能有人会问这种课设级别项目用 JSP Servlet 不是更快吗为什么非要上 SpringBoot Vue3 前后端分离我说点实在的。第一SpringBoot 让后端开发重心完全落在业务代码上不用再配置一堆 XML内嵌 Tomcat 直接 java -jar 就能跑这对课程设计来说极大降低了部署门槛。第二MyBatis 比 JPA 更适合这种面试导向的项目因为很多 Java 面试题就是问 MyBatis 中 #{} 和 ${} 的区别、一级缓存二级缓存、分页插件原理你亲手在项目里用过这些东西面试答起来完全不一样。第三不用 MyBatis-Plus 是我故意做的选择目的就是让你把手写 SQL、resultMap 映射、动态 SQL 这些基本功练扎实实际工作中很多公司还是以手写 MyBatis 为主。前端选 Vue3 Vite Element Plus 也是同理。Vite 冷启动快开发体验比 Vue CLI 舒服太多Element Plus 组件库能快速把后台管理界面搭出来样式不用自己从零写。前后端分离之后后端只出 JSON 接口前端通过 Axios 调接口渲染页面这种模式现在是企业绝对主流你做完这个项目再去看任何后台管理系统都似曾相识。2. 数据库设计与核心表结构2.1 用户中心与角色设计用户表我采用了单表加角色字段的设计没有拆成 student 表、company 表、admin 表然后各自独立登录。原因很简单三个角色登录认证的逻辑完全一样用户名、密码、手机号、状态这些字段是公共的拆开反而让认证接口变成三份重复代码。我只需要在 tb_user 表里用一个 role 字段区分身份然后学生和企业的详细信息各自放到扩展表里。role 字段我用的是 tinyint0 表示学生、1 表示企业、2 表示管理员。为什么不直接存字符串因为 tinyint 占用空间小、对比速度快而且代码里用常量类定义不会出现字符串拼写不统一的问题。这是 tb_user 的表结构我简化后直接贴出来CREATE TABLE tb_user ( id int NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint NOT NULL DEFAULT 0 COMMENT 角色0学生 1企业 2管理员, 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), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT用户表;这里有两个关键设计。用户名和手机号都建了唯一索引因为手机号可以作为登录账号的替代方案如果你后续要接入短信登录这个索引就是现成的。密码字段长度我给了 100因为 BCrypt 加密后的字符串是 60 位后续如果改用其他加密算法也不会因为长度不够而报错。status 字段用于管理员冻结账号这属于业务状态不要和逻辑删除混在一起。学生扩展表存姓名、性别、出生日期、最高学历、毕业院校、专业、工作年限、期望城市、自我评价、头像等。企业扩展表存企业全称、统一社会信用代码、所属行业、规模、简介、营业执照图片地址。企业注册时是待审核状态管理员通过后 status 才变成 1这个过程在代码里通过一个审核记录表来追踪。2.2 职位、简历与投递记录职位表是业务核心字段设计直接决定查询和筛选好不好写。我给出核心建表语句CREATE TABLE tb_position ( id int NOT NULL AUTO_INCREMENT COMMENT 职位ID, company_id int NOT NULL COMMENT 企业ID, title varchar(100) NOT NULL COMMENT 职位名称, category varchar(50) DEFAULT NULL COMMENT 职位类别Java/前端/测试等, city varchar(50) DEFAULT NULL COMMENT 工作城市, salary_min int DEFAULT NULL COMMENT 薪资下限K, salary_max int DEFAULT NULL COMMENT 薪资上限K, education varchar(20) DEFAULT NULL COMMENT 学历要求, experience varchar(20) DEFAULT NULL COMMENT 经验要求, description text COMMENT 职位描述, requirement text COMMENT 任职要求, status tinyint DEFAULT 1 COMMENT 职位状态1招聘中 0已下线, view_count int DEFAULT 0 COMMENT 浏览次数, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_company_id (company_id), KEY idx_category_city (category, city) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT职位表;薪资我特意拆成 salary_min 和 salary_max 两段而不是直接存一个字符串“8k-13k”。因为字符串没法做范围筛选学生搜“10k-15k”的职位时SQL 要能同时比较下限和上限。这种看起来很小的设计其实反映了对查询场景的理解面试官问起来你就讲这个点。投递记录表 tb_delivery 是这个系统的关键表它记录了学生和职位之间的动作。我在 user_id 和 position_id 上建了联合唯一索引直接从数据库层面杜绝重复投递。投递状态用 tinyint 表示0 待处理、1 已查看、2 已通过、3 已拒绝。每次状态变更记录更新时间方便学生端展示进度。这里说一个重要经验表之间的关联不要加物理外键。外键约束在并发写入时会带来额外的锁开销而且后续做分库分表、数据迁移都非常痛苦。现在主流做法是只建普通索引关联关系在 SQL 里通过 JOIN 维护。你可能会觉得没有外键数据会乱但实际上业务代码里的逻辑判断完全能保证一致性这个取舍在面试中也能体现你对数据库设计的理解。2.3 初始化数据与 SQL 脚本数据库初始化我准备了完整的 init.sql包括建库、建表、插入管理员账号和测试数据。字符集一定要用 utf8mb4不要用 utf8因为 utf8 在 MySQL 里最多存 3 个字节遇到表情符号就报错。插入管理员账号的 SQL 如下密码是 BCrypt 加密后的值你直接用源码里的密码常量注册登录即可INSERT INTO tb_user (username, password, role, status) VALUES (admin, $2a$10$E.UesuNkuBox4EYCMuQmUeBcY2nudoEOCSs9Qh0yW3N9Y2Hn/q8bW, 2, 1);测试数据我准备了 10 家企业、20 个职位、若干学生账号行业覆盖 Java、前端、运维、测试、产品等方向。建议初始化时多造一些数据后面测试分页、搜索、筛选功能才有真实感空表测试很多问题测不出来。3. 后端核心实现与关键代码3.1 SpringBoot 分层架构与统一返回结果后端包结构我按 controller、service、mapper、entity、dto、common、config 来分。entity 对应数据库表dto 是接口传输对象避免把实体类直接暴露给前端。实体类里的 password 字段加了 JsonIgnore防止序列化时把密码返回到前端。所有接口统一返回 Result 对象结构是 code、message、data。code 为 200 表示成功400 表示参数错误401 表示未登录或 token 失效500 表示服务器异常。前端 axios 拦截器只要判断 code 不是 200 就弹对应提示整个项目的错误处理就统一起来了。统一返回类代码很简单重点在于全局异常处理。我在项目里加了 RestControllerAdvice 全局异常处理器业务异常通过自定义 BizException 抛出参数校验异常通过 MethodArgumentNotValidException 捕获。这样 controller 里就不需要写一堆 try-catch代码非常干净。这个设计是面试加分项很多人做课设项目每个接口单独 try-catch那代码根本没法看。3.2 MyBatis 与分页插件列表查询的正确打开方式列表接口是高频接口职位列表、投递记录、用户管理都涉及分页。我选的是 PageHelper 分页插件因为它侵入性最小不用写 limit 参数也不用自己数总数。集成步骤很简单pom.xml 加依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependencyapplication.yml 只需要一个配置pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: trueservice 层用法非常简单public PageResultPositionVO listPositions(PositionQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListPositionVO list positionMapper.selectPositionList(query); return PageResult.build(new PageInfo(list)); }注意一个关键点PageHelper 只对紧随其后执行的一条 SQL 生效。所以在调用 startPage 之后必须立刻执行 mapper 查询方法中间不要穿插其他查询。如果你在 startPage 之后又去查了用户表分页就会加错地方总数也乱了。我见过很多新手在这个地方踩坑排查半天发现是查询顺序问题。另一个要点是 MyBatis 的多表关联查询。职位列表需要展示企业名称和 LOGO我的做法是在 Mapper 里用 resultMap 做映射。手动写 resultMap 确实比 MyBatis-Plus 的注解麻烦但它是 MyBatis 面试题的高频考点你亲手配置过 collection 和 association对数据映射的理解会深很多。3.3 登录认证与权限控制JWT Token 的落地细节登录认证我选择 JWT 而不是传统 Session理由有三前后端分离后Session 依赖 Cookie跨域场景下 Cookie 处理麻烦移动端假如要复用后端接口Session 没法用JWT 天然适合分布式系统以后系统扩容加一台服务器不需要做 Session 共享。我用的是 jjwt 库版本这里特别注意0.9.1 版本依赖 jaxbJDK 11 以上运行会报错。建议直接用 0.11.5API 稍有不同但稳定很多。核心代码我拆成 JwtUtil 工具类包含生成 token 和解析 token 两个方法public String generateToken(Integer userId, String username, Integer role) { Date now new Date(); Date expiryDate new Date(now.getTime() 7 * 24 * 60 * 60 * 1000); return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }token 过期时间设为 7 天这个时间要权衡用户体验和安全。太短学生投个简历就过期要重新登录太长又有安全风险。项目里我还在拦截器里做了自动刷新处理token 剩余有效期不足 2 天时重新签发新 token 放在响应头里。权限控制通过 HandlerInterceptor 实现在 preHandle 方法里先检查请求头是否携带 token然后解析校验最后把用户信息放入 ThreadLocal。这里要说一个细节拦截器里放行的路径要配置好登录接口、注册接口、文件访问路径、静态资源都要放行否则前端还没登录就被拦截器挡掉了。密码存储我用的是 BCrypt。不用 MD5 的原因很简单MD5 是消息摘要算法不是为密码存储设计的彩虹表可以轻易破解BCrypt 自带盐值每次加密结果都不一样即使两个用户密码相同密文也不相同。这是安全常识写进简历是亮点面试官也爱问。3.4 简历文件上传与静态资源映射学生在简历模块需要上传 PDF 或 Word 文件。文件上传的核心实现就是 SpringBoot 的 MultipartFile但我在这里遇到一个最容易被忽略的坑文件存储路径和访问路径的映射。文件不能直接存到数据库的 BLOB 字段因为数据库会迅速膨胀备份恢复都成问题。正确做法是存到本地磁盘一个目录数据库里只存文件的相对路径。配置文件里我用自定义属性upload: path: D:/data/upload/然后写一个 WebMvcConfigurer把 /upload/** 的请求映射到本地磁盘目录Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceResolver(new PathResourceResolver() { // 实际项目中直接使用绝对路径映射即可 }); registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); }这个映射不配置的话前端 img 标签访问 http://localhost:8080/upload/xxx.pdf 会 404但文件明明已经存在磁盘上问题就在虚拟路径没做映射这个坑很典型。文件上传我还加了大小限制application.yml 里配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB超过大小会抛出 MaxUploadSizeExceededException我在全局异常处理器里单独捕获这个异常返回友好提示。另外在上传时把文件扩展名做白名单校验只允许 pdf、doc、docx、jpg、png防止有人上传 JSP 或 exe 文件这是最基础的安全意识。4. Vue3 前端实现要点4.1 工程创建与基础配置前端部分我用 Vite 创建 Vue3 项目命令就是 npm create vite选择 Vue 模板。这里需要提醒一下Vite 要求 Node.js 16 以上装的时候先检查 node -v 版本很多新人在这一步就卡住了。项目依赖我安装了 vue-router、pinia、axios、element-plus。Element Plus 按需引入还是全量引入课设项目全量引入就行省事按需引入配置起来多一层麻烦对最终打包体积影响不大。main.js 里挂载 Element Plus 和路由import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)Vue3 项目的核心思路就是组件化。我会把页面拆成布局组件和视图组件登录页是一个独立视图学生端是一个带侧边栏的布局企业端和管理员端各自有自己的布局。路由做嵌套侧边栏菜单根据角色动态渲染这个结构后面加新页面只是加一个路由和对应的 view 文件。4.2 Axios 封装与拦截器前后端联调的关键在 axios 封装。我在 src/utils/request.js 里创建了 axios 实例baseURL 设置为 /api这样开发时通过 Vite 代理转发到后端 8080 端口生产环境可以把前端静态文件部署到 Nginx再把 /api 反向代理到后端服务整个项目不用改任何代码。请求拦截器里把 token 从 localStorage 取出来放到请求头service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config })响应拦截器统一处理业务码service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { router.push(/login) return Promise.reject(new Error(res.message)) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )这里说个我踩过的坑在响应拦截器里弹错误提示时同一个页面上多个请求同时失败会连续弹出好几个 Message 弹窗非常烦人。后来我封装了一个防抖提示函数相同错误内容在一段时间内只弹一次。这种体验细节虽然不是功能需求但做完之后整个项目质感会提升一个档次。4.3 核心页面拆解学生端的职位列表页是核心页面。搜索区放职位名称、城市、薪资范围三个筛选条件点击搜索后重新请求第一页数据。列表用 el-table 展示职位名称、企业名称、城市、薪资、学历要求、发布日期最右侧是“投递”和“收藏”按钮。投递按钮点击后调用后端接口如果后端返回重复投递提示前端就直接禁用按钮。简历编辑页我用了动态表单教育经历和项目经历都是可以增减的数组。Element Plus 的 el-form 用 v-for 渲染动态表单项校验规则要写在 form 的 rules 里。这里要注意动态表单的 model 绑定新增一行时要用数组的 push 方法同时为了 Vue 3 的响应式不能直接改数组索引必须用 Vue 推荐的响应式方法。企业后台页面需要 tab 切换已发布职位、收到的简历、面试管理。已发布职位用表格展示每条数据有下线按钮收到的简历列表根据投递状态显示“通过”“拒绝”按钮。这个页面的数据基本来自三个不同的接口但页面骨架完全一致所以我抽了一个通用的列表组件传入接口地址和列配置就能复用。路由守卫是前端权限控制的核心。在 router.beforeEach 里判断目标路由的 meta.roles 是否包含当前用户角色不包含就跳转 403 页面。比如企业后台路由 meta.roles 是 [1]学生登录后访问这个路径会被拦住。前端控制虽然安全等级不如后端但它能挡住绝大多数越权访问提升用户体验。5. 部署、联调与常见问题排查5.1 本地环境准备新手第一次跑这个项目最容易栽在环境配置上。我按自己的实际顺序列一下后端需要 JDK 8 或 17我推荐 JDK 17SpringBoot 用 2.7 系列完全兼容。Maven 用 3.8 以上配置阿里云镜像仓库否则依赖下载能等半小时。MySQL 安装时注意设置 root 密码字符集选 utf8mb4然后新建数据库导入 init.sql。开发工具用 IDEA打开项目后等待 Maven 导入依赖注意 JDK 版本要和项目的编译版本一致否则启动直接报错。前端需要 Node.js 16 以上npm 换淘宝镜像源安装依赖后 npm run dev 启动开发服务器默认端口是 5173后端是 8080。所以 vite.config.js 里需要配置开发代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这里 rewrite 的作用是把 /api 前缀去掉后端接口设计时不需要带 /api 前缀前端所有请求统一加 /api代理转发时再拆掉。这样做的原因是生产环境 Nginx 可以继续做同样的反向代理前后端联调和生产环境部署用同一套路径逻辑。5.2 前后端联调的常见坑我把实际联调过程中遇到的坑统一整理成一张速查表按出现频率排序现象原因解决方案前端请求接口报 404后端接口路径和前端请求路径不一致检查 controller 的 RequestMapping 注解登录后请求其他接口返回 401拦截器校验失败看请求头是否带 tokentoken 是否过期前端页面跨域请求被浏览器拦截后端没有开启跨域或代理配置错误开发环境用 Vite 代理生产用 Nginx上传的文件访问 404静态资源虚拟路径没有映射配置 addResourceHandlers分页数据不对或总数异常PageHelper.startPage 调用后混入其他查询保证 startPage 后紧跟目标查询数据库中文乱码连接串没设置字符集URL 加 useUnicodetruecharacterEncodingutf8端口被占用启动失败Tomcat 8080 被其他程序占用换端口或杀掉进程这里特别说一下跨域问题。开发环境我用的是 Vite 代理方案前端请求 /api 开头Vite 代理转发到后端浏览器看到的请求是同源的所以不需要在后端配置 CORS。如果后端配置了 CORS 又同时用了代理有时候反而会因为重复添加跨域头导致问题。我通常就是二选一开发用代理后端保持干净。5.3 面试怎么讲这个项目项目做完之后下一步就是把它讲清楚。我建议你在简历上这样描述独立设计并开发了大学生就业招聘系统采用 SpringBoot Vue3 前后端分离架构实现了学生、企业、管理员三类角色的完整业务闭环包含职位发布、简历投递、在线审核、数据统计等模块。面试官针对这个项目最可能问的点我提前帮你列几个为什么用 JWT 不用 Session答前端分离后 API 无状态设计JWT 自包含校验适合分布式扩展详细答案见 3.3 节。分页插件底层是怎么实现的答PageHelper 是基于 MyBatis 拦截器的启动时把当前线程的 Page 参数绑定到拦截器上下文中SQL 执行时拦截器拦截 Executor 的 query 方法改写 SQL 拼接 limit并自动执行 count 查询执行完清除 ThreadLocal。多表查询是怎么做的答通过 resultMap 做关联映射collection 标签处理一对多集合属性SQL 层用 LEFT JOIN 一次查出来避免 N1 查询。权限控制除了前端路由守卫还做了什么答后端拦截器校验 token 后解析角色在需要权限的接口上通过自定义注解或者代码判断角色双重校验防止绕过前端直接请求 API。你在面试中如果能把这些问题答清楚这个项目的含金量就直接拉满了。不要只停留在“我用了 SpringBoot 和 Vue”这种层面要做到每个技术选型都能说出一二三。我个人在实际开发中的体会是这个项目是少数几个能把前后端全链路走通还包含完整业务闭环的课设选题。从需求梳理到表设计从接口开发到页面联调每一步都会逼着你做取舍。比如薪资字段拆两段、投递加唯一索引、PageHelper 的 ThreadLocal 机制、JWT 过期策略这些点没做过真的学不会。最后再分享一个小技巧如果你时间紧张建议先做核心链路——登录、职位列表、投递简历、企业处理投递。把这条链路彻底跑通再补收藏、审核、统计这些附加功能。核心功能做到位项目就立住了附加功能是锦上添花。做完之后记得写一份 README把启动步骤和环境要求写清楚无论交作业还是开源给同学参考都会方便很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。