不做毕设里的“空壳项目”。很多同类系统停在增删改查层面,界面堆得花里胡哨,实际审题、投递、流转的逻辑一碰就碎。这篇博文我想拆的这套大学生就业招聘系统,是用 Spring Boot + Vue 搭的全栈项目,带源码、数据库脚本和完整文档,覆盖学生、企业、管理员三个端口,从职位发布、简历投递、状态管理到数据统计都能跑通。如果你正在做 Java 课程设计、毕业设计,或者想找一个能写进简历的真实全栈练习项目,这套东西的模块划分和表结构可以直接拿来当参考骨架,尤其适合用来理解“一个业务闭环从前端页面到后端接口再到数据库是怎么串起来的”。
写这篇文章之前,我自己把项目从头到尾跑了一遍,边跑边整理,所以下面所有内容都来自实际操作,不是把 README 抄一遍。我会先把整套系统的设计思路和模块边界说透,再逐个拆后端核心接口怎么写的、前端页面怎么接数据的、数据库表为什么这么建,最后把部署和联调时最容易踩的坑列出来,看完你应该能对“基于 Spring Boot + Vue 的全栈项目”这回事有一个从抽象到具体的感觉。
1. 项目核心思路拆解:一套招聘系统到底在管什么
1.1 三个端口的角色划分与核心诉求
这套系统要解决的实际问题很明确:校园招聘场景里,学生找工作、企业招人、学校/平台方做审核和管理,三方信息流转效率太低。企业发布职位后靠邮箱收简历,学生投出的简历石沉大海,管理员想统计就业数据又拿不到统一口径。所以系统按角色拆成三个端口,每个端口只做自己的核心事,不越界。
- 学生端(前台用户):注册登录、浏览职位、搜索筛选、投递简历、查看投递反馈、收藏心仪职位、维护个人简历信息。
- 企业端(平台入驻方):企业注册认证、发布职位、管理已发布职位、接收和筛选简历、更新投递状态(笔试、面试、录用、淘汰)。
- 管理端(平台运营方):审核企业入驻、审核职位信息、管理公告、用户管理、数据统计看板。
这样拆的好处在于:第一,业务边界清晰,后端 Controller 不会变成一坨大杂烩;第二,权限模型好做,三个角色对应三套接口权限,拦截器里按角色白名单控制即可。
很多新手做这类项目容易犯一个设计错误:把学生和企业的功能硬塞到一个端口里,通过一个下拉框切换角色。表面看着功能齐全,实际一旦要做权限控制和数据隔离就会非常痛苦。所以“三端分离”不只是页面层面的分离,而是后端接口、数据结构、权限验证都对应分离,这是整套系统里我认为最值得借鉴的架构决策。
1.2 功能模块的全景图与“刚需优先”的取舍
这套系统的基础功能分成六大模块,围绕“发布职位—投递简历—反馈结果”这条主线展开:
| 模块 | 核心功能点 | 涉及角色 |
|---|---|---|
| 用户认证 | 注册、登录、退出、信息修改 | 学生/企业 |
| 职位管理 | 发布、编辑、上下架、审核 | 企业/管理员 |
| 简历管理 | 在线编辑、上传附件、预览 | 学生 |
| 投递管理 | 投递职位、状态流转、取消投递 | 学生/企业 |
| 企业管理 | 企业入驻、认证审核、企业信息维护 | 企业/管理员 |
| 系统管理 | 公告管理、用户管理、数据统计 | 管理员 |
做功能取舍时我的原则是“刚需优先,玩票靠后”。比如站内信、在线聊天、AI 职位推荐这种功能,听起来很加分,但实际做起来会占用大量开发时间,而且和“招聘”这个核心闭环的关系是弱关联。这套系统把聊天功能去掉,把省下来的精力放在投递状态流转和职位审核流程上,这才保证了核心流程真正跑得通。
一个真实的工作流程举例:学生登录后看到企业发布的“Java 开发实习生”职位,点投递,系统生成一条投递记录,状态为“待筛选”。企业端登录后看到这条投递,点击“通过初筛”,状态变成“笔试”。学生端再刷新,就能看到这条记录已经进入笔试阶段。如果企业长时间未处理,学生还能主动“取消投递”。这个流程虽然简单,但完整覆盖了招聘闭环,而且每一个状态变化都有对应的数据记录。
1.3 为什么用“源码 + 数据库 + 文档”的方式交付
拿到这套项目的时候,我第一反应是看交付物齐不齐。因为经历过太多“源码能打开但数据库只有一堆 SQL 注释”、“文档写得像说明书但没有架构图”的项目了。这套系统的方式值得表扬:源码是完整的 Maven 工程,后端、前端分目录放好;数据库脚本里建库建表、初始数据、演示账号一应俱全;文档里的需求分析、数据库设计、接口说明、部署手册分段清晰。
对要做课设或毕设的同学来说,这个交付方式本身就是一种示范。你在做自己的项目时,也应该把数据库脚本和部署文档当成一等公民来对待,而不是随便塞一个.sql文件。因为答辩时老师大概率会现场要求你“把数据库重新初始化一遍”或“换台电脑把这个系统跑起来”,这时候一套干净、可复现的交付物就是你的底牌。
2. 技术栈与核心原理:Spring Boot + Vue 为什么是黄金组合
2.1 后端选型:Spring Boot + MyBatis-Plus 的现实理由
后端用的是 Spring Boot 2.7 + MyBatis-Plus + MySQL。很多人会问,为什么不用更“新潮”的 Spring Cloud 或者 JPA?这里有一个很实在的答案:这是一个单体应用,Spring Cloud 那套微服务拆分根本没有业务支撑场景,硬上只会让项目变得臃肿,也让答辩时“为什么这么设计”这个问题变得难以回答。
Spring Boot 自带内嵌 Tomcat,打成 jar 包就能跑,部署成本极低。MyBatis-Plus 很关键,它在 MyBatis 基础上做了增强,单表 CRUD 不用写 SQL,内置分页插件,对这类业务系统来说开发效率提升得非常明显。比如职位列表的分页查询,我只需要写一个 LambdaQueryWrapper,设置条件,然后调用 selectPage 方法,不用手写 XML 里的动态 SQL,出错的概率也小很多。
安全方面做了三层防护:用户密码 MD5 加盐存储,不能明文入库;登录成功后签发 JWT,后续请求在拦截器里校验 token;通过自定义注解加角色判断,限制接口访问权限。
MD5 加盐这一点多说两句。很多课程设计里密码直接明文存,答辩演示时无所谓,但一旦项目上线,这就是安全隐患。加盐的写法也不难,盐值可以取用户名或随机字符串,和密码拼在一起再做 MD5。这种方式不是最强的,但作为课程设计项目的安全意识展示完全够用。
2.2 前端选型:Vue 3 + Element Plus 的工程化套路
前端用的是 Vue 3 + Vue Router + Pinia + Axios + Element Plus。Vue 3 的组合式 API 写业务逻辑比选项式舒服很多,代码复用性也强。Element Plus 直接用现成的表格、表单、对话框组件,能省掉大量写 UI 的时间,对于不是专业前端开发的同学来说,这是最稳妥的选择。
前端工程化上有几个细节值得学习:
- 路由懒加载:页面组件用动态 import 引入,首屏只加载必要的 JS,避免白屏时间太长。
- Axios 统一封装:baseURL、请求拦截器(带上 token)、响应拦截器(统一处理后端返回结构、错误提示)。这样所有页面里的接口调用代码都很简洁,分工明确。
- 状态管理拆分:用户信息、token 这种全局状态放到 Pinia store 里,组件里用
computed获取,不通过 props 层层传值。
Vue 3 的初学者常见问题是把页面组件写得又长又乱,所有逻辑都塞在一个<script setup>里。我建议哪怕功能简单,也养成把接口调用抽到src/api目录的习惯,按模块建文件,页面组件里只负责数据和 UI 的绑定。
2.3 数据库设计:核心表结构与关联关系
数据库是这个项目的重中之重。这套系统的核心表包括:用户表(含学生、企业两类,用角色字段区分)、企业信息表、职位表、简历表、投递记录表、收藏表、公告表。表结构设计上遵循第三范式,尽量减少冗余字段。
核心表的关系我梳理一下:
- 用户表 user:id、username、password、role、phone、email、avatar。role 区分 STUDENT 和 COMPANY 两种角色,管理员单独在 admin 表,或者用 role = ADMIN 区分,具体实现里看需求。
- 企业表 company:user_id 外键关联用户表,加上公司名称、行业、规模、简介、地址、营业执照等。
- 职位表 position:company_id 外键关联企业,加上职位名称、类型(全职/实习)、薪资范围、学历要求、工作城市、职位描述、状态(待审核/已发布/已下架)。
- 简历表 resume:user_id 关联学生,基本信息和技能特长、教育经历、项目经历等字段。
- 投递表 delivery:resume_id + position_id + user_id 关联三张核心表,状态字段 status 从 0 到 4 或 5,表示待筛选/笔试/面试/录用/淘汰等。
- 收藏表 favorite:user_id + position_id 联合唯一,防止重复收藏。
这里面最有价值的设计是投递记录的状态字段。它不只是存一个数字,而是在代码里定义了状态枚举,每次状态变更都有 SET 语句和前端联动展示。这样设计的好处是,整个招聘流程的状态机清晰可控,不会出现投递记录“凭空消失”或“状态跳变”的 bug。
我翻了下建表脚本,注意到一个细节:职位表和简历表都预留了扩展字段,比如职位表有“招聘人数”、简历表有“自我评价”。从产品角度看,这些字段做校招场景是够用的,但如果你后续想扩展“在线笔试”“视频面试”等功能,就需要新建关联表而不是简单加字段,这一点做二次开发时要提前想清楚。
数据库脚本里还准备了初始数据,包括几个演示账号和十几条测试职位。这个细节对验收很重要,因为答辩或演示的时候可以直接登录账号演示,不用现场一步步注册、发布。这也是我前面说的“交付物友好度”的一部分。
3. 后端核心接口实现详解
3.1 登录认证与权限控制:JWT + 拦截器的完整过程
后端的认证逻辑拆开来看很清晰:用户提交用户名密码,后端用 MD5 加盐后的密码比对数据库,匹配成功就用 JWT 生成 token 返回前端。前端把 token 存到 localStorage 或 Pinia,后续每次请求在 Axios 拦截器里加Authorization: Bearer <token>。
后端写了一个拦截器,在 WebMvcConfigurer 里注册,排除登录、注册、获取职位列表等公开接口,其余接口全部走 token 校验。token 校验通过后,从 token 里解析出 userId 和 role,放入 ThreadLocal 或 request attribute,后面的业务代码用到当前登录用户时直接从上下文里取。
权限控制上,我在每个 Controller 的方法上加了自定义注解@RequireRole({"COMPANY"}),然后在拦截器里做角色匹配。比如发布职位接口只允许企业角色访问,学生访问就返回 403。
这个机制的实际链路大体是:
- 前端调用登录接口
/api/user/login,后端校验通过后返回{ token, userInfo }。 - 前端存储 token,路由跳转到对应首页。
- 用户点击“发布职位”,Axios 携带 token 请求
/api/position/add。 - 拦截器先校验 token 是否有效,再检查当前角色是否为 COMPANY。
- 校验通过,Controller 层拿到参数开始业务处理;不通过,直接返回“未登录”或“无权限”。
这里有一个常见的坑:JWT token 里如果没有存 role 字段,那在拦截器里做角色判断时又得查一次数据库,性能差不说,逻辑也绕。所以生成 token 时要把 user_id、role 一起放进 payload。还有 token 过期时间建议设 24 小时,太短的话演示中途要重新登录,很影响体验。
3.2 职位发布与分页检索:从 QueryWrapper 到前端表格
职位发布的逻辑不复杂,核心是状态默认置为“待审核”。企业发布职位后,管理员在后台审核通过,职位才会在学生端可见。这一步对真实校园招聘场景很有意义:防止企业乱发垃圾职位污染平台环境。
分页检索是学生端最核心的接口之一,支持按关键词、城市、职位类型、学历要求做组合筛选。代码里用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼接条件,配合分页插件,返回给前端{ records, total, current, size }。
真实实现中,筛选条件有两种组织方式:
- 后端拼接:前端把筛选条件作为请求参数传给后端,后端在 Service 层用
StringUtils.isNotBlank判空后逐个拼接查询条件。优点是前端写法简单,缺点是后端代码里可能出现一堆 if 判空,但对这个体量的项目完全可控。 - 前端过滤:一次性拉取所有已发布职位,前端用 computed 做条件过滤。优点是后端接口简单,但职位数据到一定量级后性能会明显下降。
这套系统用的是第一种方式,我更推荐这种方式,因为分页天然需要在数据库层做,前端过滤和分页会互相冲突。
我在实际测试中发现职位搜索接口做了两个优化点:一是关键词搜索用了 MySQL 的 LIKE 但不是%关键词%前匹配,而是前后通配,方便模糊搜索;二是对“薪资范围”做了区间映射,前端传的是薪资档位 1/2/3/4,后端翻译成对应的薪资区间再做查询。这种“前端少传参,后端统一翻译”的做法在答辩时很加分。
3.3 投递状态流的设计与实现
投递状态是整套系统最有含金量的设计。我建议从数据层面理解它:投递记录表里有一个 status 字段,枚举值大致如下:
| 状态值 | 状态名称 | 说明 |
|---|---|---|
| 0 | 待筛选 | 学生已投递,企业未处理 |
| 1 | 已通过初筛 | 企业标记初筛通过 |
| 2 | 笔试中 | 进入笔试环节 |
| 3 | 面试中 | 进入面试环节 |
| 4 | 已录用 | 发放 offer |
| 5 | 未通过 | 淘汰 |
| 6 | 已取消 | 学生主动取消投递 |
后端在外层做了一层状态机校验,不允许任意跳转。比如企业不能直接把待筛选改成已录用,必须经过初筛和笔试。这个限制通过 Service 层的 if 判断实现,虽然不如状态机框架那么优雅,但逻辑清晰、便于调试,适合项目当前体量。
状态变更接口的实际行为是:企业端传入deliveryId + targetStatus,后端拿到当前状态,检查是否允许从当前状态跳转到目标状态,允许则更新,不允许则抛业务异常返回提示信息。同时每次状态变更可以往日志表里写一条记录,方便后续追踪。
这个设计还有一个隐藏的好处:前端能根据状态值控制按钮的显示。比如投递状态为“待筛选”时,学生端显示“取消投递”按钮,企业端显示“通过初筛”和“不合适”按钮;状态变为笔试后,这些按钮就隐藏,换成对应的下一步操作。这样后端状态机一旦定义明确,前端按钮的显隐逻辑就非常容易编写。
4. 前端页面落地与前后端联调的关键细节
4.1 路由设计:三个端口的页面如何组织
前端路由按端口分模块组织,而不是全部平铺在路由表里。具体做法是:
/student/login、/student/register、/student/dashboard、/student/jobs、/student/deliveries、/student/resume/company/login、/company/register、/company/dashboard、/company/positions、/company/candidates/admin/login、/admin/audit、/admin/positions、/admin/users、/admin/stats
路由守卫在每次跳转前检查 token 和角色,如果角色和路由前缀不匹配就跳转到对应端口的登录页。比如学生用户手动输入/company/positions,会被守卫拦下并重定向到/student/dashboard。
这种前缀式路由组织的好处很明显:代码结构跟端口一一对应,接手项目的人打开路由文件一眼就能看懂哪个模块属于哪个角色。坏处是如果路由数量很多,每个模块下的二级路由会有点重复,但对课程设计级别完全是合理取舍。
页面骨架用的是布局组件嵌套子路由的方式:外层布局包含侧边导航和顶栏,子路由通过<router-view>渲染到内容区。这样左侧菜单只写一次,不用在每个页面重复处理。
4.2 Axios 封装与统一响应结构
前端把 Axios 封装成了一个模块,所有接口请求统一走这个封装。这里有几个设计点值得参考:
- 请求拦截器里自动加上
Authorization头部,从 localStorage 或 Pinia 里取 token。 - 响应拦截器里统一判断 HTTP 状态码和后端返回的业务 code。后端约定返回结构是
{ code, message, data },code 200 表示成功,401 表示未登录或 token 过期,403 表示无权限,500 表示服务器异常。 - 遇到 401 时自动清除本地 token 并跳转到登录页,避免用户停留在失效页面反复操作。
- 每个模块的接口单独放在
src/api目录下的文件里,比如job.js、resume.js、delivery.js,页面组件里引入函数后直接调用。
对新手来说,最容易忽略的是“统一错误提示”。如果没有在响应拦截器里做一层消息提示,那每个页面调用接口时都要 try-catch 并自己写提示逻辑,代码冗余程度会非常高。统一封装之后,Controller 里抛出的业务异常信息能直接在页面上通过 ElMessage 显示,体验会好很多。
4.3 典型交互场景:投递与状态流转的前端操作
投递操作的前端逻辑是这样的:学生在职位详情页点击“投递简历”,前端先检查是否有简历,如果还没有简历就弹窗引导先维护简历;有简历则调投递接口,后端校验该用户对该职位是否已投递过,未投递则生成投递记录并返回成功。
“已投递过”这个校验必须有,不然学生反复点击按钮就会生成大量重复投递记录。后端在 delivery 表建了联合唯一索引(user_id + position_id),数据库层面也兜了一层底。
企业端处理投递的交互是典型的“表格 + 状态切换”:候选列表中每行显示学生姓名、职位、投递时间、当前状态,右侧是操作按钮。点击按钮后,弹出确认对话框或直接调用接口更新状态。列表数据用分页组件控制,每页默认 10 条。
在联调阶段我遇到一个常见交互问题:企业修改职位状态后,学生端看不到变化,原因是前端本地缓存了职位列表。解决办法很简单:投递状态流转和职位上下架这类操作成功后,主动调用接口刷新列表,而不是依赖本地缓存。缓存一致性在前后端联调里是个很容易被忽略的点。
5. 部署、测试与常见问题排查
5.1 项目启动全流程与运行环境配置
跑通这套系统的环境要求不高:JDK 1.8+、Maven 3.6+、Node.js 14+、MySQL 5.7+。后端是标准的 Spring Boot 工程,配置文件在application.yml里,需要修改的数据源配置主要是 URL、用户名、密码。前端是 Vue 工程,需要确认vite.config.js里的代理配置和后端端口一致。
启动顺序是:先导入数据库脚本,再启动后端,最后启动前端。后端的启动直接运行 main 方法,或者mvn spring-boot:run。前端开发模式是npm install安装依赖,然后npm run dev。
开发模式下的联调建议不要让前端直接请求后端地址,而是配一个 Vite 代理:/api开头的请求全部转发到http://localhost:8080。这样能避免开发阶段的跨域问题,上线后再通过 Nginx 反向代理解决。
5.2 新手最容易踩的坑:跨域、端口冲突、依赖版本
跨域:浏览器直接请求http://localhost:8080和前端端口不一致一定会跨域。两种解法,要么后端配 CORS(跨域资源共享)过滤器,要么前端配代理。开发阶段推荐代理,因为不用改后端代码,线上部署时再统一用 Nginx 处理。
端口冲突:Spring Boot 默认 8080,如果本机端口被占,启动会报错。要么在配置里改端口,要么用lsof -i:8080(macOS/Linux)或netstat -ano | findstr 8080(Windows)找到占用进程并处理。
依赖版本:Spring Boot 2.7 和 JDK 17 会有兼容性问题,建议直接用 JDK 1.8 或 11。前端 Node 版本太老会导致 Vite 启动报错,建议 Node 14 以上。这套系统在 JDK 1.8 + Node 14 的环境下最稳。
5.3 数据库层面的常见问题与解决方案
数据库问题往往比代码问题更隐蔽,列出三个典型的:
- 时区问题:MySQL 5.7 不带默认时区,Spring Boot 连接时如果 URL 里没有
serverTimezone,会报 CST 相关异常。解决方案是在 JDBC URL 里加serverTimezone=Asia/Shanghai。 - 中文乱码:建库时没有指定 utf8mb4 字符集,职位描述里的中文就会乱码。正确姿势是建库时用
CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。 - 日期字段精度:投递时间用 TIMESTAMP 或 DATETIME 存储,查询时最好在 Java 侧做格式化,或者使用 Jackson 的日期格式化注解,避免返回给前端的时间字符串带多余时区后缀。
初始化数据时还容易忽略 ID 主键问题。如果表结构用了自增主键,导入 SQL 时要把 AUTO_INCREMENT 值带上,不然演示环境里插入新数据时主键会冲突。这个细节我在自己有一次导入别人项目时踩过,后来养成了导入完先跑一条 INSERT 测试的习惯。
5.4 部署到服务器的简易流程
把项目部署到 Linux 服务器的核心步骤不难,一个 Vite 前端项目构建后生成 dist 目录,后端打成 jar 包,用java -jar启动即可。Nginx 负责托管静态文件并把/api请求反向代理到后端服务。
一个建议是后端不要用裸java -jar启动,日志很难看。配合nohup java -jar xxx.jar > app.log 2>&1 &把日志输出到文件,方便排查线上问题。有条件可以写一个简单的 shell 脚本,一键重启服务,这对答辩演示切换环境很有帮助。
我在测试部署时发现前端路由刷新后会 404,原因是用 history 模式路由时,Nginx 没有配置try_files。解决方案是在 Nginx 的 location 里加:
location / { try_files $uri $uri/ /index.html; }这样刷新任意前端路由都会回退到首页入口文件,由前端路由接管展示。很多第一次部署的同学都会卡在这个问题上,记住这句配置能省一晚上时间。
6. 如何把这份项目变成自己的作品
6.1 二次开发的正确顺序与建议
拿到源码后不要急着改功能,先完整跑一遍。我的建议顺序是:看数据库脚本理解表结构,再启动后端用 Postman 或 Apifox 调用几个核心接口,最后启动前端页面走走业务流程。三端都通了以后,再去看代码里具体某个功能怎么实现。
二次开发时优先从“能演示的新功能”切入。比如给职位列表加一个“热门职位”排序,后端在查询接口里加一个按照投递量排序的逻辑;或者给学生端加一个“职位收藏”的列表页,用到现有的收藏表。这类改动既不影响核心逻辑,又有明显的功能增量,答辩时说“我新增了 XX 功能”比“我改了 XX 字段”有说服力得多。
6.2 可扩展方向:从课程设计到真实产品
如果时间充裕,有几个扩展方向值得考虑:
- 招聘数据可视化:管理员端增加一个柱状图/折线图页面,展示投递量、职位发布量趋势。可以用 ECharts,前端画图,后端提供聚合统计接口。
- 简历文件上传:目前简历数据主要是结构化表单,可以扩展为支持 PDF 上传,用 Spring Boot 的文件上传接口 + 本地存储或对象存储。
- 消息通知:投递状态变更时给学生发站内消息,数据模型上增加一个消息通知表,前端做一个通知中心。
- AI 职位推荐:根据学生简历里的技能标签和职位描述做简单匹配推荐,可以用字符串相似度算法,也可以接大模型接口做语义匹配。
这些方向里面,我会优先推荐数据可视化,理由很简单:ECharts 图表的展示效果直观,答辩现场看的人都能看懂,而且前后端都能体现工作量。
6.3 写简历和面试时怎么讲这个项目
最后聊一点实际的:一个招聘系统项目写在简历上,面试官最可能问的三个问题是:权限怎么做、状态流转如何控制、数据库为什么这么设计。如果你照着这篇文章把 JWT 拦截器、状态机校验、表结构设计几条线捋清楚了,这三个问题都能答出“为什么”而不只是“怎么做”。
面试官其实很怕听到“这个功能我用 XX 框架实现”这种回答,他们想听到的是“我遇到了什么问题、为什么选择这种方案、有没有考虑过其他方案”。比如“MySQL 分页在数据量大时会变慢,所以考试时我用了一个假设索引优化的方案”,这种有取舍的表达比背一段框架文档的印象好得多。
说白了,做课设和毕设不只是“交一份作业”,它是一次完整的全栈项目训练机会。把这份源码吃透,你收获的将不只是一个能跑的项目,而是一套“拿到任何业务需求都能拆解成模块、表、接口”的思考框架。我在实际跑这个项目的时候,最深的体会是:一个系统的复杂度不在于代码行数,而在于把业务流程真正理顺并落成结构和接口的能力。你能把招聘这种人人熟悉的场景讲清楚,未来换个电商、问答、社交场景,换汤不换药,底子已经在了。