ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3+MyBatis-Plus实战:校园招聘系统全栈开发详解

SpringBoot2+Vue3+MyBatis-Plus实战:校园招聘系统全栈开发详解 做校园招聘系统技术栈选了SpringBoot2Vue3MyBatis-PlusMySQL8.0这套组合很多人第一反应是“为啥不用最新的SpringBoot3”“为啥MyBatis-Plus而不是JPA”这些问题我在开发过程中反复被问到。说实话这套组合在当下的Java Web项目里是相当能打的一套配置尤其在校园招聘这种典型的“信息管理审批流转多角色权限”业务场景里它兼顾了开发效率、代码可读性和后续维护成本。这篇博客我用一个完整的校园招聘系统项目为例把从数据库设计、后端接口实现到前端页面开发再到打包部署的完整链路拆开讲清楚项目包含了学生、企业、管理员三种角色涵盖职位发布、简历投递、面试邀约、数据统计等核心功能并且随项目附带了一份完整的开发文档适合刚学完Java Web基础、想找个完整项目练手的大学生也适合毕业设计选题方向是校园招聘的在校生或者准备转行Java开发、想看看真实业务项目长什么样的朋友参考。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot2而不是SpringBoot3为什么是MyBatis-Plus先回答最常被问到的问题都2024年了为什么还选SpringBoot2最简单的理由是稳定性和生态。校园招聘系统这类项目本质上是个典型的管理系统核心诉求是快速开发、稳定运行、资料好找。SpringBoot2经过这么多年的迭代各种坑早就被踩平了网上随便一搜就有大量解决方案遇到问题不至于卡死。SpringBoot3虽然新但要求JDK17起步很多学校机房和服务器环境未必支持而且部分第三方库的兼容性还需要时间验证。对于教学、毕业设计和商用起步阶段SpringBoot2.7.x其实是最省心的选择。再聊MyBatis-Plus。其实做校园招聘这种系统数据关系不算特别复杂JPA也能做但我个人更推荐MyBatis-Plus原因有三点第一它保留了MyBatis的SQL控制力复杂报表和联表查询可以手写SQL不会像JPA那样遇到复杂查询就抓瞎第二它内置的BaseMapper、ServiceImpl这些通用方法能覆盖单表CRUD大约80%的场景可以少写大量重复代码第三分页插件、逻辑删除、自动填充这些功能都是配置即用对校园招聘系统里常见的用户列表、职位分页、简历管理等场景特别合适。这套技术栈选完之后整个项目的形态就很清晰了SpringBoot2负责后端接口Vue3负责前端页面MyBatis-Plus负责数据库操作MySQL8.0负责数据存储前后端通过JSON交互完全的前后端分离架构。这个架构的好处是团队协作时可以前后端并行开发不会互相阻塞而且后续如果要做小程序端或者App端后端接口可以直接复用。1.2 校园招聘系统的业务边界和核心功能拆解校园招聘系统听起来很简单但真正把业务流程理清楚并不容易。我最初画图时第一版功能特别多什么在线笔试、视频面试、Offer管理都放进去了后来发现以这套技术栈和精力根本做不完果断做了减法。最终落地的系统围绕三种角色来设计边界非常清晰。学生端前台注册登录、个人信息维护、浏览企业发布的职位、按城市/岗位类型/薪资范围筛选职位、投递简历、查看投递状态、收藏职位、接收面试通知。企业端前台企业信息认证与维护、发布职位、查看收到简历、对简历进行筛选处理通过/淘汰、发送面试邀约、查看已发布的职位列表。管理员端后台学生账号管理禁用/启用、企业账号审核、职位信息审核、公告管理、全站数据统计用户总数、职位总数、投递总数、行业分布等。这三条线加起来差不多就是校园招聘系统的核心闭环。实际开发时我建议先做管理员端因为管理员端几乎是学生端和企业端的汇总先把数据管理跑通学生和企业端就有数据可以联调了。1.3 前后端分离的工程结构怎么摆放工程结构这块我踩过不少坑最初尝试过把前端文件直接扔进SpringBoot的static目录里后来发现开发时改一行代码要重启后端效率低到崩溃。后来老老实实改成前后端分离两个独立工程前端用npm启动后端用Maven启动联调时通过代理解决跨域。整个项目的目录结构大概是这样的campus-recruitment ├── backend (SpringBoot2 后端工程) │ ├── src/main/java │ │ └── com/example/recruit │ │ ├── controller // 接口层 │ │ ├── service // 业务逻辑层 │ │ ├── mapper // MyBatis-Plus的Mapper接口 │ │ ├── entity // 实体类 │ │ ├── common // 公共类结果封装、异常处理、工具类 │ │ └── config // 配置类跨域、拦截器、分页插件 │ └── src/main/resources │ ├── application.yml // 配置文件 │ └── mapper // XML文件复杂SQL └── frontend (Vue3 前端工程) ├── src │ ├── api // 接口请求封装 │ ├── router // 路由配置 │ ├── stores // Pinia状态管理 │ ├── views // 页面组件 │ ├── components // 公用组件 │ └── utils // 工具函数 └── package.json这种结构的好处是责任单一后端不关心页面长什么样前端不关心SQL怎么写。对于一些刚接触项目开发的同学来说这个结构也特别好理解每一层干什么一目了然。2. 数据库设计与MyBatis-Plus代码生成2.1 MySQL8.0建库时的字符集和排序规则选择数据库设计是整个项目的地基校园招聘系统涉及大量中文数据字符集如果选错后面插入中文直接乱码或者报错排查起来非常头疼。我建库时用的是MySQL8.0字符集和排序规则一定要指定UTF8MB4。CREATE DATABASE campus_recruit CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;为什么是utf8mb4而不是utf8因为MySQL的utf8是阉割版最多支持3个字节像某些生僻字和Emoji符号根本存不进去而utf8mb4是完整的4字节UTF-8编码兼容性最好。排序规则选unicode_ci是因为它对中文和英文字母的排序比较合理且不区分大小写查询时更宽容。MySQL8.0和旧版本还有个明显的区别是默认认证插件变成了caching_sha2_password如果用一些老版本的数据库连接工具或者JDBC驱动可能会报认证失败。解决办法是在建用户时指定mysql_native_password不过SpringBoot2的ojdbc驱动版本够新的话一般不会遇到这个问题。2.2 核心表结构设计与字段规划校园招聘系统的表结构我最终设计了8张核心表用户表、学生信息表、企业信息表、职位表、简历表、投递记录表、收藏表、公告表。这里我挑几张关键表来讲。用户表sys_user是所有角色的基础学生、企业、管理员统一存在这张表里通过role字段区分身份简化了登录逻辑。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码MD5加密后, 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;职位表job_position是业务的核心字段比较多重点包括职位名称、所属企业ID、职位类别、工作城市、薪资范围、学历要求、招聘人数、职位描述、状态等。其中一个比较关键的设计是“状态”字段我用的是TINYINT0表示待审核1表示已发布2表示已下线3表示审核驳回。这种状态机的设计在业务里特别常见建议初学者一定要有这种意识。简历表resume相对独立因为一个学生只能有一份默认简历简化场景但设计上用了外键关联学生ID并保存了简历文件的URL、姓名、手机号、邮箱、教育经历、项目经历、技能标签等字段。简历这块我建议既存文件又存结构化文本因为有些场景只需要快速展示摘要不需要每次下载文件再解析。投递记录表delivery_record是另一个核心表记录学生ID、职位ID、投递时间、状态待查看/已查看/已通过/已淘汰、面试时间、面试地点、面试备注。这张表的两端连接了学生和企业所有交互都在这张表上流转。2.3 通用字段和逻辑删除的约定所有核心表我都统一加了create_time、update_time两个字段这样后端做列表排序时可以直接按创建时间倒序不用每个表单独设计。MyBatis-Plus的MetaObjectHandler可以统一处理这两个字段的自动填充不用每个插入操作手动set值。另外表设计时我用了逻辑删除而不是物理删除。所有业务表都加了一个deleted字段默认0删除时执行UPDATE而不是DELETE。这样做的核心原因是校园招聘系统里存了大量用户投递记录和简历数据物理删除后一旦需要追溯历史数据就凉了。MyBatis-Plus内置了TableLogic注解配置好之后所有的select都会自动带上deleted0条件delete操作会自动转成update基本无感。2.4 MyBatis-Plus代码生成器和BaseMapper的威力表结构设计好之后如果用传统MyBatis每个实体类、Mapper接口、XML文件都要手写光是建表就够折腾。MyBatis-Plus提供了代码生成器只需要配置数据库连接和表名就能自动生成entity、mapper、service、controller全套代码。虽然生成的controller层比较粗糙但entity和mapper可以直接用能省掉大量机械工作。生成之后基础的CRUD完全不用写SQLBaseMapper提供了selectById、selectList、selectPage、insert、updateById、deleteById等方法。比如分页查询职位列表只需要PageJobPosition page new Page(current, size); LambdaQueryWrapperJobPosition wrapper new LambdaQueryWrapper(); wrapper.eq(JobPosition::getStatus, 1) .like(StringUtils.isNotBlank(city), JobPosition::getCity, city) .orderByDesc(JobPosition::getCreateTime); jobPositionMapper.selectPage(page, wrapper);LambdaQueryWrapper好处是类型安全写字段名不会手滑拼错而且支持条件拼接的懒操作比如城市参数为空时就不会拼上这个条件满足条件时才动态添加查询条件这对筛选类接口来说太重要了。3. 后端核心接口实现与业务逻辑细节3.1 登录认证与拦截器的实现方案校园招聘系统的登录认证我没有引入Spring Security这种重框架而是用了更轻量的方案JWT 自定义拦截器。JWT的无状态特性非常适合前后端分离后端不需要存session前端把token放在localStorage里请求时放在Authorization请求头里就行。JWT工具类核心代码大概是这样public class JwtUtil { private static final String SECRET campus-recruit-secret-key; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; public static String generateToken(Integer userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }生成token时我把userId和role放进token里这样后端从请求头拿到token后解析出用户身份直接在Service层用它做数据隔离。比如企业端只能查自己公司发布的职位学生端只能查自己的简历和投递记录这些限定条件从token中取userId后拼进查询条件就能有效防止越权访问。拦截器的实现是写一个HandlerInterceptor重写preHandle方法从请求头取token解析失败就返回401解析成功就把用户信息放到ThreadLocal里供后续使用。然后在WebMvcConfigurer里注册拦截器并配置拦截路径。这里要注意排除登录、注册、验证码等无需认证的接口不然前端一进来就全被拦住了。3.2 前端的Axios拦截器与Token管理后端接口设计好了前端怎么统一处理Token我在Vue3项目里封装了一个request.js基于Axios核心思路就是请求拦截器自动携带Token响应拦截器统一处理HTTP状态码和业务状态码。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 token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )有个细节值得说一下baseURL我设成/api然后在Vite的配置里加了代理开发环境下把/api开头的请求转发到后端的8080端口这样开发时完全不涉及跨域问题。生产部署时Nginx再把/api代理到后端服务前后端联调非常顺畅。这个模式我在多个项目里复用强烈推荐。3.3 投递简历的业务逻辑与状态流转投递简历是校园招聘系统的关键业务这个接口不能简单做一个insert就完事需要处理几个边界情况第一学生必须已经完善个人信息和简历否则提示先完善资料第二同一个职位不能重复投递需要校验是否已经存在投递记录第三投递成功后职位表的投递数量要加1。第二点很容易被忽略我最初开发时没加校验测试时连点两次投递按钮瞬间生成两条记录后来在前端禁用按钮的同时也在后端做了唯一性校验双重保险。投递记录的SQL是手写的因为需要关联简历表、职位表、企业表来返回一个完整的投递详情对象这个用MyBatis-Plus的BaseMapper做不了需要自定义Mapper方法加XML或者注解的方式Select(SELECT dr.*, jp.position_name, jp.salary_range, c.company_name, r.real_name, r.phone FROM delivery_record dr LEFT JOIN job_position jp ON dr.job_id jp.id LEFT JOIN company_info c ON jp.company_id c.id LEFT JOIN resume r ON dr.student_id r.student_id WHERE dr.student_id #{studentId} ORDER BY dr.create_time DESC) ListDeliveryVO selectDeliveryListByStudent(Param(studentId) Integer studentId);MyBatis-Plus虽然自带通用CRUD但一旦涉及多表联查还是要回到原生SQL的写法。这也是我为什么说MyBatis-Plus不是让你完全不写SQL而是让你能少写的地方绝不手写该写的地方也别硬撑。3.4 统计报表接口与数据看板管理员端的数据看板需要统计用户总数、学生数、企业数、职位总数、投递总数、待审核职位数等等。这些数据如果一个个count去查接口会非常慢而且代码啰嗦。我的做法是把常用统计SQL集中在StatsMapper里一次查询返回多个维度的数量Select(SELECT (SELECT COUNT(*) FROM sys_user WHERE role 0) AS studentCount, (SELECT COUNT(*) FROM sys_user WHERE role 1) AS companyCount, (SELECT COUNT(*) FROM job_position) AS jobCount, (SELECT COUNT(*) FROM delivery_record) AS deliveryCount FROM DUAL) MapString, Object selectOverviewStats();这里的SQL用的是标量子查询一次请求返回所有汇总指标MySQL执行同样数量的count查询耗时比多次连接查询快得多。当然后续如果想增加趋势图比如近七日投递量折线图就需要再写一个按日期分组的查询用DATE_FORMAT函数处理时间字段分组这个看需求即可。4. 前端Vue3页面开发与核心交互实现4.1 Vue3组合式API与Element Plus的配合前端选择Vue3最大的感受就是组合式API带来的代码组织方式的自由度。经历过Vue2的同学应该都有体会Options API里data、computed、methods分散在各个块里写一个复杂页面经常要上下滚来滚去。Vue3的script setup语法糖则可以把一段业务逻辑完整地放在一起代码阅读顺序就是执行顺序非常舒服。校园招聘系统的前端UI组件我选了Element Plus这是目前Vue3生态里最成熟的组件库。表格、表单、分页、弹窗、消息提示这些高频组件开箱即用开发效率极高。比如职位列表页核心逻辑就是template el-table :datajobList v-loadingloading el-table-column proppositionName label职位名称 / el-table-column propcompanyName label公司名称 / el-table-column propsalaryRange label薪资范围 / el-table-column propcity label工作城市 / el-table-column label操作 template #default{ row } el-button typeprimary link clickhandleApply(row)投递/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequeryParams.pageNum v-model:page-sizequeryParams.pageSize :totaltotal current-changefetchJobList / /template script setup import { ref, onMounted } from vue import { getJobList, applyJob } from /api/job import { ElMessage } from element-plus const jobList ref([]) const total ref(0) const loading ref(false) const queryParams ref({ pageNum: 1, pageSize: 10, city: , positionType: }) const fetchJobList async () { loading.value true try { const res await getJobList(queryParams.value) jobList.value res.data.records total.value res.data.total } finally { loading.value false } } const handleApply async (row) { await applyJob({ jobId: row.id }) ElMessage.success(投递成功) fetchJobList() } onMounted(fetchJobList) /scriptElement Plus的表单校验也很好用比如简历编辑页需要校验手机号格式、邮箱格式、必填项等只需要在el-form-item上配置rules再在提交时调用formRef.validate()即可。这个小功能看似简单但一个前后端分离项目里前端做基本格式校验能省下一大堆无谓的后端请求。4.2 多角色权限控制与动态路由校园招聘系统有三种角色如果只做登录不做权限控制那么学生登录后就能直接访问管理员页面显然是灾难。前端权限控制我采用了两层方案路由守卫 侧边栏动态渲染。路由守卫的核心逻辑是在router.beforeEach中检查当前用户是否已登录以及当前路由需要的角色与用户角色是否匹配router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userRole localStorage.getItem(userRole) if (to.path /login) { next() return } if (!token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(Number(userRole))) { next(/403) return } next() })这个方案简单实用不需要引入动态路由那种复杂方案对校园招聘系统这种角色固定、页面数量不多的场景来说完全够用。同时在侧边栏渲染时根据userRole过滤出当前角色可见的菜单项保证界面上不会出现灰色入口。4.3 招聘求职场景的交互体验优化校园招聘系统的用户体验很大程度上取决于列表页的筛选体验和状态反馈。我做了几个细节上的优化职位列表页提供城市、职位类别、薪资范围的多条件组合筛选筛选条件变化时自动重置页码为1投递按钮在投递成功之后置灰并显示“已投递”防止重复提交面试邀请会通过红色徽标提醒学生查看最新消息这些交互细节看起来简单但对提升整个项目完成度很有帮助。数据看板页面还用了ECharts来做可视化图表比如按行业统计职位数量、按月份统计投递趋势、企业排名Top10柱状图等。ECharts和Vue3结合虽然需要处理组件的销毁与重绘问题但其实不太复杂简单封装一个Chart组件在onMounted时初始化实例监听数据变化时调用setOption即可。5. 开发环境搭建与项目部署5.1 MySQL8.0安装与数据库初始化如果你本地还没有MySQL8.0这里给出一个最简单的安装流程以Windows为例。下载MySQL8.0安装包一路Next安装注意选择Server only即可安装完成后在配置界面设置root密码字符集选择utf8mb4。命令行连接mysql -u root -p然后创建数据库和账号CREATE DATABASE campus_recruit CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER campus% IDENTIFIED BY Campus123; GRANT ALL PRIVILEGES ON campus_recruit.* TO campus%; FLUSH PRIVILEGES;项目里附带的sql脚本直接source执行就行。生产环境部署时我建议用Docker安装MySQL8.0一条命令搞定docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e MYSQL_DATABASEcampus_recruit \ -e MYSQL_USERcampus \ -e MYSQL_PASSWORDCampus123 \ mysql:8.0MySQL8.0相比5.7性能、JSON支持、窗口函数等都有明显优势校园招聘系统里没有特别复杂的分析需求但JSON字段在存简历的技能标签时可太方便了直接用JSON类型存储查询时还能用JSON_CONTAINS过滤。5.2 SpringBoot与Vue3的本地联调配置后端启动前需要确认application.yml里的数据库连接和端口配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_recruit?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: campus password: Campus123 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个关键配置容易踩坑。第一个是serverTimezone必须指定Asia/Shanghai否则MySQL8.0连接时会报时区相关的异常。第二个是allowPublicKeyRetrievaltrue如果不加使用caching_sha2_password认证时可能出现Public Key Retrieval is not allowed的报错。前端启动前确认vite.config.js里的代理配置server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }然后分别执行npm install和mvn spring-boot:run前后端就可以联调了。5.3 前后端分离项目打包与服务器部署开发完成后打包部署是很多人感觉陌生的一步。后端打包很简单mvn clean package -DskipTests生成target目录下的jar包然后上传到服务器用java -jar命令启动即可。前端打包npm run build生成dist目录下的静态文件需要放到Nginx或者任意静态服务器里。生产环境的Nginx配置需要把前端路由的history模式和后端API代理都处理好server { listen 80; server_name your-domain.com; root /opt/campus-frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个很关键的细节try_files配置是为了解决Vue Router在history模式下刷新页面出现404的问题忽略这个配置的话你从首页跳转到子页面后一刷新页面就白屏了。我第一次部署时就被这个坑过排查了很久才发现是Nginx配置少了一行。6. 常见问题与排查技巧实录6.1 问题速查表实战开发过程中我遇到并解决了不少问题这里整理成一张速查表希望帮你少走弯路。问题现象可能原因排查思路与解决方案前端请求后端报跨域错误前端没有配置代理或者后端未开启跨域配置开发环境优先用Vite代理生产环境用Nginx反代保证前后端域名一致中文数据写入MySQL报错字符集不是utf8mb4检查建库语句和连接URL统一设置utf8mb4MyBatis-Plus分页不生效返回所有数据没有配置分页插件在config类中添加MybatisPlusInterceptor并注册PaginationInnerInterceptorMySQL8.0连接报Public Key Retrieval错误认证插件是caching_sha2_password在JDBC连接URL加上allowPublicKeyRetrievaltrue上传图片或文件后无法访问文件路径和静态资源映射未配置后端配置addResourceHandlers映射本地磁盘目录前端拼完整的访问URLVue3中直接修改数组length不更新页面响应式代理不监听length属性使用splice方法或整体重新赋值打包后刷新子路由404Nginx没有配置try_files添加location / { try_files $uri $uri/ /index.html; }JWT Token过期后前端仍展示登录状态前端未拦截401状态码在Axios响应拦截器中对401统一跳转登录页6.2 几个我印象深刻的Bug第一个是投递简历时分页插件失效的问题。一开始MyBatis-Plus分页查询返回的总记录数是0查了半天发现是因为没有配置PaginationInnerInterceptor。这也是MyBatis-Plus初学者最容易忽略的一步没有拦截器selectPage方法不会自动拼接LIMIT语句。配置很简单Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }第二个是Vue3中export default里写响应式数据的坑。Vue3的
返回列表