☰
基于SpringBoot+Vue的大学生就业服务平台系统设计与实现
2026/9/26 18:13:53 网站建设 项目流程

每年三到六月,校园里最热闹的除了招聘会就是辅导员群里来回传的Excel表:学生发一份简历到邮箱、企业HR在宣讲会现场收纸质简历、就业办统计就业率要手动汇总每一个班的数据。我在接到这个项目需求时第一反应就是——这个场景太适合做一套前后端分离的系统了。于是就有了这套基于SpringBoot+Vue的大学生就业服务平台管理系统,技术栈就是标题里写的那一串:Java、SpringBoot、MySQL、MyBatis,前端用Vue全家桶。系统的核心价值是把学生、企业、学校就业办三个角色拉到同一个平台上,让职位发布、简历投递、企业筛选、就业数据统计这些动作全部线上化。这套源码我已经整理成完整工程,包括初始化SQL、后端代码、前端页面三个部分,适合正在做毕业设计、或者想系统学习前后端分离项目全流程的同学。

整个项目做完之后我的感受是:就业平台这种业务,功能模块不算多,难点全在数据关系设计和流程状态的流转上。下面我把这套系统的设计思路、核心实现、跑通步骤以及我在开发过程中踩过的坑,按项目推进的顺序完整写出来。

1. 就业服务平台到底做什么:三类用户与一条完整业务链

1.1 三种角色的边界划分

系统的第一件事不是写代码,而是先把角色理清楚。大学生就业服务平台里天然存在三类人:学生、企业招聘人员、学校就业办管理员。这三类人的诉求完全不同,页面权限和数据范围也完全不同。

学生端关心的是:有什么岗位适合我、怎么投简历、投出去的简历进展到哪一步了。所以学生端的核心页面是职位检索、职位详情、简历投递、投递记录、职位收藏。

企业端关心的是:怎么把职位信息发布出去、收到了哪些简历、怎么筛选候选人。所以企业端的核心页面是职位管理(发布/下线/编辑)、收到的投递列表(查看简历、标记面试/录用/淘汰)。

管理员端关心的是:平台上的企业是否真实可信、职位信息是否合规、学生和企业的整体数据情况。所以管理员端核心功能是企业资质审核、职位内容审核、学生档案查看、以及简单的就业数据统计。

1.2 核心业务闭环

我把整个系统的业务流程画成一条直线:企业注册并提交资质 → 管理员审核通过 → 企业发布职位 → 学生搜索并查看职位 → 学生投递简历(可先收藏) → 企业在投递列表中查看简历 → 企业更新投递状态(已查看/邀约面试/录用/不合适) → 学生查看投递反馈。

这个闭环里有两个关键节点:第一,企业注册后不能直接发布职位,必须等管理员审核,这是平台内容安全的基础;第二,投递记录的状态只能由企业端修改,学生端只能查看,保证信息流的单向可控。

从实现角度看,这两个节点分别对应了状态字段设计和权限设计,在后面的数据库设计和后端鉴权章节我会展开讲。

1.3 项目功能清单

我整理一下这套系统实际落地的功能点,给你一个直观参考:

模块功能点说明
用户认证登录、注册、密码加密支持学生/企业/管理员三种角色
学生端职位搜索、职位详情、投递简历、收藏职位、投递记录职位可按关键词/城市/学历模糊筛选
企业端企业信息维护、职位发布、职位管理、收到的投递列表投递状态由企业维护
管理端企业资质审核、职位审核、学生档案列表、就业统计就业统计按学院/专业聚合
通用功能简历文件上传、个人信息维护简历存储路径在application.yml中配置

这个功能清单里,最花时间不是页面,而是投递状态的状态机变化、以及数据权限的隔离。前端页面再多也只是壳,真正值钱的是这些业务约束的落地。

2. 为什么是SpringBoot+Vue+MySQL+MyBatis这套组合:选型背后的判断

2.1 后端框架:SpringBoot和它的生态优势

很多人选型的时候会纠结用SpringBoot还是SSM(Spring+SpringMVC+MyBatis)甚至Spring Cloud。我的判断很直接:如果目标是一个可运行的完整项目,不是学习框架底层原理,SpringBoot就是最优解。

