我手里这套以 SpringBoot 做后端、Vue 做前端、MySQL 做数据存储的 Web 就业管理系统源码,最初是从开源仓库下载下来的。当时页面截图显示包含了学生信息、就业审核、统计报表,核心功能看起来挺全,项目文档也写了“可直接运行”。但做这一行的人都懂,“可直接运行”这四个字在真实环境里往往代表一堆环境依赖、版本兼容和配置项需要自己处理。这篇文章我不打算照着开发文档翻译一遍,而是把从下载源码到跑通、再到读完核心业务、最后成功二次部署的整个过程拆开讲。
1. 就业管理系统的业务全貌:它不是简单的增删改查
1.1 真实使用场景
我之前给学校里的就业办做过类似的项目,太清楚这类系统的痛点了。学生毕业季一到,就业信息收集基本靠辅导员发 Excel 表格,学生填完再汇总,格式五花八门,有的把公司全称写成了简称,有的上传的就业证明是手机拍的模糊照片。到了要上报就业数据的时候,老师还要对着表格一个个统计就业率、升学率、自由职业占比,非常容易出错。这个就业管理系统解决的就是三件事。
首先是信息收集的结构化。系统把就业信息拆成固定字段,比如单位全称、统一社会信用代码、岗位类别、月薪范围、就业类型、证明文件,学生只需要按表单填,后续统计口径就统一了。
其次是审核链路的透明化。学生提交就业信息后,辅导员能在线审核,通过或者驳回,驳回时还能写理由。整个过程有时间记录,谁审核的、什么时候审核的、为什么驳回,全部留痕。这个设计特别重要,因为就业信息里面的证明材料学校是要留档的,缺少这一步,后期整理材料会是一场灾难。
最后是统计分析自动化。系统按专业、班级、就业类型做分组统计,页面直接用图表展示,也可以导出 Excel。到了学校要求上报就业率的时候,管理员不需要再手算,直接导出一张表就能交差。
1.2 三种角色的权限边界
从源码里的角色字段能看出,这个项目固定了三种角色,没有做复杂的 RBAC 权限模型,这是很务实的选择。
管理员 admin 负责用户管理、重置密码、数据字典维护和全局数据查看。教师 teacher 负责审核本专业或本班学生的就业信息,查看班级维度和专业维度的统计数据。学生 student 只能维护自己的基本信息和提交就业信息,不能看到其他人的数据。
权限在实现上是两层控制的。前端根据角色的返回结果渲染不同的菜单项,后端在接口上做角色校验,比如学生角色调用审核接口直接返回无权限。我第一次看源码的时候,只关注了前端隐藏菜单,后来做了一个越权测试,发现后端并没有做充分校验,于是我在二次开发里补了基于注解的权限拦截。
1.3 核心模块与业务闭环
这个系统可以拆成六大模块,用表格列一下比较直观。
| 模块 | 核心功能 | 使用角色 |
|---|---|---|
| 系统管理 | 用户管理、角色管理、重置密码 | 管理员 |
| 学生信息管理 | 学生基本信息维护、按班级专业查询 | 管理员、教师 |
| 就业信息管理 | 学生填写就业信息、上传证明、查看进度 | 学生、教师 |
| 审核管理 | 就业信息审核、驳回、审核记录查询 | 教师 |
| 统计分析 | 就业率统计、分维度图表、Excel导出 | 管理员、教师 |
| 公告管理 | 招聘信息、政策通知的发布 | 管理员 |
整个业务闭环是这样的:管理员先导入学生账号,学生首次登录完善个人信息,毕业季时提交就业信息并上传证明材料;教师端出现待审核列表,逐条查看并做出通过或驳回操作;学生端实时看到审核状态,被驳回后按理由修改再提交;管理员在统计页看到整体就业数据,按专业或班级下钻,最后导出数据表上报。
搞清楚业务闭环之后,再回头看代码结构就会清晰很多。就业管理系统归根到底是两个核心对象在流转:学生产生的就业信息,以及信息经过的审核状态。后续所有表设计、接口设计,全都是围绕这两条线展开的。
2. 为什么是 SpringBoot + Vue + MySQL:选型背后的权衡逻辑
2.1 SpringBoot 相比传统 Java 方案的优势
我早期做过不少 SSM 架构的项目,写配置文件的时间比写代码还长。SpringBoot 在这方面确实省事得多,它把所有常用组件的自动配置都封装进了 starter。
对这个项目来说,最实际的好处有三个。第一,内嵌 Tomcat,后端打成 jar 包直接就能跑,不需要单独装一个外置容器再部署 war 包,这对一台云服务器部署多个项目的场景非常友好。第二,Spring Boot 的配置中心化,数据源、文件上传路径全部写在 application.yml 里,换环境只需要改一个文件。第三,生态成熟,MyBatis-Plus 的 starter 一引就能用,分页查询、条件构造器都是现成的,省掉了大量手写 XML 的时间。
2.2 为什么前端选 Vue 而不是 JSP
过去传统 Java 项目用 JSP 渲染页面,Java 和 HTML 混在一起,每次改一个按钮都要重启应用服务器,多人协作时前后端接口要一边写代码一边口头对齐。Vue 项目把前端独立出来了,打包产物是一堆静态文件,可以直接扔到 CDN 或者用 Nginx 托管,后端只负责输出 JSON。
组件化和 Element UI 对管理后台的提效也很明显。比如审核列表页,一个表格组件加一个分页组件,配合状态标签组件就能拼出来,不需要像 JQuery 时代一样手动拼 HTML 字符串。而且这个系统的统计页面用了图表库,Vue 生态里直接找现成的封装,做到页面上无刷新更新图表。
2.3 MySQL 在这个场景的适用性
就业管理系统的数据量不是特别大,属于典型的关系型数据模型。学生、就业信息、审核记录之间存在清晰的关联关系,统计场景需要 GROUP BY 按专业分组、需要 JOIN 连接学生表和就业信息表,这两件事都是 MySQL 的强项。加上 MySQL 的事务能力,审核接口里更新就业状态加插入审核记录这种组合操作,可以保证原子性。
有些人可能会觉得应该用非关系型数据库,但在这个系统里没有意义。就业数据的关联查询和报表统计太依赖 SQL 能力了,如果换成文档型数据库,统计逻辑会变得很复杂。
3. 数据库设计的关键:就业数据怎么建模才合理
3.1 用户与学生基础表
源码里数据库名字一般是 employment_db 或者类似名字。用户表的设计比较典型:sys_user 表,字段包括 id、username、password、real_name、role、avatar、create_time。密码一般存的是加密后的字符串,不是明文。
我见过很多项目为了做权限系统,把角色单独拆一张表,再搞角色菜单关联表。但就业管理系统角色是固定的三到四种,用户和角色就是多对一的关系,直接放字段可以少两张表,查询效率更高,代码也更简单。如果你的需求可能扩展出七八种角色,那是该拆表,否则别过度设计。
学生信息我建议单独放一张表,比如 student_info。与 sys_user 一对一关联,字段包含学号、姓名、性别、入学年份、专业、班级、联系电话、邮箱、辅导员 ID。学院老师要按班级筛选学生,单独一张表逻辑更清晰。建表字符集要用 utf8mb4,因为学生姓名可能包含生僻字。下面是一段可以直接复制执行的建表 SQL:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '加密密码', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT NOT NULL DEFAULT 3 COMMENT '角色 1管理员 2教师 3学生', avatar_url VARCHAR(255) COMMENT '头像', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';3.2 就业信息表的字段设计逻辑
就业信息表是系统里最核心的一张表,字段设计直接关系到统计报表能不能做出来。
我建议设计成下面这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| student_id | BIGINT | 关联 sys_user,学生ID |
| student_name | VARCHAR(50) | 学生姓名,冗余字段 |
| major | VARCHAR(50) | 专业,冗余字段 |
| class_name | VARCHAR(50) | 班级,冗余字段 |
| employment_type | TINYINT | 就业类型:1 已签约 2 升学 3 自主创业 4 参军 5 待业 |
| company_name | VARCHAR(100) | 单位全称 |
| company_credit_code | VARCHAR(30) | 统一社会信用代码 |
| job_title | VARCHAR(50) | 岗位名称 |
| salary_range | VARCHAR(30) | 薪资范围 |
| proof_url | VARCHAR(255) | 就业证明文件路径 |
| status | TINYINT | 审核状态 0待审核 1通过 2驳回 |
| review_comment | VARCHAR(255) | 审核意见 |
| submit_time | DATETIME | 提交时间 |
这里面的冗余字段是很多初学者不理解的。为什么不直接 JOIN 学生表拿姓名专业,反而要存一份冗余在就业表里?原因是统计场景非常多。系统要按专业查就业率,按班级查待审核数量,按学生查历史记录。如果每次都关联 sys_user 和 student_info,SQL 写起来繁琐,索引压力也大。冗余这两个字段后,统计就是单表 GROUP BY,性能和写法都简单很多。代价是学生改名或转专业时,需要同步更新就业表,考虑到就业信息是在毕业季集中提交的,这个代价完全可接受。
审核状态字段 status 是整个业务闭环的核心。后续所有接口,比如待审核列表、已通过列表、就业率统计,都围绕这个字段做筛选。另外,就业信息表最好加一个 created_at 和 updated_at,不然排错和比对数据时很被动。
3.3 审核记录表与数据字典
审核操作建议单独记录一张表,不要只在就业信息表里存一个状态字段。审核记录表大概长这样:
CREATE TABLE employment_review ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employment_id BIGINT NOT NULL COMMENT '就业信息ID', reviewer_id BIGINT NOT NULL COMMENT '审核人ID', action TINYINT NOT NULL COMMENT '操作 1通过 2驳回', comment VARCHAR(255) COMMENT '审核意见', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='就业信息审核记录';留这个表的理由是出了问题能追溯。比如某学生说“我的就业信息为什么被驳回了”,老师可以在审核记录里查到谁在什么时候给了什么意见,不用互相踢皮球。另外,系统公告或招聘信息这块,只需要一张 notice 表,做简单增删改查就行。如果项目用数据字典管理就业类型,可以再加一张 sys_dict,但我个人建议字段少的时候直接写死枚举即可,选型上不存在对错之分。
4. 后端核心实现:鉴权、业务接口与统计逻辑
4.1 JWT 登录鉴权的落地方式
这个 SpringBoot 后端在鉴权上采用的主流方案是 Spring Security + JWT。登录流程是:前端把用户名密码发给 LoginController,后端用 UserMapper 按用户名查出用户,再用 BCrypt 算法比对密码。比对成功就生成一个 JWT token 返回给前端,前端存到本地存储里,后续每个请求在请求头带一个 Authorization 字段。
JWT 的核心优势是无状态。服务器不需要在内存或 Redis 里面保存用户会话,每个请求自己带着身份信息来。这样后端做水平扩展的时候不需要考虑 Session 同步问题。
关键实现是一个 OncePerRequestFilter。这个过滤器在 Spring Security 的过滤器链里被注册,每次请求都会执行,逻辑是取请求头的 token,解析出用户 ID 和角色,然后放到 SecurityContext 里。核心代码模型大概是这样的:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims != null) { Long userId = claims.get("userId", Long.class); Integer role = claims.get("role", Integer.class); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userId, null, getAuthorities(role)); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }我之前提到这个项目源码的后端权限控制并不完善,这也是很多毕业设计类项目的通病。我手动测了一下,学生登录后直接调用教师的审核接口,如果接口上没有加校验,返回的竟然是正常数据。这个问题必须修。修复方式是在审核接口上加一个自定义注解,比如 @RequireRole(2),角色数字在拦截器里统一校验。
4.2 就业审核接口的业务逻辑
审核接口是整个系统里最典型的一个复杂操作。前端提交审核请求,入参是就业信息 ID 和审核结果。后端要做的事情有三件:
第一件,查就业信息是否存在并且当前状态是不是待审核,防止重复审核。第二件,更新就业信息表的状态字段,设置审核意见。第三件,插入一条审核记录。
这三步操作不能拆开执行,否则会出现状态重复更新或者记录丢失的问题。所以在审核方法上加 @Transactional 事务注解。这个注解的作用是,如果三步中任何一步抛异常,数据库回滚到操作之前的状态,保证数据一致性。实际中我用 MyBatis-Plus 的 updateById 更新就业信息表,再调用 reviewMapper.insert 插入记录。
再一个细节是审核列表的分页查询。用 MyBatis-Plus 的 LambdaQueryWrapper 构造条件,比如按班级查询、按状态筛选、按关键词搜索,最后用 Page 对象分页返回。这里需要提醒的是分页查询必须开启 MyBatis-Plus 的分页插件配置,不然分页不生效,我一开始忽略了这个配置,所有列表都返回全量数据,排查半天。
4.3 就业率统计与 Excel 导出
就业率统计是这种系统的核心输出。源码里的统计实现是,按专业分组以后,计算已就业人数与总人数的比例。给一个参考的 SQL 写法:
SELECT major, COUNT(*) AS total_count, SUM(CASE WHEN status = 1 AND employment_type IN (1,2,3,4) THEN 1 ELSE 0 END) AS employed_count, ROUND(SUM(CASE WHEN status = 1 AND employment_type IN (1,2,3,4) THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS employment_rate FROM employment_info GROUP BY major;这里的逻辑要仔细想一想,就业率的分母是全部毕业生人数,但分子一般只统计“成功就业”的人数,具体来说已签约、升学和参军都算进入就业统计口径,而待业和自主创业是否需要算进就业口径,不同学校政策不一样。写代码的时候最好把口径配成一个可配置项,而不是写死在 SQL 里,否则后期调整口径会让你很头痛。
Excel 导出我是用 Apache POI 实现的。查询出统计数据后,创建 XSSFWorkbook 对象,按行写入数据,设置单元格格式,最后通过 HttpServletResponse 把文件流写入响应。注意要设置正确的响应头,特别是 Content-Disposition,指定文件名和编码,不然浏览器下载的文件名会乱码。实测导出一万行数据没有问题,如果数据量再大,建议改成分批查询,避免内存溢出。
5. Vue 前端:怎么把就业管理业务串起来
5.1 路由设计与动态菜单
前端这块我用的 Vue 框架,配的 Vue Router。页面大致有登录页、学生个人信息页、就业信息填写页、审核管理页、统计报表页、系统管理页。
路由设计上要注意一个问题:不能只在前端做路由拦截,因为前端路由只是用户体验层面的控制,真正的安全防线在后端接口。前端用 beforeEach 路由守卫检查本地存储里有没有 token,没有就直接跳转登录页。这个代码很常见:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path !== '/login' && !token) { next('/login'); } else { next(); } });动态菜单的实现方式是登录接口返回用户角色,前端根据角色去匹配一个菜单配置数组,然后把能访问的菜单过滤出来。学生看到的是“我的信息”“我的就业记录”,教师看到的是“审核管理”“统计查询”,管理员看到的是全部菜单。这块就是要维护一张角色到菜单的映射表,写起来不复杂,但别漏掉一个点:菜单过滤了,对应的路由组件也要做权限判断,否则用户手动改 URL 依然能访问页面。
5.2 Axios 封装与登录授权
Vue 项目里一般会在 src/utils/request.js 里封装一个 axios 实例。基础配置是 baseURL 指向后端地址,timeout 设为十秒左右。然后在请求拦截器里把 token 加到请求头,在响应拦截器里统一处理 401 失效和业务错误码弹窗提示。
我实际跑的时候遇到一个很常见的坑:前端开发环境的 baseURL 配的是 http://localhost:8080,后端 SpringBoot 默认端口也是 8080,结果前端 devServer 和后端端口冲突,必须先改掉其中一个。这个项目后来我把后端的 server.port 改成了 8081,前端用 Vite 默认端口,两边才不会打架。
5.3 核心页面拆解
就业信息填写页是学生最常用的页面。表单字段很多,包括公司全称、统一社会信用代码、就业类型、岗位、薪资、证明文件上传。这里前端要注意的是和上传接口的对接。推荐做法是把文件传到后端一个 upload 接口,返回的是拼接好的文件路径,最后提交表单时把这个路径拼接进表单一起传。
审核管理页的关键是列表与详情联动。教师进入页面后看到的是待审核列表,点开详情能看到学生提交的完整就业信息,还有证明文件的缩略图和预览链接。通过和驳回的操作按钮要带确认弹窗,避免误操作,驳回的时候必须输入意见再提交。
统计页面我配了 ECharts,后端返回的数据包括各专业就业率、各就业类型的人数占比,前端直接用饼图和柱状图渲染。这里有个经验:不要让前端去算百分比,后端返回什么前端就显示什么,保证统计口径一致性。否则前端过滤了一部分数据,图表数字和后端统计挂不上,到时候不好解释。
6. 从零把项目跑起来:环境配置、启动部署与常见问题
6.1 环境要求与版本匹配问题
根据我对源码的检查和实际跑通的经验,环境要求大致是:JDK 1.8 或 11,Maven 3.6 以上,Node.js 14 以上,MySQL 5.7 或 8.0。这几个版本是配套的,不建议用太新的 JDK 去跑老项目,比如 JDK 17 启动 SpringBoot 1.x 或某些 2.3 之前的版本,会遇到类加载的兼容性问题。源码里如果 pom.xml 写的是 SpringBoot 2.3.x 或者 2.4.x,就老老实实用 JDK 8。
前端的环境跟 SpringBoot 版本也有关系。有些报错说 springboot 版本太高,本质上就是 JDK 版本和 SpringBoot 版本不匹配。SpringBoot 2.x 用 JDK 8 或 11 都没问题,但如果你用 JDK 17,又没加额外的 JVM 参数,很可能启动时报错。我的建议:直接装 JDK 8,最稳。
6.2 MySQL 数据库初始化与常见的 ssl 连接错误
拿到源码后,一般会带一个 .sql 文件,就叫它 init.sql。使用 Navicat 或者命令行导入都行。先在本地建一个和连接配置同名的数据库,比如 employment_db,字符集选 utf8mb4,然后运行 init.sql,导入完检查一下有没有表。
这里必须提醒一个经典问题:mysql ssl 连接错误。具体报错是类似 SSL connection error 或者 Establishing SSL connection without server's identity verification is not permitted 这样的提示。原因是 MySQL 8.0 默认开了 SSL 校验,而项目里的 JDBC 连接配置没有处理 SSL 参数。
解决方法是在 application.yml 的数据库连接地址后面加参数,把 SSL 关掉,同时把 serverTimezone 也指定好:
spring: datasource: url: jdbc:mysql://localhost:3306/employment_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true 这个参数在 MySQL 8.0 认证插件是 caching_sha2_password 时也要带上,不然会出现 public key retrieval is not allowed 的报错。这一行连接串包含了四五个常见坑点,建议直接抄过去。
6.3 后端启动的具体步骤
用 IDEA 打开根目录,第一次导入 Maven 项目会自动下载依赖。如果下载很慢,可以在 maven 的 settings.xml 里配置国内镜像源。等待依赖解析完,找到主类,即带 @SpringBootApplication 的类,右键运行。
启动的时候观察控制台。启动成功的标志是看到 Spring Boot 的 Banner 和 Tomcat started on port(s): 8080。这里有个容易被忽略的坑:如果项目里配置了上传文件路径,Windows 上和 Linux 上路径格式不同。比如代码里写的 file.upload-path: /data/upload,Windows 本地是不存在的,启动时虽然不报错,但上传文件的时候找不到目录。我第一次跑就遇到这种问题,上传接口一直 500,日志里提示目录不存在,创建目录后就正常了。
6.4 前端启动的具体步骤
进入前端目录,执行 npm install 安装依赖。这一步可能耗时间比较长,也可能遇到 node-sass 安装失败这种经典问题。如果报错提示 node-sass 相关的 binding 问题,就检查 Node 版本和 node-sass 版本是否匹配,或者直接换成 sass 和 sass-loader 的新版本组合。
然后执行 npm run dev,启动开发服务器。启动后访问提示的端口,正常情况下能打开登录页。有一个坑是前端请求后端接口会有跨域问题。开发环境下,在 vue.config.js 或者 vite.config.js 里配置代理,把 /api 前缀的请求转发到后端地址。如果后端配了 CORS,也可以解决,但我更推荐代理方式,因为生产环境 Nginx 也是这么配的。
6.5 打包与生产部署
最终交付不能只用 IDEA 和 node 跑 dev server,还要做生产构建。后端用 mvn clean package 打出 jar 包,在服务器上执行 java -jar target/employment-system.jar 就行。前端执行 npm run build,在 dist 目录下产生一堆静态文件,用 Nginx 托管。
Nginx 配置要注意 API 反向代理。前端静态文件交给 Nginx,所有 /api 请求反向代理到本地的后端端口,从而实现前后端同一域名访问。还有 history 模式路由的 fallback 配置,把非静态文件路径都指到 index.html,否则页面刷新会 404。这两条配置几乎每次部署都要写,趁早存一份。
7. 扩展这个系统,我建议从这三个方向改
7.1 就业统计口径做成可配置
学校每年的就业统计要求可能不同,写死 SQL 的做法早晚要改代码。我在里面加了一张统计口径配置表,管理员在后台可以勾选哪几种就业类型计入就业率,刷新统计页面时前端参数自动带上,这个改动很小但对维护体验提升很大。
7.2 证明文件加校验与水印
目前上传的证明文件就是原始文件,学生传什么就是什么,存在身份信息被借用的风险。我后来的做法是给证明文件自动叠加学生姓名和学号水印,并在上传时对图片做尺寸和类型校验。
7.3 审核操作接上消息通知
学生提交信息后,教师端能收到通知,通过或驳回后学生端也能看到新的状态变化。开发环境可以用简单的轮询,上线后可以用 WebSocket 推送。这个小功能会让整个系统的闭环体验好很多。
我接手这套源码的整个过程大概就是这样。从跑通环境,到发现后端权限校验漏洞,再到读表结构、改统计口径、最后部署上线,花了差不多一周。最值得说的一点是,看源码的时候千万不要只盯着 Controller 层一段段读,一定要先从业务闭环去理解数据和状态的流转,才能把项目真正吃透。如果照着这套思路跑一遍,你自己改起来会顺手很多。