☰
SpringBoot+Vue+MyBatis+MySQL前后端分离实战:从零开发就业管理系统
2026/10/2 8:45:27 网站建设 项目流程

每年毕业季,就业办和辅导员的催表现场我见得太多了:学生信息一个Excel、企业岗位一个Excel、就业情况又散落在各种聊天记录里,最后统计就业率的时候,三个表对不上,数据全靠拍脑袋。接这种"就业管理系统"的需求多了以后,我反而觉得它特别适合拿来练手前后端分离技术栈。用SpringBoot+Vue+MyBatis+MySQL把这套流程做成系统,既能让学生自助登记信息、教师在线审核,又能按学院和专业自动生成就业统计报表,比Excel来回传靠谱得多。

这是一篇写给正在做毕业设计、课程设计,或者想用一套完整项目实战SpringBoot+Vue前后端分离开发的同学的拆解文章。我不会只贴代码,而会把"这个系统到底要管哪些数据""前后端怎么配合""怎么从本地跑通到服务器部署"讲清楚,最后再分享几个我在这类项目里反复踩的坑。整套源码的逻辑我尽量按照就业管理系统的真实业务来设计,你可以直接拿来改。

1. 就业管理系统拆解:先想清楚管什么,再谈技术栈

1.1 就业管理系统的核心业务闭环

做管理系统最忌讳一上来就建表写接口。就业管理系统听起来简单,实际上业务流程是有一条主线的:学生维护个人信息 → 管理员发布企业岗位 → 学生登记就业去向 → 教师/管理员审核 → 系统按口径统计就业率。任何一个环节缺失,系统都会变成"花架子"。

把这个闭环拆开,系统的角色就清晰了:

  • 学生端:查看岗位、完善个人信息、登记就业去向(签就业协议、劳动合同、升学、灵活就业等)、查看审核结果。
  • 教师/辅导员端:维护所带学生信息,审核学生的就业登记,查看本专业的就业统计。
  • 系统管理员端:维护学院、专业、班级等基础数据,管理企业信息和岗位发布,管理用户账号和角色权限,看全站统计报表。

这也是这类项目最容易出彩的地方——它不是单一的CRUD,而是有角色、状态流转、统计报表这三个难点的。你在答辩或者给面试官讲的时候,能讲清楚这三块,项目含金量会高很多。

1.2 为什么选SpringBoot+Vue+MyBatis+MySQL这套组合

说实话,就业管理系统这种业务不算复杂,但它是典型的管理信息系统场景,用这套技术栈非常合适:

  • SpringBoot承担后端接口层:内置Tomcat,起步依赖一键引入,写RESTful接口的成本极低,省掉一堆Spring XML配置。这对需要快速出功能的项目很关键。
  • Vue承担前端交互层:就业登记表单、审核列表、统计图表这些场景,用Vue的双向绑定和组件化开发,比传统JSP+JQuery的写法舒服太多,尤其是统计部分配合ECharts,体验感完全不一样。
  • MyBatis承担数据库访问层:就业统计经常要写多表关联和条件拼接,MyBatis的动态SQL在这种"条件不固定"的统计场景里非常灵活,躲开了JPA那种自动生成SQL导致的不确定性,出了问题好排查。
  • MySQL作为数据底座:这类管理系统数据量几万条顶天了,MySQL的性能完全够用,而且部署环境最普及,实训/毕设环境基本都有。

这里补充一句:有人会问,那直接用若依框架改不就行了?若依确实把权限、代码生成、用户管理都封装好了,用它做项目确实快,但问题在于你很难讲清楚"系统里哪块是你自己写的"。如果是为了学习,我更建议从零搭一套轻量骨架出来,把登录、权限、核心业务自己走一遍。真正理解了以后再去看若依,会有完全不同的收获。

1.3 数据库设计:就业管理系统的关键表与字段

数据库设计是这类系统最不能省的一步。我设计这套项目时,核心表大概是这样的:

表名用途关键字段
sys_user系统用户/登录账号id, username, password, role_id, status
student_info学生基本信息id, user_id, name, student_no, college_id, major_id, class_id, phone
college/major/class学院/专业/班级基础数据id, name, parent_id 等相关字段
company企业信息id, name, industry, address, contact, contact_phone
job岗位信息id, company_id, title, salary_min, salary_max, location, status
employment_record就业意向/就业去向登记id, student_id, job_id, emp_type, company_name, salary, sign_date, status, audit_by, audit_time
sys_role/menu角色与菜单权限id, name / id, parent_id, path, name, perms