SpringBoot最大的价值是“约定大于配置”。一个就业服务平台需要的Web能力、数据源管理、事务控制、参数校验、日志输出,SpringBoot全都通过Starter自动装配好了。你要做的只是写业务代码,不用再去配置一堆XML文件。SSM的XML配置你花一天时间调通,SpringBoot五分钟就能跑起来,而且代码结构对后面要接手维护的人来说友好得多。

Spring Cloud对这个项目来说明显超重。就业服务平台没有高并发、没有复杂的分布式事务、没有多服务独立部署的需求,强行引入微服务只会把简单问题复杂化,光是Nacos注册中心、网关、OpenFeign那一套就能压垮一个小型毕设项目。

2.2 持久层:为什么选MyBatis而不是JPA或MyBatis-Plus

持久层我选的是MyBatis原生框架。理由有三点。

第一,MyBatis的SQL完全由开发者掌控。就业服务平台的查询场景很典型:职位列表需要按城市、学历、薪资多条件拼接查询;投递记录需要关联企业表、职位表、简历表做多表联查。这些场景如果写原生SQL,逻辑非常直观,调优空间也大。你用JPA的Specification或者QueryDSL去拼动态条件,写出来的代码可读性反而更差。

第二,MyBatis的Mapper机制天然适合这类管理系统。每个实体对应一个Mapper接口和XML文件,职责清晰,出了问题定位也快。像分页查询、模糊搜索这种高频操作,在XML里维护一套SQL模板,后面加条件只需要改片段。

第三,MyBatis的缓存机制和分页插件生态都很成熟。我在项目里用了PageHelper做分页,下一节会详细讲用法和注意事项。热门搜索词里很多人都在搜“mybatis分页插件的用法”,说明这块确实是高频需求,但也确实容易踩坑。

为什么不用MyBatis-Plus?倒不是说它不好——它确实能省掉大量单表CRUD的代码。但我始终觉得,做毕设或者入门项目,把MyBatis的原生SQL写法、关联映射、动态SQL这些基本功练扎实了,再去用增强工具才算真正理解它在做什么。而且我这份源码里保留了原生XML写法,你也可以对照着MyBatis-Plus的写法自己改造一遍,理解会更深刻。

2.3 前端方案:Vue + Vite + Element Plus

前端这一层,标题里写的是Vue。具体实现上我推荐用Vue3 + Vite + Element Plus这套组合。Vue3的组合式API在处理职位列表、投递状态这种多交互场景时,代码组织比Vue2的选项式API清晰很多。Vite的启动速度相比Webpack快一个量级,开发体验好太多了。

组件库选Element Plus,是因为它和Vue3是同一套生态,表格、表单、弹窗、分页这些管理系统最常用的组件都齐全,样式干净。你在网上看到的大多数管理系统模板也是基于它,遇到问题比较容易搜到答案。

前端这一层要特别注意环境配置。Vue项目能不能跑起来,一半的坑都在Node环境和依赖版本上。Node版本太高或太低都会导致依赖安装失败,npm镜像源没配好的话装依赖能卡到你怀疑人生。这些细节我在第6章的常见问题里会重点写。

2.4 数据库选型:MySQL在当前规模下的合理性

存储层用MySQL没什么悬念。就业服务平台的表量级撑死几十张,单表数据量在万级到十万级之间,MySQL 8.0在这个规模下性能和稳定性都游刃有余。更关键的是,MySQL的安装门槛低、文档多、面试常考,Java开发绕不开。

和MySQL绑定在一起的是一系列工程细节:字符集要用utf8mb4而不是utf8(否则存不了emoji和生僻字)、时区要配置好(否则时间字段差8小时)、数据库账号密码要单独建业务账号而不是直接用root。这些我都是在第6章专门列出来讲的,因为它们太容易被忽略,出了问题又很难一眼看出来。

3. 数据库设计:从用户表到投递记录表的建模细节

数据库设计是这套系统最核心的部分。我在建表之前先把实体关系理清楚,总共设计了八张核心表。下面我挑几张最有代表性的表展开讲设计思路。

3.1 用户体系:一张表还是多张表

