毕业季一到最忙的往往不是正在找工作的学生而是负责统计就业数据的辅导员和教务老师。手里一堆Excel表格今天这个学生签了三方明天那个学生考上了编制信息散在各个微信群里月底要报表时全是手动对账。我也是在那段时间接触到了这套毕业就业信息管理系统基于SpringBoot做后端、Vue做前端、MySQL存数据三个角色分别对应学生、企业和学校管理员把“投简历—发岗位—审企业—统计就业”这条业务链完整串了起来。源码标注了“可直接运行”我实测下来确实改改数据库账号密码就能启动。这篇文章适合几类人准备做管理类毕业设计但还没定题目的在校生需要快速交付一个小型信息系统的开发者以及想把就业管理流程从Excel里解放出来的学校技术团队。我会从选型逻辑、数据库设计、后端接口、前端结构到本地部署排错完整拆一遍尽可能把每一步为什么要这么做的原因也说清楚方便你直接参考或二次改造。1. 为什么SpringBootVueMySQL这套组合能成为“默认答案”1.1 前后端分离的成熟度已经足够高现在再去做那种用模板引擎渲染页面的单体Java Web项目除非学校有硬性要求否则真没必要。前后端分离的开发模式下后端只负责提供数据接口前端只负责渲染页面和交互两边通过JSON通信团队协作和后期扩展都舒服很多。SpringBoot在后端圈子里几乎是事实标准起步快、生态全一个mvn spring-boot:run就能把服务跑起来Vue在前端圈子的上手曲线比React更平缓中文资料也更多MySQL则是最常见的开源关系型数据库部署简单学校和中小企业用得最多。这套组合不是“最先进”的但它是“最稳妥”的。一个非一线团队或者个人开发者用这套组合去搭中小型管理系统网上能查到的解决方案是最多的踩过的坑也都被前人填得差不多了。所谓可直接运行本质上是依赖了这套组合的低环境门槛。1.2 三类角色把就业业务闭环串了起来这个系统里有三个登录入口对应三种身份登录后看到的功能完全不同角色核心功能学生维护个人简历、浏览招聘岗位、投递岗位、查看企业反馈、登记就业信息企业注册与入驻申请、发布招聘岗位、查看收到的学生简历、反馈录用结果管理员审核企业入驻、管理学生与企业账号、发布通知公告、查看就业统计报表业务闭环可以这样理解企业先注册提交入驻申请管理员审核通过后企业才能发布岗位学生在岗位列表里筛选合适的岗位并投递简历企业查看投递记录后给出“邀约面试”或“不匹配”的反馈学生被录用后在系统里登记就业单位信息管理员端汇总所有登记数据按学院和专业统计就业率。这个流程覆盖了“招聘前—招聘中—招聘后”的完整周期比单纯做一个“岗位信息展示系统”要扎实得多。1.3 这套系统的可复用边界在哪里不只是毕业设计能用。只要把“学生”换成“求职者”、“岗位”换成“商品”、“就业登记”换成“订单登记”系统骨架几乎可以迁移到其他信息管理类项目上。权限模型、统一返回、分页接口、文件上传这些代码块在任何一个中小型前后端分离项目里都是一样的。所以我不太建议把这类系统当成一次性的作业来对待更应该把它当成一套可复用的管理系统脚手架。2. 数据库设计是整套系统能不能跑通的底层关键我接手这类项目的习惯是先看数据库脚本。表结构定得不合理后面写多少代码都别扭。这套系统里核心的业务表大概有九张我逐个说清楚它们的作用和设计时的取舍。2.1 九张核心表的功能定位表名用途最关键的两个字段sys_user统一登录账户表username、rolestudent_info学生基础与扩展信息user_id、student_nocompany_info企业信息与入驻状态user_id、statusjob_info招聘岗位表company_id、statusresume_info学生简历内容表student_id、skillsdelivery_record学生投递岗位记录表student_id、job_idemployment_info就业信息登记表student_id、employment_statusnotice_info通知公告表title、publish_timecollege_info学院/专业基础数据表college_namesys_user 单独拆出来的原因很直接三种角色的登录行为是统一的都需要用户名、密码、状态和角色标识如果各自建表会让登录逻辑没办法复用。业务扩展信息则放各自的资料表里通过 user_id 去关联。这个“账户表资料表”的拆分模型在很多大型系统里也是同样的思路。2.2 用户表与资料表的一对一关系怎么维护sys_user 里的主键 id 会被 student_info.user_id 和 company_info.user_id 引用两张资料表与用户表都是一对一关系。我见过有人图省事直接在用户表里拼上学号、姓名、公司名称、统一信用代码这些字段结果就是角色一多表变得不伦不类。宁可多建两张表也别把不同角色的字段堆在一个账户表里。登录后的用户信息可以从 sys_user 查出角色再按角色去查询对应的扩展表得到完整的用户信息。这个步骤在前端叫“获取用户详情”在后端往往就是一个简单的条件查询加一个业务查询逻辑清晰也方便以后扩展。2.3 就业信息表的设计决定了统计报表好不好写就业信息表是整个系统里最需要用心设计的一张表。它不能只记录一个“已就业/未就业”字段否则根本支撑不了后面的统计报表。我建议至少要包含这些字段学生ID、就业单位名称、岗位名称、薪资范围、入职时间、工作城市、就业类型签就业协议、升学、应征入伍、自主创业等、就业凭证附件路径、审核状态。为什么要留就业类型因为高校的就业统计里“升学”和“签就业协议”会被计入不同口径的毕业去向没有这个字段报表只能笼统显示一个数字。为什么要留附件路径因为后续管理员需要核验就业证明材料没有凭证这数据对上报表就是无源之水。一张表的设计直接决定了两个星期后写统计接口时会不会想砸键盘。2.4 建表脚本里值得注意的细节我给 job_info 和 employment_info 这两张表的建表脚本做个示范实际全套SQL比这个多但关键写法都在里面了CREATE TABLE job_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 岗位ID, company_id BIGINT NOT NULL COMMENT 所属企业ID, title VARCHAR(100) NOT NULL COMMENT 岗位名称, job_type VARCHAR(50) COMMENT 岗位类型, salary_min DECIMAL(10,2) COMMENT 最低薪资, salary_max DECIMAL(10,2) COMMENT 最高薪资, work_city VARCHAR(50) COMMENT 工作城市, degree_requirement VARCHAR(20) COMMENT 学历要求, recruit_num INT DEFAULT 1 COMMENT 招聘人数, job_desc TEXT COMMENT 岗位描述, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, deadline DATETIME COMMENT 招聘截止时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT招聘岗位表; CREATE TABLE employment_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID, company_name VARCHAR(200) COMMENT 就业单位名称, job_title VARCHAR(100) COMMENT 岗位名称, salary_range VARCHAR(50) COMMENT 薪资范围, entry_date DATE COMMENT 入职时间, work_city VARCHAR(50) COMMENT 工作城市, employment_type VARCHAR(20) COMMENT 就业类型, certificate_url VARCHAR(255) COMMENT 就业凭证路径, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT就业信息登记表;两个细节值得说一是所有表都用 InnoDB 引擎和 utf8mb4 字符集后者能完整支持中文和特殊符号老项目的 utf8 在遇到生僻字时很容易出问题二是字段带 COMMENT这个习惯能帮你省掉大量翻代码对字段的时间尤其是三个月后再回来看这个项目的时候。3. SpringBoot后端工程结构、登录鉴权与核心接口3.1 后端工程结构先规划好一套能直接运行的后端工程最怕的就是所有代码堆在 controller 里。我建议按下面这种分层结构组织com.example.employment ├── controller/ # 接口层 │ ├── AuthController.java │ ├── StudentController.java │ ├── CompanyController.java │ └── AdminController.java ├── service/ # 业务逻辑层 │ ├── impl/ ├── mapper/ # MyBatis-Plus的Mapper接口 ├── entity/ # 数据库实体 ├── dto/ # 请求参数对象 ├── vo/ # 返回视图对象 ├── common/ # 通用返回结果、异常类、常量 ├── config/ # 跨域配置、拦截器配置 ├── utils/ # JWT工具、密码加密工具 └── GraduateEmploymentApplication.javacontroller 只负责接收参数和返回结果service 负责写业务逻辑mapper 负责和数据库打交道。这样的代码在排错时会舒服很多。3.2 登录接口与JWT鉴权的具体实现思路登录接口是第一个要写的接口也是后面所有接口的地基。它的逻辑不复杂后端接收到用户名和密码后先拿用户名去 sys_user 表查用户查不到直接返回“用户名或密码错误”查到了就用 BCrypt 密码校验器去比对加密后的密码一致则生成一个 JWT Token 返回给前端不一致同样返回错误。密码在数据库里一定不能存明文。我在系统里只引入了 spring-security-crypto 这个轻量依赖来使用 BCryptPasswordEncoder并没有把完整的 Spring Security 引进来因为完整的权限框架对这个项目来说过于重了反而会带来很多默认拦截规则增加“直接运行”的难度。这就是一个很典型的取舍工具类可以单拿框架不一定要全套上。JWT生成后后续所有请求在请求头里携带 Authorization 字段后端用一个拦截器统一去解析和校验。拦截器里通过后把用户ID和角色放进请求属性里接口里随时可以取出来用Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }这个拦截器不用区分角色因为所有业务接口都需要登录。角色校验则留给更细粒度的做法比如在需要管理员权限的接口上增加一个自定义注解或者在接口方法里手动判断当前用户的角色。系统只有三种角色的时候手动判断就够用了不需要引入复杂的权限框架。3.3 统一返回格式与全局异常处理不能省接口返回格式如果不统一前端处理起来会非常痛苦。我一般定义一个 Result 类结构固定为 code、msg、data 三个字段成功时 code 为 200失败时为业务错误码401 统一表示未登录。配合统一返回类的还有全局异常处理器用 RestControllerAdvice 把所有业务异常接住转换成统一的错误结构。不然系统一旦报个空指针前端拿到一堆看不懂的堆栈信息体验立刻回到石器时代。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg 操作成功; result.data data; return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.code 500; result.msg msg; return result; } }3.4 岗位发布与投递的核心业务逻辑岗位发布的主体是企业。企业账号登录后通过 company_id 关联企业信息新增岗位时把 company_id 一并写入 job_info。岗位发布后默认是上架状态企业也可以手动下架但不删除历史数据因为投递记录表里关联着岗位ID删除会造成大量孤儿数据报表也没法统计历史招聘情况。学生投递岗位时delivery_record 表会插入一条投递记录。这里要注意幂等控制同一个学生对同一个岗位不能重复投递。在代码里就是提交前先查一下 delivery_record 里有没有 student_id job_id 的记录有就提示“请勿重复投递”没有才插入。这个逻辑虽然简单但少了它学生连点几次提交按钮企业端就会看到好几条一模一样的简历。投递记录的 status 字段可以由企业操作流转待查看 → 已查看 → 已邀约/已录用/已拒绝。前端根据状态渲染不同颜色的标签学生就能在自己投递列表里看到进展。这套流程本质上就是一个状态机建模不需要额外引工作流引擎。3.5 就业率统计接口的SQL写法这套系统的管理员端最核心的页面就是就业数据统计。如果之前就业信息表的字段设计得合理到这里反而最简单无非就是分组统计加条件聚合SELECT s.college AS 学院, COUNT(*) AS 学生总数, SUM(CASE WHEN e.employment_type IS NOT NULL AND e.employment_type ! 未就业 THEN 1 ELSE 0 END) AS 已就业人数, ROUND( SUM(CASE WHEN e.employment_type IS NOT NULL AND e.employment_type ! 未就业 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2 ) AS 就业率 FROM student_info s LEFT JOIN employment_info e ON s.id e.student_id GROUP BY s.college;这里的 LEFT JOIN 很关键必须用左连接否则没有填写就业信息的学生会被直接过滤掉统计出来的总数就会比实际学生数少。很多第一次写这个统计的同学都栽在这里。如果还需要按专业细分GROUP BY 后面再加上 s.major 字段即可。4. Vue前端页面结构、权限路由与接口请求封装4.1 前端工程结构长什么样Vue 工程我建议用 Vite 创建目录规划如下src/ ├── api/ # 每个模块的接口方法 │ ├── auth.js │ ├── student.js │ ├── company.js │ └── admin.js ├── assets/ ├── components/ # 通用组件 ├── layout/ # 后台布局侧边栏顶栏主内容区 ├── router/ │ └── index.js # 路由表 ├── store/ # Pinia状态管理 ├── utils/ │ ├── request.js # Axios实例封装 │ └── auth.js # Token存取 └── views/ ├── login/ ├── student/ ├── company/ └── admin/layout 是整个后台界面的外壳登录成功后的所有页面都渲染在它里面。侧边栏菜单根据角色动态生成顶栏放用户信息和退出按钮。这种骨架式写法避免了每个页面都重复写侧边栏。4.2 登录后的Token存储与路由守卫Token 在前端一般存 localStorage配合 Pinia 存用户基本信息。页面跳转通过路由守卫控制只有拿到 Token 才能进入系统内页面否则全部重定向到 /login。路由守卫代码大致是router.beforeEach((to, from, next) { const token getToken() if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })角色控制放在菜单渲染层面。我这里的做法是侧边栏菜单数组里每一项标注允许显示的角色比如“岗位管理”允许 student 和 company而“就业统计”只允许 admin。登录后根据当前用户角色过滤菜单数组这样接口层面已经做了鉴权前端再控制一下菜单展示体验就完整了。4.3 Axios实例封装要覆盖三个关键点请求封装是前端工程里最值得花时间写好的部分。我封装的 request.js 主要做三件事请求拦截器里给每个请求头自动加上 Authorization响应拦截器里统一拿到后端 Result 结构code 为 200 就直接返回 data否则提示错误遇到 401 状态码时清除本地 Token 并跳转登录页。service.interceptors.response.use( (response) { const res response.data if (res.code 200) { return res } if (res.code 401) { removeToken() router.push(/login) return Promise.reject(new Error(登录已过期)) } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg || 请求失败)) }, (error) { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )有了这一层封装页面里调用接口只需要关注业务代码不需要到处处理错误弹窗和 Token 过期问题。这也是“可直接运行”这个承诺能落到实处的关键。4.4 三端核心页面的拆解思路学生端最重要的页面是岗位列表和投递记录。岗位列表支持按岗位名称、城市、薪资范围筛选列表分页展示每条记录右侧有“投递”按钮。投递成功后在投递记录列表里能看到状态标签从“待查看”一步步变成“已邀约”或“已录用”。企业端最核心的页面是岗位管理页和应聘管理页。岗位管理页就是一套表格的增删改查列表展示岗位上下架状态应聘管理页展示该企业下所有岗位收到的投递记录点击一条记录可以看到学生的简历详情并做出反馈操作。管理员端最核心的页面是企业审核页和就业统计页。企业审核页需要展示营业执照等附件信息然后做通过或驳回操作就业统计页用 ECharts 图表展示各学院就业率饼图看去向分布柱状图看各学院人数对比。图表数据从统计接口一次拿回来前端按格式塞给图表组件即可。4.5 一个容易忽略的前端细节表单校验与字段一致性这个系统里学生要填简历、企业要填岗位信息、管理员要填公告内容涉及的表单很多。前端组件库自带的表单校验规则一定要用起来比如岗位名称必填、薪资必须是数字、企业信用代码长度固定。校验规则要和后端字段校验保持一致。还有一个实际操作中的坑前端表单里的字段名必须和后端接收的 DTO 字段名完全一致否则就会出现前端填了一堆数据后端收到了全是 null 的情况。这个问题排查起来很费劲因为前后端日志都不报错。建议定义接口时先用 Swagger 或手写的接口文档把字段确定下来前端照着写。5. 从零到一运行起来配置步骤和启动阶段的常见坑5.1 环境版本选择这套系统对运行环境的要求不高但如果版本太新也可能踩坑我列一下我试过的稳定组合组件推荐版本备注JDK8 或 118最稳11也没问题Maven3.6用IDEA内置的更省事MySQL5.7 或 8.08.0需要配置时区Node.js16 或 18Vite对新版本要求较高npm随Node附带建议保留 lock 文件5.2 后端启动前必须改的几个配置拿到源码后第一步是创建数据库执行项目里的 sql 脚本把表和初始数据导入。然后打开 application.yml修改数据库地址、用户名、密码三个值server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/graduate_employment?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 20MB max-request-size: 50MB这里有两个高频报错点MySQL 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver不是老旧的 com.mysql.jdbc.Driver数据库连接 URL 里的 serverTimezone 不写启动后会报时区错误。新一点的 MySQL 驱动版本如果通信链路不安全也可能报 SSL 警告URL 里加上 useSSLfalse 就行。5.3 前端启动和接口代理配置前端依赖安装用 npm install如果遇到 ERESOLVE 依赖树冲突可以用 npm install --legacy-peer-deps 绕过。启动命令是 npm run dev默认端口是 5173。前端开发服务器和后端接口不在同一个端口跨域问题必须处理。我的做法是利用 Vite 的代理能力把前端发起的 /api 请求转发到后端 8080 端口。在前端项目根目录的 vite.config.js 里配置如下export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })后端所有接口统一以 /api 开头前端请求路径写 /api/xxx经过代理转发到后端后实际会到达 http://localhost:8080/api/xxx。这个方案开发阶段很好用不需要后端额外写跨域配置。如果要部署到生产环境再用 Nginx 做反向代理思路一样。5.4 启动顺序和验证流程后端在 IDEA 里直接运行启动类即可看到日志打印出 Tomcat started 就说明成功了。前端在终端执行 npm run dev浏览器访问 http://localhost:5173能看到登录页就说明前端起来了。用初始管理员账号登录进入后台随便点几个页面再测试一下学生账号投递岗位走一遍完整流程确认数据库里新增了记录整套系统就是真正跑通了。5.5 我把启动阶段的高频报错整理成了排查清单报错现象可能原因解决办法8080端口被占用本机有其他服务占用改 server.port或杀掉占用进程Access denied for user数据库账号密码错误检查 yml 里的用户名密码Unknown database数据库没有创建先执行建库语句The server time zone valueMySQL时区未配置URL加 serverTimezoneAsia/ShanghaiFailed to start bean documents文件上传目录不存在检查配置里的上传目录路径并手动创建npm ERESOLVE依赖版本冲突npm install --legacy-peer-deps前端请求404后端没有/api前缀或代理路径配错检查后端Controller RequestMapping和vite代理登录报“用户名或密码错误”初始账号被改过或加密方式不匹配直接查sys_user表确认数据这个表是我实际排查过多次后整理出来的基本覆盖了“源码能直接运行”这句话背后最容易翻车的八个环节。6. 真实体会这套系统的短板和几个值得扩展的方向6.1 目前这套系统离生产级还有多少距离我必须说实话这类系统标着“可直接运行”不等于可以直接上生产。安全方面目前只能算基础版JWT 没有刷新机制过期后用户只能重新登录没有验证码机制容易被脚本暴力尝试登录密码虽然加密了但缺少登录失败次数限制。业务方面简历目前更多是文本填写加附件上传没有实现在线预览通知公告功能有但没有站内信提醒机制学生登录后才能看到。如果只是课程设计或团队内部使用这些都不是问题。如果要推向生产环境至少得补上操作日志、验证码、密码找回这三个模块。6.2 我觉得最值得做的三个扩展第一个扩展方向是 Excel 导入导出。就业数据统计完管理员有极大可能要把报表导出成 Excel 交给上级。后端用 EasyExcel 工具类前端一个下载按钮工作量不大但对系统的实用价值提升非常明显。第二个扩展方向是消息通知机制。当企业的反馈状态发生变化时给学生发一个站内消息提醒当学生投递简历时给企业推送一条新消息。这类功能不需要引入消息队列数据库里加一张消息表前端轮询或者用 WebSocket 都能实现。第三个扩展方向是统计维度的细化。当前统计到学院级别就够了但真实场景里可能还要按专业、按班级、按性别、按生源地等维度钻取。数据表设计时已经预留了这些字段扩展也就只是加接口和加图表的问题。6.3 最后给接手的同学几句实务建议如果你准备拿这套系统作为毕业设计或者项目实战练手我建议先原样跑通再去做个性化修改。先跑通的意义在于你能从前到后理解整个数据流转链路后面改功能时才知道动了前端一个按钮后端哪个接口、哪张表会跟着受影响。改功能时最忌讳只改前端不改数据库。比如岗位列表要加一个“工作性质”字段前端表单加了输入框后端实体没加字段数据库表没加列结果就是前端提交报错排查半天。正确顺序是先改数据库表再改后端实体和接口最后改前端页面。保留原始SQL脚本也非常重要。你要是依赖 MyBatis-Plus 的自动建表功能改了表结构后面想回退或者换一台电脑部署没有SQL脚本就只能凭记忆重建表非常被动。每次调整表结构后记得把最新的建表语句导出来更新到项目的 sql 目录下。这套系统整体框架是稳的把它跑熟、吃透再按自己的需求去延伸你收获的绝对不止是“一个毕业设计能通过”这么简单。
阅读完成 · 觉得有帮助?