几个值得注意的设计点:

  • 学生信息与用户账号分离:sys_user只管登录和权限,student_info管业务数据,这样辅导员账号、管理员账号就不用硬塞进学生表。
  • 就业类型 emp_type 用字典值:比如1=签就业协议、2=劳动合同、3=灵活就业、4=升学、5=自主创业,后续统计全靠这个字段分组,不建议用字符串散存。
  • 审核状态带上审核人:audit_by、audit_time 这两个字段一定要留,不然数据出了问题没办法追溯到人,答辩时被问"数据准确性怎么保证"也能接得上。

建表SQL这里给一个核心片段,学生表和就业登记表大概是这样的关系:

CREATE TABLE student_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '关联sys_user.id', name VARCHAR(50) NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, college_id INT NOT NULL, major_id INT NOT NULL, class_id INT NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE employment_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT '关联student_info.id', job_id INT DEFAULT NULL COMMENT '关联岗位,可空', emp_type TINYINT NOT NULL COMMENT '1协议 2合同 3灵活 4升学 5创业', company_name VARCHAR(100), salary VARCHAR(50), sign_date DATE, status TINYINT DEFAULT 0 COMMENT '0待审核 1通过 2驳回', audit_by INT, audit_time DATETIME, audit_remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

我把company_name放进了就业登记表,而不是只关联job_id,因为很多学生是自主找的工作,并没有走系统里的岗位,这种"冗余字段"在业务上是必要的——统计和展示的时候,你不可能每次都去关联岗位表再join企业表。

2. 后端SpringBoot+MyBatis:从登录认证到就业登记的接口实现思路

2.1 后端项目结构分层:先立规矩再写代码

SpringBoot后端的分层,我的习惯是controller/service/mapper/entity四层,另外加config、common、dto、vo几个辅助包。就业管理系统虽然体量不大,但分层清晰有一个直接好处:以后加一个功能,你知道代码应该往哪放。

com.example.employment ├── controller // 接收请求,校验参数,返回Result ├── service // 业务逻辑,事务边界 │ └── impl ├── mapper // MyBatis接口,SQL映射 ├── entity // 数据库实体 ├── dto // 前端传参对象 ├── vo // 返回前端展示对象 ├── config // 跨域、拦截器、WebMvc配置 ├── common // 统一返回结果、异常处理、常量 └── utils // JWT、字符串等工具

一个容易犯的错是把entity直接返回给前端。比如学生信息里有user_id、create_time这种字段,前端未必需要,而且直接把实体暴露出去,一旦前端拿到不该看的数据就不好收拾。我习惯在接口返回前转成VO,哪怕VO结构和实体一模一样,这个习惯也能防止后续字段膨胀时手忙脚乱。

2.2 登录认证与权限拦截:前后端分离项目的会话方案

前后端分离之后,Session那套在跨域场景下体验很糟糕,我在这套系统里用的是JWT方案。流程不复杂:

  1. 前端登录,把username和password发到/api/auth/login。
  2. 后端校验用户信息,生成JWT令牌返回,前端存到localStorage。
  3. 后续请求在请求头里带Authorization: Bearer <token>。
  4. 后端写一个拦截器,解析token,拿到用户ID和角色,放行或拦截。