用户体系我采用的是“单表+角色字段”方案。sys_user表统一存账号、密码、角色、状态,学生和企业的详细信息再分别用student_profile和company_info扩展。

CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `role` varchar(20) NOT NULL COMMENT '角色:STUDENT/COMPANY/ADMIN', `phone` varchar(20) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `status` tinyint NOT NULL DEFAULT '1' COMMENT '账号状态:1启用 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), KEY `idx_role` (`role`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

为什么用单表而不是三张独立的用户表?因为登录认证只关心账号密码和角色,如果分成三张表,登录时得先猜用户到底是哪个角色,再去对应的表里查,逻辑绕而且不好扩展。单表加角色字段的做法,登录只查一次,后续的业务扩展交给信息扩展表,这是目前常规做法,也是我推荐的做法。

密码字段长度我留到100,是因为后面用的BCrypt加密算法生成的密文是60个字符,如果一开始按32位设计,后面换加密方式就得改表结构。这种“留有余量”的设计习惯在真实项目里很重要。

3.2 职位表与投递表:关系建模的关键

职位表job_position是信息的中转站,字段设计重点在筛选条件和展示信息:

CREATE TABLE `job_position` ( `id` bigint NOT NULL AUTO_INCREMENT, `company_id` bigint NOT NULL COMMENT '所属企业ID', `title` varchar(100) NOT NULL COMMENT '职位名称', `category` varchar(50) DEFAULT NULL COMMENT '职位类别', `salary_min` int DEFAULT NULL COMMENT '最低薪资(K)', `salary_max` int DEFAULT NULL COMMENT '最高薪资(K)', `city` varchar(50) DEFAULT NULL COMMENT '工作城市', `education` varchar(20) DEFAULT NULL COMMENT '学历要求', `description` text COMMENT '职位描述', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1招聘中 0已下线', `view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_company_id` (`company_id`), KEY `idx_city_status` (`city`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位表';

这里有一个容易被忽略的设计点:城市和状态字段。前端职位列表的筛选条件90%是“城市+学历+关键词”,所以我在city和status上建了联合索引,查询性能会好很多。语句量级不大时可能看不出差别,但这是一个该有的好习惯。

投递记录表delivery_record是整个系统业务约束最密集的表:

CREATE TABLE `delivery_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_user_id` bigint NOT NULL COMMENT '学生用户ID', `job_id` bigint NOT NULL COMMENT '职位ID', `resume_id` bigint DEFAULT NULL COMMENT '投递时使用的简历ID', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待查看 1已查看 2邀约面试 3已录用 4不合适', `delivery_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_job` (`student_user_id`, `job_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投递记录表';

这张表最关键的设计是student_user_id和job_id的联合唯一索引。它从根本上挡住了重复投递:同一个学生对同一个职位,不管前端页面点了多少次提交按钮,数据库层面都只有一条记录。这个设计我在第4章讲投递接口时会再次提到,因为它是整个“防重复”机制的基石。

3.3 扩展表设计:简历和收藏

简历表resume_file存的是上传的附件信息,字段设置很简单:原始文件名、存储路径、文件大小、上传时间。这里有个经验:原始文件名和存储路径必须分开存,因为后端保存文件时一般会用UUID重命名,防止文件名冲突,同时又需要把原始文件名展示给企业HR看。

收藏表favorite_job也是一个典型的“用户+目标”关系表,核心同样是一对唯一索引:user_id + job_id。收藏和投递不同,收藏可以取消,所以不要做状态字段,取消直接删记录就行。这个地方如果加一个status字段去标记“已收藏/已取消”,反而会留下垃圾数据,简单删除才是更干净的做法。

从这套表结构你能看到一个问题:我全程没有建物理外键。所有表关联靠的都是逻辑外键——字段名带company_id、job_id,但不强制数据库级约束。原因是在真实业务里,物理外键会带来插入顺序的强约束、额外的索引开销,而且分库分表时外键会失效。用代码层面保证数据一致性,配合唯一索引做约束,是更符合实际工程习惯的做法。

4. 后端核心实现:JWT鉴权、PageHelper分页与投递事务

4.1 登录认证:为什么用JWT而不是Session

用户登录这块我选择的是JWT(JSON Web Token)方案,没有用传统的Session。两者本质区别在于:Session状态存在服务器内存,JWT状态存在客户端token里。对前后端分离的项目来说,JWT有几个天然优势——后端可以水平扩展(任何一台服务器都能验证同一个token)、跨域友好(不需要维护Session共享)、天然适合移动端和Web端共用一个认证体系。

实现上分为三个部分:

登录接口:接收用户名密码 → BCrypt校验密码 → 查询用户角色 → 生成JWT返回前端。

public String generateToken(User user) { // 使用jjwt生成token,payload里放userId和role return Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

拦截器:写一个JwtInterceptor实现HandlerInterceptor,在preHandle里从请求头Authorization拿token,解析失败或过期直接返回401。

放行规则:登录、注册、职位列表查询这些接口不需要token,但投递、收藏、审核、发布职位这些操作必须校验token且校验角色。拦截器里用PathMatcher做路径匹配,放行名单在配置类里维护。

我最开始写这版的时候把token校验逻辑塞在了每个Controller里,后来发现重复代码太多,而且容易漏。后来统一改成拦截器之后,整个代码清爽很多。现在源码里你看到的是拦截器版本。

4.2 MyBatis分页插件的用法和坑

职位列表是典型的分页场景,这里我用的是PageHelper。先说最基础也最容易出问题的用法:PageHelper一定要放在查询语句的前一行。

public PageInfo<JobVO> searchJobs(String keyword, String city, String education, int pageNum, int pageSize) { // PageHelper.startPage必须紧挨着mapper方法调用 PageHelper.startPage(pageNum, pageSize); List<JobVO> jobList = jobMapper.searchJobs(keyword, city, education); return new PageInfo<>(jobList); }

我见过太多人把PageHelper.startPage(pageNum, pageSize)写在一个提前执行的方法里,结果分页不生效,查出来的还是全量数据。原理解释一下:PageHelper通过MyBatis拦截器,对当前线程下一次执行的SQL做count查询和limit拼接。一旦中间隔着其他SQL操作,分页就会串到别的查询上,这是这个插件最大的坑。

第二个坑是多表联查时,如果结果集存在嵌套对象映射(比如一个职位包含企业信息),分页插件统计count时执行的SQL是改造后的count语句,碰到复杂的group by或者distinct可能会统计不准。解决方案是手写count语句,或者尽量保持查询结果扁平化——我在项目里使用VO对象收纳关联字段,而不是复杂的嵌套映射,就是为了规避这个问题。

第三个坑是分页参数的前端配合。前端传pageNum和pageSize,后端返回PageInfo对象,里面有list、total、pageNum、pages这些标准字段,前端直接绑定到表格组件上。这里避免自己造轮子,直接用PageInfo的标准结构,Element Plus的el-pagination组件能无缝对接。

4.3 投递接口:唯一索引加事务的双保险

投递简历这个接口是整个后端逻辑最值得仔细看的地方。三件事要同时成立:不能重复投递、简历必须存在、投递状态要正确初始化。

@Transactional public DeliverResult deliverResume(Long studentUserId, Long jobId, Long resumeId) { // 1. 校验职位是否还在招聘中 JobPosition job = jobPositionMapper.selectById(jobId); if (job == null || job.getStatus() == 0) { return DeliverResult.fail("职位不存在或已下线"); } // 2. 校验简历是否存在且归属于当前学生 ResumeFile resume = resumeFileMapper.selectById(resumeId); if (resume == null || !resume.getUserId().equals(studentUserId)) { return DeliverResult.fail("简历不存在"); } // 3. 插入投递记录,依靠唯一索引防止重复 // DuplicateKeyException出现时说明已经投递过 try { deliveryRecordMapper.insert(studentUserId, jobId, resumeId); } catch (DuplicateKeyException e) { return DeliverResult.fail("您已投递过该职位,请勿重复投递"); } // 4. 职位表的投递数加一(如果有该统计字段) jobPositionMapper.increaseDeliveryCount(jobId); return DeliverResult.success(); }

这个方法加@Transactional是因为第3步和第4步必须保持原子性——投递记录插入成功但投递数没加上,数据就不一致了。事务在这里是必须的,不能用“先干第一步再干第二步”的顺序图省事。

注意这里有个取舍:我先用代码校验了职位状态、简历归属,又用数据库唯一索引兜底重复投递。双层防护的原因在于,代码校验解决的是“业务上不允许”,唯一索引解决的是“并发情况下也不允许”。如果一个接口只做代码判断,两个并发请求同时通过校验,就会出现重复数据。数据库层的唯一约束是对并发场景的最后一道防线,这也是我反复强调“业务系统的约束不能只写在Service层”的原因。

4.4 关键配置:数据源、MyBatis与文件上传

application.yml里几个关键配置我直接给出来:

spring: datasource: url: jdbc:mysql://localhost:3306/job_platform?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.jobplatform.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-dir: ./upload

map-underscore-to-camel-case一定要开,数据库的create_time才能自动映射到Java实体里的createTime,不然每个字段都要写专门的结果映射。log-impl配成StdOutImpl能把SQL和执行参数打印到控制台,排查问题时简直救命。

文件上传路径这个配置看着简单,实际上很容易踩坑。如果配置成相对路径./upload,后端用IDE运行时文件会生成在工作目录;打成jar包运行时,文件会生成在jar包所在目录的旁边。所以到了部署阶段,我建议直接配置成绝对路径,比如/data/job-platform/upload,让运维同学好管理,也避免磁盘被撑爆了找不到文件在哪。

5. 前端Vue实现:路由守卫、Axios封装与页面组件划分

5.1 环境搭建:Node、Vite与依赖安装

前端这套我用的是Vue3 + Vite + Element Plus。先把环境准备说清楚,因为“vue安装及环境配置”是互联网上被搜索最多的关键词之一,可见大部分人第一次都会卡在这一步。

推荐版本组合:Node.js 16.x或18.x,npm 8+,Vite 4.x。如果Node版本装成了20+,个别旧依赖可能会有兼容问题,所以我更推荐LTS版本。装完Node之后,强烈建议先把npm镜像切到国内镜像源,不然装Electron或者node-sass这类带二进制文件的依赖时会非常痛苦。

创建项目推荐直接用Vite官方脚手架:

npm create vite@latest job-platform-frontend -- --template vue cd job-platform-frontend npm install npm install element-plus axios vue-router pinia npm run dev

装依赖的时候注意卡住的情况:如果某个依赖反复安装失败,优先查Node版本和镜像源,不要盲目重装。等npm run dev跑起来浏览器能访问5173端口,环境就算通了。

5.2 路由与路由守卫:角色权限的前端防线

前端路由是这套系统权限体系的一个重要环节。我按照角色划分路由模块:

  • 公共路由:/login、/register
  • 学生路由:/student/jobs(职位广场)、/student/deliveries(投递记录)、/student/favorites(收藏列表)
  • 企业路由:/company/jobs(职位管理)、/company/deliveries(收到的投递)
  • 管理员路由:/admin/companies(企业审核)、/admin/students(学生档案)、/admin/stats(就业统计)

每个路由的meta里带上roles字段,比如meta: { roles: ['STUDENT'] }。全局路由守卫里读token、解析token里的角色、对比路由要求,不符合就跳转到登录页。这段对应热词里“vue路由参数”的实际工程用法——路由不只是跳页面,还是权限控制的载体。

需要说明的是,路由守卫只是前端体验层面的拦截,真正必须守住的是后端接口的角色校验。前端路由隐藏某个入口,不代表用户不能通过URL直接访问,所以后端拦截器不能少。

5.3 Axios封装:token注入与统一错误处理

Axios封装是前端工程化里标配的动作。我维护了一个request.js,做了三件事:

请求拦截器:从localStorage拿token,有就放到请求头Authorization: Bearer xxx。

响应拦截器:统一处理业务状态码。后端接口我约束统一返回{ code: 200, message: 'ok', data: ... }结构,所以响应拦截器里非200的code直接弹错误提示,HTTP 401时清掉本地token并跳转登录页。

// request.js 核心代码 service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( res => { const { code, message, data } = res.data if (code === 200) return data // 对401特殊处理:清除登录态,跳转登录页 ElMessage.error(message) return Promise.reject(new Error(message)) }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

这套封装的好处是:所有页面组件里只需要关心业务数据,token管理和错误处理都在一个地方收敛,不然几十个页面页面重复处理401逻辑,想想都头大。

5.4 页面组件划分与跨域联调

前端页面我按“通用 + 角色”来组织组件。通用的有职位搜索栏、职位卡片、分页组件;学生端有职位列表页、投递确认弹窗、收藏按钮;企业端最复杂的是职位发布表单和投递列表的操作列(查看简历、标记状态)。

联调阶段最烦人的是跨域。Vite开发服务器的默认端口是5173,后端接口在8080,直接请求会被浏览器的同源策略拦掉。解决方案是在vite.config.js里配代理:

server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

前端所有请求都发到/api/xxx,Vite在开发阶段把请求代理到后端。页面代码里不用写全路径,也解决了跨域问题。至于生产环境,前端打包成静态文件后交给Nginx托管,再在Nginx配置里把/api反向代理到后端服务即可。这里我提一点实战体会:开发和生产的跨域方案要分开处理,不要在图省事用一个前端代理方案硬顶生产环境。

6. 从源码到能跑:环境版本、启动顺序与六个常见坑

6.1 推荐的版本组合

版本匹配是这个项目能不能一键跑起来的决定因素。我在源码的README里写了我验证过的版本组合,这里再强调一遍:

组件推荐版本说明
JDK1.8 或 11SpringBoot 2.7兼容性好
Maven3.6+不低于3.5
SpringBoot2.7.x配JDK8最稳
MySQL8.0驱动类名注意是com.mysql.cj.jdbc.Driver
Node.js16.x 或 18.xLTS版本
Vue/ViteVue3 + Vite4配合Element Plus

SpringBoot 2.7 + JDK8的组合当前依然是最稳的。SpringBoot 3.0要求JDK17起步,如果你本机没有JDK17,没必要为了追新去折腾版本,毕设评审看的是系统设计和代码逻辑,不是版本号。

6.2 完整启动步骤

第一步,建数据库。用Navicat或命令行执行job_platform.sql,这个文件会把八张表全建好,并且插入两个企业账号和几个学生账号的测试数据。

第二步,改后端配置。打开application.yml,把数据库密码改成自己的,文件上传路径改成你喜欢的位置。

第三步,启动后端。IDEA里直接运行JobPlatformApplication主类,或者用Maven命令mvn spring-boot:run。启动日志里看到Started JobPlatformApplication就说明成功了。

第四步,启动前端。进入前端目录,执行npm install,然后npm run dev,浏览器打开http://localhost:5173。

第五步,用测试账号登录。源码的初始化SQL里内置了三个测试账号:管理员admin/123456、企业company01/123456、学生student01/123456。登录后按角色操作一遍:学生搜职位并投递 → 切到企业账号查看投递 → 切到管理端审核企业,整个链路就算通了。

6.3 六个典型的启动期问题

把整个开发周期里反馈最多的问题集中列一下,每一个我都给解决思路。

问题一:MySQL连接报错Public Key Retrieval is not allowed。这是MySQL 8的驱动校验机制导致的,解决办法是在数据库连接URL上加allowPublicKeyRetrieval=true。

问题二:数据库时间字段差8小时。原因通常是连接URL缺少时区参数,把serverTimezone=Asia/Shanghai加上,并且确保MySQL服务器的时区也是东八区。

问题三:前端依赖安装卡住或报错。优先检查npm镜像源、Node版本和网络。另外npm install时提示权限错误,就检查是不是用了root权限运行npm,Windows下则是以管理员身份运行终端。

问题四:MyBatis的XML里SQL报错但控制台看不到SQL。检查mybatis.configuration.log-impl是否配置成了StdOutImpl,配置后所有SQL和参数都会打到控制台。这是“mybatis配置打印”最直接的做法。

问题五:分页不生效,查出来全是第一页的数据。大概率是PageHelper.startPage和真正的查询语句之间隔着其他SQL。务必把startPage放在mapper调用前一行。

问题六:有人问我“怎么将springboot jar反编译成项目”。这里多说一句,如果是你自己写的源码,没必要去做反编译这种操作,直接把源码工程在新环境里跑通就好。反编译只能还原业务逻辑参考,还原不了工程结构和全部注释,与其纠结反编译,不如维护好源码工程。如果是接手别人的jar又没源码,建议先用文档把接口行为梳理清楚,再根据行为重建工程,比逐行反编译更高效。

6.4 部署:后端jar包与前端静态文件

开发环境跑通之后,部署其实很简单。后端在项目根目录执行mvn clean package,拿到target目录下的job-platform-0.0.1-SNAPSHOT.jar,然后在服务器上执行:

java -jar job-platform-0.0.1-SNAPSHOT.jar

前端执行npm run build,生成的dist目录里的静态文件交给Nginx托管。Nginx配置里加一段反向代理,把/api转发到后端端口:

server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

有一个部署层面的小经验:后端接口不要裸奔在公网端口上,用Nginx代理转发可以顺带做一层基础防护。文件上传路径在部署时记得改成服务器上的绝对路径,不然文件散落各处,后面清理维护都是问题。

7. 毕设项目到生产项目:这个架构还能怎么补强

7.1 当前架构的几个短板

这套系统做到能跑、业务流程完整没问题,但离“生产可用”还有一段距离。最明显的问题是:职位搜索用的SQL模糊查询,在数据量过万之后性能会明显下滑;简历文件存在本地磁盘,服务器重启或迁移时文件管理容易出问题;没有引入缓存,高频访问的职位详情和热门职位每次都查数据库,压力大时扛不住。

如果是作为毕设,这些短板老师们能接受。但如果你想认真往简历上写这个项目,强烈建议补上几个点:用Redis缓存职位热门列表和用户登录状态;把简历文件存储切换成对象存储方案(比如MinIO或阿里云OSS);搜索场景引入Elasticsearch或者MySQL全文索引;后台管理引入独立的统计表和定时任务。

7.2 面试和答辩环节高频追问的技术点

这套项目如果拿去做毕业设计答辩,或者写进简历找Java开发工作,有几个技术问题是几乎一定会被问到的,提前把自己的答案准备好:

  • 为什么用JWT而不用Session?如果非要Session方案,多服务器部署时怎么解决Session共享?
  • 投递记录的重复投递问题是怎么防止的?如果去掉唯一索引,只用代码判断,会出现什么情况?
  • PageHelper分页的原理是什么?为什么startPage必须放在查询前一行?
  • BCrypt加密相比MD5加盐有什么优势?
  • 前端路由权限和后端接口权限的关系是什么?只做前端拦截有什么安全风险?

这些问题核心都在考“你是不是真的理解了项目里的技术决策”。能把这些设计决策讲清楚,比堆砌功能列表要有说服力得多。这也是我为什么在上面的章节里反复强调设计理由——不仅是为了实现功能,更是为了让项目经得起追问。

7.3 我的实操体会

整套系统从数据库设计到前后端联调完成,我最大的体会是:就业平台这种管理系统,真正的复杂度不在技术本身,而在“数据的约束关系”。一个投递动作背后连着职位状态、简历归属、重复投递、状态流转、统计字段更新,任何一个约束漏掉,系统在测试阶段就会给你端上一堆奇怪的问题。

另外一个经验是,前后端联调阶段一定要先把接口的返回结构统一约定好。我在项目开始前就定了{ code, message, data }这样的统一响应格式,前端Axios封装和后端全局异常处理都围绕它设计,后期的磨合成本低很多。如果后端这个接口返回一个Map、那个接口返回一个Object,前端写起来会非常痛苦。

还有一点想提醒:源码里的测试数据非常有用,别删。初始化SQL里预置的企业账号对应着已审核通过的企业记录,这样登录后能直接体验“发布职位→学生投递→企业筛选”的完整流程;如果删了再注册新企业,就会被卡在企业资质审核状态,容易误认为是系统Bug。测试数据本身就是你调试和演示的最佳工具。

如果你正好在找一套结构清晰、能跑得通的Java就业平台项目作为参考,这套SpringBoot+Vue的完整源码可以直接基于它改造成你自己的课设或毕设,按第六章的步骤跑通原版,再替换成你的学校名、专业方向、自定义功能,会比从零开始写省下大量时间。

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

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

立即咨询