Spring Boot + Vue 求职招聘平台全栈开发实战指南 先抛一个我的结论用 Spring Boot Vue 做求职招聘平台不是跟风而是这套组合能把全栈开发里真正难啃的部分一次串起来。求职招聘平台不只是一个 CRUD 壳子它同时涉及用户权限、角色分层、复杂状态流转、文件上传、消息通知、数据检索这些在实际工作中绕不开的模块。我前后花三周左右把这个项目从零到一完整落地从数据库建模、后端接口设计、前端页面拆解到联调部署踩坑整个过程走完你会发现自己对前后端协作的理解完全不一样了。这个平台按业务角色划分包含求职者、企业HR、平台管理员三端。求职者可以注册登录、完善简历、浏览职位、条件筛选、投递简历、收藏职位企业端可以注册入驻、发布职位、上下架管理、处理收到的简历管理员则负责企业认证审核、职位内容审核、用户管理等。文章按我的实际开发顺序展开适合正在做毕业设计、或者想用真实项目补齐全栈经验的开发者参考。下面所有代码片段和表结构都是可以直接照抄落地的。1. 项目立项为什么这个业务模型值得做1.1 一个平台背后的完整业务闭环很多人选项目喜欢做简单的博客系统或者商城说实话这类项目对后端的考察太浅了。求职招聘平台不一样它的业务链路天然就要靠多个角色协作才能走通。一个求职者从注册、编辑简历到搜索职位、投递再到企业查看简历、调整状态最后消息推送给求职者这一套流程本身就包含真实平台上最常见的用户行为闭环。和上门烹饪预约、婚庆服务预约这类预约服务系统对比求职招聘平台的底层业务结构其实是同一类都有用户的身份划分、都有核心资源发布与审核、都有双方的意愿匹配动作。把其中一个吃透其他类似项目几乎是平移。从实际开发角度看这个业务模型覆盖了这些关键需求多角色权限体系普通用户、企业用户、管理员接口层面要做访问控制复杂查询场景职位列表按城市、薪资、类别、关键词筛选还要分页核心状态变更投递简历后状态从待处理变成通过或拒绝状态变化要即时可见异步通知机制投递状态更新后求职者需要收到提醒附件与富文本简历支持上传附件职位描述需要富文本编辑后台审核管理企业发布职位后管理员还需要审核防止垃圾内容这些点不是额外加的而是这个业务天然要求的。你做完之后和别人聊项目随便抓一个点都能往深了说。1.2 技术选型的真实理由Spring Boot Vue 的搭配逻辑后端选 Spring Boot核心是生态成熟、上手效率高。求职招聘平台这种以 CRUD 为基础、夹杂业务状态流转的系统Spring Boot 的自动配置、Starter 机制、丰富的官方文档能省掉大量重复的基建工作。我自己习惯配合 MyBatis-Plus它提供了单表 CRUD 的现成方法分页插件也很方便项目里的用户表、职位表这类简单表操作基本不需要手写 XML。复杂一点的查询再单独写 SQL灵活性和开发效率平衡得比较好。前端的 Vue 我选择 Vue 2 Element UI 的组合不是说 Vue 3 不好而是这个项目开发周期短Element UI 的表格、表单、弹窗、上传组件可以直接套用快速把页面撑起来。如果你是新项目直接用 Vue 3 Element Plus 也完全没问题核心逻辑是一样的。技术栈清单大致如下端技术说明后端Spring Boot 2.7.x核心框架后端Spring Security JWT认证与接口鉴权后端MyBatis-Plus 3.5.xORM 与分页后端MySQL 8.0业务数据存储后端Redis 邮件/短信验证码与缓存后端WebSocket站内消息通知前端Vue 2 Element UI页面与组件库前端Vue Router Vuex路由与状态管理前端AxiosHTTP 请求封装部署Nginx Docker前端静态资源与后端容器化这套选型的理由后面会在具体模块里继续展开。比如 Spring Security 已经内置了 BCrypt 密码加密不需要自己写加密工具类WebSocket 的引入是为了避免前端轮询接口导致服务器压力变大。2. 数据库设计三端用户、职位、简历怎么组织2.1 核心表结构与字段设计数据库是整个项目的底座我花了大概两天时间反复调整表结构因为前期设计不合理后面写接口和前端页面时会非常痛苦。项目最终核心表包括用户表、企业表、简历表、职位表、投递记录表、收藏表、消息通知表。用户表是最基础的一张表同时也是权限控制的核心。这里注意不要给每个角色单独建表而是用 role 字段区分这样后端权限拦截和前端路由守卫处理起来都统一。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, role VARCHAR(20) NOT NULL COMMENT JOBSEEKER 求职者 / COMPANY 企业 / ADMIN 管理员, avatar VARCHAR(255) COMMENT 头像地址, phone VARCHAR(20), email VARCHAR(100), status TINYINT DEFAULT 1 COMMENT 账号状态 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );企业表保存企业的基本信息同时通过 user_id 关联到用户表这样企业账号登录后可以拉取自己的企业资料。企业认证字段的设计要特别留意我加了一个 audit_status用来记录企业资料的认证状态。为什么要单独做认证因为招聘平台的职位发布是有门槛的没有认证的企业不能发职位这个逻辑必须有字段支撑。CREATE TABLE company ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 关联 sys_user.id, company_name VARCHAR(100) NOT NULL, credit_code VARCHAR(50) COMMENT 统一社会信用代码, industry VARCHAR(50) COMMENT 行业领域, scale VARCHAR(50) COMMENT 公司规模, address VARCHAR(255), license_url VARCHAR(255) COMMENT 营业执照图片, audit_status TINYINT DEFAULT 0 COMMENT 0未认证 1审核中 2已认证 3已驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );简历表我设计成和用户表一对一关联。这里有个细节简历不是注册就自动生成的而是用户第一次进入我的简历页面时通过接口初始化否则每个注册用户都会多一条空记录白白占空间。简历字段我拆成了两大部分普通字段用数据库列技能标签、工作经历、教育经历这类有嵌套结构的数据用 JSON 存。MySQL 5.7 支持 JSON 类型查询时用 JSON_EXTRACT 也可以处理开发成本低很多。2.2 状态字段设计的几个关键取舍这个项目里到处是状态每个状态我都用了 TINYINT 数字类型。比如职位表有一个 status 字段0 表示下架1 表示招聘中2 表示待审核。用户表 status 控制账号是否禁用。投递记录表则设计了 application_status0 待处理1 已通过2 已拒绝。用数字而不是字符串很多人觉得可读性差但实际查询和索引效率更好业务代码里只需要定义常量或者枚举就行。我在 Java 中定义了一个枚举类 ApplicationStatusEnum前端则用字典映射来显示对应中文。这样做的好处是数据库里不会出现各种五花八门的拼写错误状态流转逻辑都集中在同一个地方维护。另外一个关键设计是职位上下架状态和审核状态分开存储避免用一个字段表达两种意思。比如一个职位被管理员驳回后企业端要能看到审核失败的原因并编辑后重新提交这个流程如果只有一个状态字段逻辑会变得难以处理。我的做法是职位表里有两个字段audit_status 负责审核链路status 负责最终的上下架展示状态。3. 后端核心逻辑鉴权、检索、投递状态机与消息推送3.1 登录注册与角色权限控制登录认证我选的是 JWT Spring Security 的方案。JWT 无状态、适合前后端分离场景客户端拿到 Token 后存储到 localStorage每次请求在 Authorization 请求头中携带后端通过过滤器统一校验。相比 Session 方案不需要在 Redis 里维护会话部署多实例时也天然支持横向扩展。注册逻辑相对简单但密码必须用 BCryptPasswordEncoder 加密存储明文密码在任何情况下都不应该落库。用户注册时选择角色如果是求职者角色直接注册成功如果是企业角色则需要完善企业信息并提交认证认证通过后才能发布职位。注册接口里还需要处理用户名唯一性校验捕获数据库的唯一索引冲突异常返回给前端友好的提示。权限控制用 Spring Security 的注解方式在 Controller 方法上添加权限校验PreAuthorize(hasRole(COMPANY)) PostMapping(/jobs) public ResultVoid createJob(RequestBody JobPosition job) { // 只有企业角色可以发布职位 } PreAuthorize(hasRole(ADMIN)) PutMapping(/jobs/{id}/audit) public ResultVoid auditJob(PathVariable Long id, RequestBody AuditRequest request) { // 只有管理员可以审核职位 }这里要注意使用 PreAuthorize 之前必须开启方法级安全配置在启动类或配置类上加上 EnableGlobalMethodSecurity(prePostEnabled true)。3.2 职位发布与检索接口的实现细节职位发布是企业的核心操作。企业用户登录后先判断认证状态只有认证通过的企业才能调用发布接口。职位信息包括职位名称、所属行业、城市、薪资范围、学历要求、经验要求、职位描述等。职位描述用富文本编辑器输入前端提交 HTML 字符串后端存储时要做好 XSS 过滤否则用户可以在内容里注入恶意脚本这一点很多项目都会忽略。职位列表的检索接口是前端压力最大的接口因为列表页要支持多种筛选条件。我的查询条件是动态拼接的MyBatis-Plus 可以通过 LambdaQueryWrapper 实现链式条件也可以直接用 XML 的if标签动态 SQLSELECT * FROM job_position WHERE audit_status 2 AND status 1 if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR company_name LIKE CONCAT(%, #{keyword}, %)) /if if testcity ! null and city ! AND city #{city} /if if testcategory ! null and category ! AND category #{category} /if ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}这里有个小优化如果职位表数据量大LIKE 模糊查询会走全表扫描。对于个人项目或者小规模内网部署这个方案没问题但如果未来要承载真实业务建议引入 Elasticsearch 做职位检索这是一个可扩展的方向。我在项目里用的是 MySQL 方案数据量在几千条量级时响应速度完全可以接受。3.3 投递、收藏与消息通知怎么串起来投递逻辑是整个项目里最有业务感的地方。求职者点击投递简历按钮后后端先查用户是否已经完善简历简历不完整直接返回提示再查同一用户是否已经投递过这个职位避免重复投递最后写入投递记录同时维护一个冗余的投递数量字段方便职位列表展示热度。投递状态流转我用了一个简单清晰的常量定义public enum ApplicationStatusEnum { PENDING(0, 待处理), ACCEPTED(1, 已通过), REJECTED(2, 已拒绝); }企业 HR 进入收到的简历列表后可以查看每个求职者投递的简历详情然后选择通过或拒绝。HR 做出操作后系统需要给求职者发送一条站内消息。这里有两个方案一个是前端定时轮询消息接口简单但浪费资源另一个是 WebSocket 长连接推送。我在项目里选择了 WebSocket因为招聘平台的投递状态变化是低频但要求及时性的场景长连接体验更好。WebSocket 的接入并不复杂核心就是建立连接时把用户 ID 作为身份标识后端在状态变更后向指定用户推送消息Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MessageHandler(), /ws) .setAllowedOrigins(*); } }需要注意一个实战细节WebSocket 握手时需要校验 Token。我在拦截器里从 URL 参数获取 Token 并解析用户信息解析成功才放行否则直接拒绝连接。网页端在登录成功后用当前用户的 ID 拼成 WebSocket 地址建立连接服务端用一个 ConcurrentHashMap 维护 userId - WebSocketSession 的映射。用户退出登录或刷新页面前主动关闭连接避免连接泄漏。4. 前端 Vue 工程化落地路由、状态管理与核心页面拆解4.1 前端环境配置与工程结构前端从 Vue 环境配置开始我用 Vue CLI 创建项目。安装依赖的时候注意Element UI 和 Vue 2 的版本要匹配Element Plus 只支持 Vue 3。项目结构我按业务模块组织而不是单纯按文件类型堆在一起src/ ├── api/ # 接口请求定义 │ ├── auth.js │ ├── job.js │ └── resume.js ├── assets/ ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex 状态管理 ├── views/ # 页面组件 │ ├── job/ # 职位相关页面 │ ├── company/ # 企业管理相关页面 │ ├── user/ # 个人中心相关页面 │ └── admin/ # 后台管理页面 └── utils/ # 工具封装路由设计上我把页面分为公共页面、需要登录的页面、需要特定角色的页面。登录、注册、职位浏览列表是公共的简历编辑、投递记录、企业管理后台需要登录。实现方式是 Vue Router 的 meta 字段 全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.meta.role to.meta.role ! store.state.user.role) { next(/403) } else { next() } })这个守卫逻辑解决了两个问题未登录用户不能访问需要身份才能看的页面角色不匹配的用户跳转到无权限提示页。另外路由懒加载用component: () import(/views/job/JobDetail.vue)避免首屏加载时间过长。4.2 Axios 封装与 Token 处理前端所有接口请求都走一个统一的 Axios 实例。这里有两个地方必须处理请求拦截器自动附加 Token响应拦截器统一处理业务错误码和 HTTP 错误码。const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 业务错误统一提示 Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { // Token 失效清除登录状态并跳转登录页 localStorage.removeItem(token) router.push(/login) } Message.error(error.message || 请求失败) return Promise.reject(error) } )后端接口统一返回 Result 结构体包含 code、message、data 三个字段。这样前端响应拦截器可以集中处理所有接口的返回逻辑不用每个页面都做重复判断。4.3 核心页面的实现思路职位列表页是整个前端最复杂的页面。顶部是搜索栏支持关键词、城市、类别、薪资范围筛选中间是职位卡片列表右侧是热门职位排行。筛选条件变化时重新请求接口这里要注意避免每次输入关键词都发请求输入框用防抖函数处理我设置的是 500 毫秒。职位详情页除了展示基本信息外还需要根据当前用户角色显示不同操作按钮求职者看到立即投递和收藏职位企业登录后如果这个职位是自己发布的则显示编辑和上下架按钮。这些判断在页面里通过 Vuex 中的 userRole 和职位数据的 companyId 做比较即可。我的投递页面用 el-table 展示投递记录状态列是核心我会把状态映射逻辑抽成一个公共工具函数export function formatApplicationStatus(status) { const map { 0: { text: 待处理, type: warning }, 1: { text: 已通过, type: success }, 2: { text: 已拒绝, type: danger } } return map[status] || { text: 未知, type: info } }这样页面模板里直接调用函数即可不用在每个组件里重复维护状态映射。企业管理后台的核心是职位管理表格支持分页、关键字搜索、上/下架操作。管理员后台还需要一个职位审核页面通过表格展示待审核职位点击查看详情后选择通过或驳回驳回时要填写原因。5. 联调、部署与避坑记录从 CORS 到接口文档兼容性5.1 前后端联调时最常见的几个问题前后端分离开发时第一个拦路虎必然是跨域问题。开发环境我用了 Vue CLI 的代理配置在 vue.config.js 里把/api前缀的请求代理到后端服务地址这样前端代码里不需要写完整的后端地址也规避了开发环境的 CORSmodule.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }生产环境则由 Nginx 统一处理反向代理前端静态资源和后端接口走同一个域名同样没有跨域问题。如果你在联调时非得开启跨域后端可以在配置类里实现 WebMvcConfigurer 的 addCorsMappings 方法但我不推荐生产环境用 Nginx 更优雅。第二个常见问题是时间格式。后端返回的 LocalDateTime 默认是一串带 T 的 ISO 字符串前端直接显示不友好。我的处理方式是在 application.yml 里配置全局的 JSON 序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个问题是文件上传返回的 URL 路径。本地开发时上传的图片存储在后端磁盘目录访问时需要一个固定前缀映射。我做了静态资源映射配置把 /upload/** 映射到本地目录生产环境则把上传目录挂载到 Docker 卷保证容器重启后文件不丢。5.2 部署上线Nginx 反向代理与 Docker 容器化部署我采用 Docker Nginx 的方式。前端项目先执行npm run build生成 dist 目录然后把 dist 目录挂载到 Nginx 容器的 /usr/share/nginx/html。后端项目用 Maven 打包成 jar 包写一个简单的 DockerfileFROM openjdk:8-jdk-alpine COPY target/job-platform.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]Nginx 的配置核心是 location 转发server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend-container:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个关键点try_files $uri $uri/ /index.html必须写否则刷新 /job/123 这样的前端路由页面时会直接 404。后端 API 的 location 要注意 proxy_pass 结尾是否带斜杠不带斜杠会把完整的/api路径透传给后端带了斜杠则会把/api前缀去掉两种方式要和后端的服务路径对应起来。5.3 springfox 3.0.0 与 Spring Boot 2.6 的兼容性坑这个坑是后端接入接口文档时踩的。项目里我用了 Springfox 3.0.0 生成 Swagger 接口文档结果 Spring Boot 版本是 2.7启动时直接报错Failed to start bean documentationPluginsBootstrapper; nested exception is java.lang.NullPointerException原因是 Spring Boot 2.6 之后默认的路径匹配策略从 AntPathMatcher 改成了 PathPatternParser而 Springfox 3.0.0 没有适配。解决办法是在 application.yml 里配置spring: mvc: pathmatch: matching-strategy: ant_path_matcher这个配置加上之后文档功能恢复正常。另外提醒一下如果是从零开始的新项目可以直接改用 springdoc-openapi它是 OpenAPI 3 规范对 Spring Boot 2.6 的兼容性更好不需要做这种额外配置。联调部署过程中另外一个让我印象深刻的坑是 WebSocket 在 Nginx 转发时连接不稳定。WebSocket 协议升级需要 Nginx 配置 Upgrade 头如果不加浏览器端看到的现象是连接建立成功后被立即断开location /ws/ { proxy_pass http://backend-container:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }这个配置容易被忽略但少了它所有基于 WebSocket 的消息推送功能在生产环境就全部失效。开发环境因为是直连后端不会有问题测试时一定要记得验证部署环境下的长连接场景。根据我个人经验这类前后端分离项目最耗时间的其实不是写接口而是联调阶段的细节对齐。状态码的语义、字段返回格式、异常提示信息这些在一开始就要在前端对应好。做完整个求职招聘平台之后我最大的感受是把业务状态流转梳理清楚整条链路的开发就会顺畅很多后续就算换成别的技术栈这套理解也能直接复用。