核心代码大致是这样的:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute("userId", claims.get("userId")); request.setAttribute("roleId", claims.get("roleId")); return true; } catch (Exception e) { // token过期或非法 } } // 返回401 }

这里有个很重要的细节:拦截器放行OPTIONS请求必须写在最前面。否则前端跨域请求预检直接失败,登录接口都调不通,很多人第一次做前后端分离就卡在这。

权限方面,我没用Spring Security那套重武器,而是在拦截器里拿roleId做接口级别控制。具体写法是给需要权限的接口加注解,比如@RequireRole(2)表示只允许管理员访问,拦截器里判断角色ID,不匹配直接返回"无权限"。对于就业管理系统这种只有两三种角色的项目,这个方案足够,省下的学习成本可以投入到业务功能上。

2.3 就业登记与审核的状态流转逻辑

就业登记是整个系统业务味最浓的地方,也是面试/答辩时最能讲故事的地方。它的核心是一个状态流转:

  • 学生提交登记,status=0(待审核)
  • 教师/管理员审核通过,status=1
  • 教师/管理员审核驳回,status=2,并填写驳回原因
  • 学生被驳回后可以修改再次提交,status重新回到0

这个流转必须放在service层用事务保证。比如审核通过时,不仅要改employment_record的状态,还要更新sys_user的某个标记或者记录审核日志,任何一个失败都应该回滚。我在Service里的写法是这样的:

@Transactional(rollbackFor = Exception.class) public void auditEmployment(Integer recordId, Integer auditResult, String remark, Integer auditorId) { EmploymentRecord record = employmentMapper.selectById(recordId); if (record == null) { throw new BusinessException("就业记录不存在"); } if (!Integer.valueOf(0).equals(record.getStatus())) { throw new BusinessException("当前状态不允许审核"); } EmploymentRecord update = new EmploymentRecord(); update.setId(recordId); update.setStatus(auditResult); update.setAuditBy(auditorId); update.setAuditTime(new Date()); update.setAuditRemark(remark); employmentMapper.updateById(update); // 可以在这里追加操作日志等动作 }

注意我在事务里先查了一次记录并做了状态判断,这是并发下防止重复审核的关键。不加这个判断,两个管理员同时审核同一条记录,状态就可能被覆盖成后提交的那份,数据就乱了。

2.4 MyBatis动态SQL与多表查询:统计报表一次搞定

就业统计是MyBatis的高光时刻。比如要统计"各专业就业率",SQL需要把学生表、专业表、就业登记表关联起来,还要按条件动态过滤——学院、专业、就业类型、时间范围可能都可以选。这种SQL用注解写在Java里难受,放在XML映射文件里配合动态标签就很顺手。

<select id="countEmploymentStats" resultType="map"> SELECT m.name AS majorName, COUNT(DISTINCT s.id) AS totalCount, COUNT(DISTINCT CASE WHEN er.id IS NOT NULL AND er.status = 1 THEN s.id END) AS employedCount FROM student_info s LEFT JOIN major m ON s.major_id = m.id LEFT JOIN employment_record er ON s.id = er.student_id AND er.status = 1 <where> <if test="collegeId != null"> AND s.college_id = #{collegeId} </if> <if test="majorId != null"> AND s.major_id = #{majorId} </if> <if test="empType != null"> AND er.emp_type = #{empType} </if> </where> GROUP BY s.major_id, m.name ORDER BY employedCount DESC </select>

这里分享一个我踩过的坑:如果直接用COUNT(DISTINCT s.id)和LEFT JOIN employment_record,一旦一个学生有两条审核通过的就业记录,总数会被放大,就业率甚至可能超过100%。所以我写统计时会强调"一个学生只算一条有效就业记录"的约定,业务层面要么在登记时做唯一约束,要么在统计时对er.id做去重。就业率超过100%的系统,给老师演示时会非常尴尬,这个问题一定在开发期就堵住。

另外,关于MySQL连接串,一定要设置下面这些参数,否则会出现中文乱码和时区报错:

jdbc:mysql://localhost:3306/employment?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

allowPublicKeyRetrieval=true是MySQL 8.0经常出现的认证报错解法,如果你用的新版MySQL连接时报Public Key Retrieval is not allowed,八成是少了这个参数。

3. Vue前端:页面不是堆出来的,是围绕接口设计长出来的

3.1 构建工具与目录组织:Vite和Vue CLI怎么选

前端我用Vue 3,构建工具推荐Vite。Vite启动速度快、配置简洁,而且Vue 3的生态已经全面转向它了。如果你在学校课程里学的还是Vue CLI,也不影响理解,核心逻辑是一样的。

先看一下前端目录怎么规划:

src ├── api // 按模块封装的接口请求(user.js, student.js, job.js, stats.js) ├── assets // 静态资源 ├── components // 通用组件(分页表格、弹窗表单、图片上传等) ├── layout // 主布局(侧边栏、顶部栏、面包屑) ├── router // 路由配置 ├── store // Pinia状态管理(用户信息、权限) ├── utils // request.js axios封装 ├── views // 页面视图 │ ├── login │ ├── dashboard // 首页统计大盘 │ ├── student // 学生管理 │ ├── job // 岗位管理 │ ├── employment // 就业登记与审核 │ └── stats // 就业统计报表 └── App.vue

这套目录不是随便起的,核心原则是api目录和views目录一一对应。前后端分离项目里,前端最容易乱的就是接口调用散落在各个组件里,改一个接口地址要全局搜索。把接口按页面模块抽成独立文件,后端接口变动时只需要改一个文件。

3.2 axios封装:请求拦截器里统一注入Token

axios封装是前端工程质量的分水岭。一个合格的request.js至少要干三件事:

  1. 请求拦截器中给每个请求带上Authorization头。
  2. 响应拦截器中统一处理HTTP错误码和业务错误码。
  3. 返回数据只解包到data,页面代码里不用每次res.data.data。

核心代码大致是:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

3.3 就业管理页面的核心套路:表格+搜索+弹窗+分页

就业登记与审核页面是这套系统的典型页面,几乎覆盖了管理系统前端的所有套路:顶部搜索条件、中间数据表格、右侧操作按钮、点击后弹窗展示详情/表单。开发时可以直接用Element Plus(Vue 3对应版本)的el-table、el-form、el-dialog、el-pagination组合。

页面组合套路大概是:

  • 搜索区:学院下拉、就业类型下拉、审核状态下拉、关键字输入框,点击"查询"触发loadData(1)。
  • 表格区:el-table-column绑定数据列,状态列用tag显示颜色(待审核=黄色、通过=绿色、驳回=红色)。
  • 操作区:审核通过、驳回、查看详情,不同的角色能看到不同按钮,用自定义指令或v-if按角色权限控制。
  • 分页区:el-pagination组件,current-page和page-size对应后端的pageNum和pageSize。

这类页面第一次写会觉得有点繁琐,但熟练之后会发现模式非常固定。我实际开发时会把「搜索区+表格+分页」抽成一个通用组件,后续所有管理页面通用,效率能提一倍。不过如果你是为了学习,前期不建议抽太狠,先亲手写两三个页面找到共性再去抽象,效果最好。

3.4 统计报表页面:ECharts展示就业率排行

就业统计页是系统的门面,我用ECharts来画图。主要包含三个图:

  • 各专业就业率柱状图:横向对比专业间的就业率。
  • 就业类型占比饼图:协议、合同、升学、创业的分布。
  • 月度就业趋势折线图:从9月到次年7月,就业登记通过人数的变化趋势。

图表数据的来源就是后端写的统计接口,前端拿到结果后把数据组织成ECharts需要的格式。注意一个经验:统计计算尽量放后端SQL里,不要拿全量数据到前端做遍历累加。我见过不少项目为了省事,前端把几千条明细拉回来自己写reduce算比例,不仅性能差,还容易出现口径不一致——页面一多,每个页面算出来的数都对不上,最后全在开会扯皮。

4. 完整部署教程:从本地跑通到服务器上线的五个步骤

4.1 环境准备:版本兼容性检查

部署前先把环境版本对齐,这一条非常重要。我建议统一使用以下组合,都是经过大量项目验证的稳定搭配:

组件推荐版本说明
JDK1.8 或 11SpringBoot 2.x用1.8最稳,SpringBoot 3.x需要17
Maven3.6+构建后端jar包
Node.js16 LTS 或 18 LTS前端构建环境
MySQL5.7 或 8.0注意8.0的连接驱动与认证方式
SpringBoot2.7.x和 MyBatis、MySQL驱动兼容性好,不建议一上来用3.x
MyBatismybatis-spring-boot-starter 2.x不要和SpringBoot版本乱配

这里重点提醒一下:网上很多教程默认最新版,但你用SpringBoot 3.x + mybatis starter 2.x时,启动会直接报找不到SqlSessionFactory相关的类。我自己的做法是,先确定SpringBoot版本,再反推选对应兼容的MyBatis starter版本,不要所有依赖都无脑写latest。

4.2 数据库初始化:别把建表SQL交给可视化工具手动点

我在项目里一般是把建表SQL和初始数据放在一个sql/init.sql文件里,部署时一条命令全部导入:

mysql -u root -p employment < sql/init.sql

这里有个小诀窍:init.sql里除了建表,还要插入默认管理员账号(比如admin/admin123,密码用MD5或BCrypt加密后的值),不然新环境启动后端后连登录都进不去。建议SQL文件里统一写好基础数据和演示数据,方便快速体验。

4.3 后端启动与生产配置:开发环境vs生产环境

本地开发时后端直接用:

mvn spring-boot:run

或者用IDE启动Application类。但部署到服务器时,我更推荐打包成可执行jar再运行:

mvn clean package -DskipTests java -jar target/employment-system.jar --spring.profiles.active=prod

SpringBoot的多环境配置要利用起来。项目里至少要有application-dev.yml和application-prod.yml:

  • dev环境:数据库连接指向本地127.0.0.1,日志级别DEBUG,文件上传路径指到本地临时目录。
  • prod环境:数据库连接指向服务器IP或云数据库,日志级别INFO,文件上传路径用绝对路径如/usr/local/employment/upload/,并配上日志滚动策略。

用profiles管理配置后,换环境不用改代码重新打包,这是部署类项目的基本功。

4.4 前端构建:npm install、npm run build、dist产物

前端部署前先执行:

npm install npm run build

构建完成后会生成dist/目录,里面是静态文件(index.html、js、css)。这一步最常见的问题是依赖安装版本不一致,我的建议是项目里必须锁定package-lock.json,不能用npm install随意升级小版本,否则队友或服务器上构建出来的产物可能不一样。

另外,执行npm run build之前,一定要确认vite.config.js里的base配置。如果后端接口前缀是/api,那么server.proxy下要配置/api代理到后端地址;生产环境则把接口地址配置在环境变量里,防止打包后改不了接口地址。

4.5 服务器部署方案:jar + Nginx 还是后端托管静态资源

前后端分离项目部署,我推荐两种方案,按项目规模选:

方案一:jar + Nginx分离部署(首选)

  1. 后端jar包上传到服务器,用nohup java -jar启动,监听8080端口。
  2. 前端dist目录上传到服务器的/usr/local/nginx/html/employment/。
  3. Nginx配置/api反向代理到http://localhost:8080/api,其余请求指向前端静态文件。

核心Nginx配置片段:

server { listen 80; server_name your-domain.com; location / { root /usr/local/nginx/html/employment; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里的try_files $uri $uri/ /index.html;极其重要。如果没有这一行,前端路由在刷新非首页时会出现404错误,这是Vue history路由部署的经典坑,下一节我会展开讲。

方案二:后端托管dist(简单但不够专业)

把前端dist/目录拷贝到后端项目的src/main/resources/static/下,重新打包后端jar。这样只有一个进程,部署最简单,但前后端分离的意义就没那么纯粹了,后续前端每次改动都要动后端项目,我不推荐在正规项目里这样搞。

启动jar包时用nohup命令,并把日志输出到文件里,方便以后排查问题:

nohup java -jar employment-system.jar --spring.profiles.active=prod > employment.log 2>&1 &

5. 我实际开发中反复踩的坑:跨域、404、版本兼容与统计偏差

5.1 跨域问题:不是加个@CrossOrigin就完事的

前后端分离项目第一次联调,90%会碰到跨域问题。你在浏览器里访问前端http://localhost:5173,然后前端发起请求到后端http://localhost:8080,浏览器就会拦截,提示has been blocked by CORS policy。

网上很多教程会让你在后端Controller加@CrossOrigin,或者一个类一个类地加@CrossOrigin(origins = "*")。这样做确实能解决,但不规范,因为生产环境你根本不想放开所有来源的跨域访问。我的做法是全局配置一个CorsFilter,只允许配置的前端地址和固定请求头:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:5173"); config.addAllowedOrigin("http://your-domain.com"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

另外还有个细节容易漏:如果后端配置了JWT拦截器,跨域预检OPTIONS请求会被拦截器拦截掉,导致实际请求永远发不进来。所以拦截器里第一条规则就是放行OPTIONS,这一点前面已经强调过,但值得再重复一次,因为它真的能卡人一整天。

5.2 Vue打包后刷新404:history路由必须配合服务器配置

前端开发模式下,Vue Router用的是history模式,在dev环境一切正常,因为Vite的开发服务器帮你做了rewrite。但部署到Nginx后,如果你直接访问http://your-domain.com/employment/list,Nginx会去查找实际的/employment/list这个文件或目录,找不到就返回404。

解决方式就是Nginx配置里的try_files:

location / { root /usr/local/nginx/html/employment; index index.html; try_files $uri $uri/ /index.html; }

意思是:先找真实文件($uri),找不到目录($uri/)也行,都找不到就回退到/index.html,交给Vue Router去解析路由。这个配置我每次写Nginx都会加上,已经形成肌肉记忆了。

如果你用的是history模式但不想配服务器,另一个选择是用hash模式,URL上会多一个#,不太好看,但省事。

5.3 SpringBoot版本太高引发的启动报错:怎么快速定位兼容问题

前端时间用新电脑搭环境,我直接用SpringBoot最新的3.x版本,结果引入mybatis-spring-boot-starter2.3.1之后,项目启动直接报:

Invalid value type for attribute 'factoryBeanObjectType': java.lang.String

最后定位到是MyBatis Starter 2.x 与 SpringBoot 3.x 的包不兼容。翻了一圈解决方案,要么换mybatis-spring-boot-starter3.x,要么把SpringBoot降级到2.7.x。后面我统一把项目模板锁定在SpringBoot 2.7 + MyBatis Starter 2.3.x,再也没出过这类问题。

我的经验是:遇到版本依赖问题,先确认你是否在用一个"验证过"的组合,而不是去猜哪个最新版能用。打开项目的pom.xml,把SpringBoot和starter的版本号用变量的方式集中管理,升级也方便回退。

5.4 MyBatis的缓存机制:项目里到底要不要开二级缓存

MyBatis缓存是面试常客,实际项目里也要注意。默认情况下MyBatis一级缓存是SqlSession级别的,一个会话内重复查询会命中缓存。但SpringBoot集成MyBatis后,每次请求拿到的SqlSession通常是新建的,一级缓存基本等于每请求销毁重建,所以不要以为一级缓存能帮你扛住查询压力。

二级缓存是Mapper级别的,默认不开启。我在就业管理系统里没有开启二级缓存,原因很简单:这个系统的数据实时性要求高,学生提交就业登记、审核员通过审核,数据状态一变,如果缓存没刷新,统计数据就会延迟甚至错乱。MyBatis的二级缓存刷新机制(flushCache)又是按语句配置的,稍微漏一条更新语句,就会出现"明明数据库改了,页面上数字不变"的诡异问题。缓存这东西,用对了是性能优化,用错了就是数据事故,我的建议是在这种管理类系统里默认不开,把查询优化做好就够了。

5.5 就业统计数字对不上:LEFT JOIN配COUNT的典型误用

最后一个坑也是我调试最久的一个。当时做统计报表,按专业统计就业率的SQL怎么算都不对,有些专业就业率超过100%,有些专业明明没人就业却显示有数字。排查后发现是两个问题叠加:

第一个问题,LEFT JOIN employment_record时,一个学生有多个岗位的登记记录,导致行数翻倍,COUNT(s.id)也被放大。解决办法是用COUNT(DISTINCT s.id),业务上求的是"多少个学生",不是"多少条记录"。

第二个问题,统计"未就业"时我一开始用了RIGHT JOIN或者WHERE er.id IS NULL的写法,结果漏掉了那些连就业登记都没有的学生(因为他们压根不在employment_record表里,用RIGHT JOIN从就业表出发自然查不到)。正确写法的思路是:从学生表出发,LEFT JOIN就业表,再用条件聚合。我把这个口径写死到SQL注释里,后来无论做多少张统计报表都沿用这个模板。

数据仓库的同学经常说"先定义口径,再写SQL",这句话在小项目里同样适用。你做任何一个统计功能,先问清楚:就业率=已就业人数/毕业生总人数,分母到底是全部学生还是已登记的学生?这个口径不确定,SQL写一百遍也是错的。

这套就业管理系统做到这里,从数据库设计到后端接口,从Vue页面到生产部署,已经是一条完整的链路了。我个人的感受是,技术上没有特别高深的东西,但正是这种项目能把前后端分离开发里的"坑"基本踩一遍——跨域、鉴权、动态SQL、状态流转、静态资源部署、统计口径。你把这套系统完整跑通、部署上线,再回去看SpringBoot、Vue、MyBatis、MySQL这四个关键词下搜到的各种面试题,很多问题就会从"背答案"变成"我踩过"。如果后续想加功能,我建议从"批量导入学生信息(Excel解析)"和"消息通知(审核结果推送给学生)"这两个方向入手,它们正好能补上管理系统最常用的两板斧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询