毕业设计做到校园招聘这个方向,很多同学第一反应就是"不就是个招聘网站吗"。但实际上,我们把"一网寻职"这套基于SpringBoot+Vue的校园学生网完整做下来之后发现,它远不止是简单的前后端CRUD,而是一个涉及多角色权限、复杂业务流转、文件处理、消息推送的综合系统。这篇文章我会从项目定位讲起,把数据库设计、后端核心API、前端页面交互、以及我实际开发中踩过的坑全部摊开来说,希望能给正在做同类选题的同学一份可以直接参考的实操手册。
1. 项目背景与整体定位
1.1 为什么选择"校园招聘求职一体化"这个方向
每年毕业季,大量校园招聘信息分散在学院群、辅导员朋友圈、各类招聘APP里,学生获取信息的方式极其零散。而对学校就业指导中心来说,缺少一个统一的平台去统计毕业生的就业去向、企业岗位匹配度。企业HR想进校宣讲,也只能靠邮件往来,效率非常低。
"一网寻职"要解决的就是这个问题:把原本分散的就业信息整合到一个平台上,让学校、学生、企业三方在同一个系统里完成信息对接。学生的核心需求是看岗位、投简历、查进度;企业的核心需求是发职位、收简历、筛人才;学校管理端的核心需求是审核企业资质、查看就业数据。整个系统的业务主线清晰,功能边界明确,适合作为毕业设计的完整题目。
1.2 技术选型背后的考量
项目选用了SpringBoot作为后端框架,Vue作为前端框架,这个组合在目前的毕业设计里几乎属于"标准答案"。但我个人觉得,选择它们不只是因为主流,更重要的是它们各自解决了这个项目里的真实痛点。
先看SpringBoot。校园招聘平台涉及的实体很多——学生、企业、职位、简历、投递记录、新闻公告,每一块都需要写增删改查接口。如果用传统的SSH或Servlet开发,光配置文件和繁琐的模板代码就够折腾。SpringBoot的自动配置机制把大量样板配置省掉了,我一个spring-boot-starter-web依赖导入,内置Tomcat直接跑起来,能把精力集中在业务逻辑本身。
再看Vue。招聘平台的用户界面是典型的信息密集型页面:职位列表、筛选条件、投递状态标签、弹窗确认。Vue的双向数据绑定和组件化开发,让我把职位卡片、分页器、状态标签这些UI片段封装成独立组件,页面代码的复用率提升明显。而且vue-router做页面跳转、axios做数据请求,整个前后端联调的过程非常顺畅。
2. 系统角色与功能模块拆解
2.1 三类核心角色与业务闭环
系统设计我一开始就确定了三种角色,因为招聘平台的业务逻辑天然是三角关系:
- 学生端:注册登录、完善个人简历、浏览招聘信息、投递简历、查看投递进度、收藏职位。
- 企业端:企业信息注册、发布招聘岗位、查看收到的简历、对候选人进行筛选标记。
- 管理端:学生和企业账号的审核、招聘信息合规性管理、就业公告发布、基础数据统计。
这个三角关系必须形成闭环:企业发布职位 -> 学生浏览并投递 -> 企业查看简历并反馈状态 -> 学生看到进度。任何一个环节断了,平台价值就大打折扣。所以我在设计数据库时,特意把投递记录表作为整个系统的核心枢纽,串联起职位、学生、企业三张表。
2.2 核心数据表设计思路
数据库是这类系统最容易翻车的地方。我自己第一版设计的时候就把企业ID放在职位表里,后续查询投递记录时发现关联越来越复杂。后来重新梳理,把表结构调整成这样:
sys_user:用户基础表,包含角色字段(student/company/admin)、账号、密码(BCrypt加密存储)、手机号、邮箱。student_info:学生扩展表,关联sys_user,存学号、学校、专业、毕业年份、个人简介。company_info:企业扩展表,关联sys_user,存企业名称、统一社会信用代码、行业类别、企业规模、地址。job_position:职位表,关联company_info,存岗位名称、薪资范围、学历要求、工作地点、职位描述、招聘人数、发布日期。resume:简历表,关联student_info,存教育经历、实习经历、技能特长、上传的简历文件路径。job_application:投递记录表,关联job_position和student_info,存投递时间、当前状态(待查看/已查看/面试邀请/已录用/已拒绝)。favorite_job:收藏表,关联job_position和student_info。news_notice:公告表,存平台发布的就业政策和招聘活动通知。
表设计的核心原则就是:把用户基础信息、角色扩展信息、业务数据分开。sys_user只管登录认证,student_info和company_info存各自角色的专属字段,job_application是业务主表,用来串联整个流程。这样拆分,后面写SQL和业务逻辑都轻松很多。
2.3 用例场景推演
我拿一个典型场景来推演系统流程。某学生登录后,在首页看到职位推荐列表,点击进入职位详情页,发现某科技公司的Java开发岗符合自己的期望,于是点击"投递简历"。此时系统做了一个关键动作:先检查该学生是否已完善简历,如果没有,前端会弹出提示引导到简历编辑页;如果已完善,则向后端发送投递请求。后端先查job_application表中是否已有该学生对该职位的投递记录,存在则拒绝重复投递,不存在才创建新记录并返回成功。企业登录进入"收到的简历"列表,看到该学生的投递后,点击"查看简历"并更新状态为"已查看",如果觉得合适,可以标记为"面试邀请"。学生端刷新"我的投递"页,就能看到最新的反馈状态。
3. 后端SpringBoot核心实现与关键技术
3.1 项目初始化与分层结构
我创建项目用的Spring Initializr,Java版本选的JDK 8(不要小看版本选择,后面踩坑会提到SpringBoot版本和JDK版本的兼容性问题),SpringBoot版本用的2.7.x,依赖只加了最必要的几项:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>这里我选MyBatis-Plus而不是原生MyBatis,原因很实际:平台里几乎所有业务表都需要基础的增删改查,MyBatis-Plus的BaseMapper直接提供这些方法,我只需要专注于自定义复杂的多表关联查询。比如职位列表页需要同时展示企业名称和企业Logo,这时写一个自定义的VO查询即可。
项目结构我是严格按照经典的三层架构拆的:
com.yiwangxunzhi ├── controller ├── service │ ├── impl ├── mapper ├── entity ├── vo ├── config ├── utils └── common每个包职责单一,controller层只做参数接收和结果返回,service层写业务逻辑,mapper层和数据库打交道。我的体会是,做毕业设计尤其要重视这个分层,因为论文的架构章节要靠它来写,答辩时老师也很喜欢问分层的作用。
3.2 基于JWT的用户鉴权方案
校园招聘平台有三个角色,每个角色能访问的接口完全不同,所以权限控制必须做在接口层面。我用的方案是JWT + 拦截器:
用户登录成功后,后端生成一个token,把userId和role信息放进去,返回给前端。前端把token存进localStorage,每次axios请求时在请求头里带上Authorization: Bearer token。后端写一个拦截器,放行登录、注册、浏览职位等公开接口,其余接口统一校验token合法性。
JWT生成的工具类核心代码如下:
public class JwtUtils { private static final String SECRET = "your-secret-key"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String 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(); } }这里我踩过一个比较经典的坑:JWT的密钥不能硬编码在工具类里,虽然毕业设计这样写问题不大,但答辩时很容易被老师追问。我的做法是挪到application.yml配置文件中,通过@Value注解注入,也算展示了一点工程化意识。
拦截器代码不算复杂,关键在于需要从请求头里取出token再做校验,校验通过后把userId放入request的attribute里,后续controller就能直接获取当前登录用户的ID:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = JwtUtils.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } } }还有一个细节容易忽略:预检请求OPTIONS必须放行,否则前端跨域调用时会被拦截器挡住,前端看到的是莫名其妙的CORS错误。
3.3 招聘信息模块的API设计
招聘信息是整个平台数据量最大的模块,API设计决定了前后的联调效率。我设计的接口清单如下:
| 接口路径 | 请求方式 | 功能说明 | 权限 |
|---|---|---|---|
/api/job/list | GET | 分页查询职位列表 | 所有角色 |
/api/job/detail/{id} | GET | 查看职位详情 | 所有角色 |
/api/job/company/{companyId} | GET | 查询某企业发布的职位 | 所有角色 |
/api/job/add | POST | 发布职位 | 企业 |
/api/job/update | PUT | 修改职位信息 | 企业 |
/api/job/delete/{id} | DELETE | 下架职位 | 企业 |
/api/job/search | GET | 多条件组合搜索 | 所有角色 |
职位列表的分页和搜索是我个人觉得含金量比较高的功能。我利用MyBatis-Plus的分页插件,配合自定义的查询条件构造器来实现。前端传过来的是current(当前页)、size(每页条数)、keyword(搜索关键词)、city(工作城市)、salaryRange(薪资范围)这几个参数。后端根据参数动态拼接查询条件:
public Page<JobPositionVO> searchJobs(JobSearchDTO dto) { Page<JobPosition> page = new Page<>(dto.getCurrent(), dto.getSize()); LambdaQueryWrapper<JobPosition> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(dto.getKeyword())) { wrapper.like(JobPosition::getTitle, dto.getKeyword()) .or().like(JobPosition::getDescription, dto.getKeyword()); } if (StringUtils.hasText(dto.getCity())) { wrapper.eq(JobPosition::getCity, dto.getCity()); } if (dto.getSalaryMin() != null) { wrapper.ge(JobPosition::getSalaryMin, dto.getSalaryMin()); } wrapper.orderByDesc(JobPosition::getPublishTime); // 分页查询后还需要关联企业表,补充企业名称、企业Logo等展示字段 return jobMapper.selectPageVO(page, wrapper); }这里有个很关键的细节:职位列表页不能只返回职位表本身的字段,还需要把企业名称、企业Logo这些关联信息拼上。我用了自定义SQL,在mapper的XML里写一个多表关联查询,一次查出来,避免在Java代码里循环查企业表。
3.4 简历投递的业务逻辑细节
简历投递是系统里最核心的业务操作,它的逻辑复杂度实际上被很多人低估了。我的实现里不仅包含创建投递记录,还包含投递前的资格校验、重复投递检测、企业通知消息触发。
public Result applyJob(Long studentId, Long jobId) { // 校验职位存在且正在招聘中 JobPosition job = jobMapper.selectById(jobId); if (job == null || job.getStatus() != 1) { return Result.error("职位不存在或已下架"); } // 校验学生简历是否完善 Resume resume = resumeMapper.selectOne( new LambdaQueryWrapper<Resume>().eq(Resume::getStudentId, studentId)); if (resume == null || StringUtils.hasText(resume.getFilePath()) == false) { return Result.error("请先完善简历再投递"); } // 检查重复投递 Long count = applicationMapper.selectCount(new LambdaQueryWrapper<JobApplication>() .eq(JobApplication::getStudentId, studentId) .eq(JobApplication::getJobId, jobId)); if (count > 0) { return Result.error("您已投递过该职位,请勿重复投递"); } // 创建投递记录 JobApplication application = new JobApplication(); application.setStudentId(studentId); application.setJobId(jobId); application.setCompanyId(job.getCompanyId()); application.setStatus("PENDING"); application.setApplyTime(new Date()); applicationMapper.insert(application); return Result.success("简历投递成功"); }需要特别说明的是,我在抛出错误提示时,特意把"请先完善简历"和"请勿重复投递"这类场景区分开。原因是前端需要根据后端返回的错误码来做不同的交互引导——完善简历要跳转编辑页,重复投递则只是在当前页面弹提示。所以后端接口的返回结构一定要规范,code、message、data三段式缺一不可。
3.5 文件上传:简历附件与图片处理的实操
平台涉及两类文件上传:学生上传简历附件(PDF/Word),企业上传企业Logo和工作环境照片。文件处理这里有几个点比较关键。
首先,上传目录的路径不能写死成绝对路径。我是配置在application.yml里,通过配置项读入。同时,把上传目录设置为静态资源映射,这样前端可以直接通过URL访问上传的图片:
file: upload-dir: ./upload/然后写一个配置类做资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadDir); } }文件上传接口我用的是MultipartFile接收,限制文件大小为10MB,校验文件扩展名。这里有一个很重要的坑:文件名必须重命名,不能直接用用户上传的原始文件名,否则会带来两个问题,一是中文文件名在某些浏览器下载时会乱码,二是恶意文件名可能触发安全问题。我用UUID重新生成文件名:
public String uploadFile(MultipartFile file) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + ext; File dest = new File(uploadDir + newFileName); file.transferTo(dest); return "/files/" + newFileName; }3.6 企业端与管理员端的组合查询设计
企业端最核心的查询是"收到的简历列表",这里需要同时关联学生表、简历表、投递记录表。一个很现实的需求是:企业HR需要按状态筛选候选人,比如只看"面试邀请"的、只看"待查看"的。我在这块用了MyBatis-Plus的Page<CompanyReceivedResumeVO>自定义分页查询。
同样重要的一块是管理员端的就业数据统计。学校管理端需要看到各专业投递情况、就业率等。我的方案是利用MySQL的GROUP BY语句做基础聚合,统计各专业的投递总量和企业发布的岗位数量:
public List<Map<String, Object>> getMajorStatistic() { return studentMapper.selectMajorApplyCount(); }对应的SQL在XML里:
<select id="selectMajorApplyCount" resultType="java.util.Map"> SELECT s.major, COUNT(DISTINCT a.job_id) AS apply_total FROM student_info s LEFT JOIN job_application a ON s.user_id = a.student_id GROUP BY s.major </select>这种Map接收的方式,直接在service里给前端返回一个可渲染的数据结构,不需要额外建VO类,简洁高效。
4. 前端Vue实现与页面交互细节
4.1 Vue项目搭建与路由设计
前端我用的Vue2 + Element UI的组合,没有上Vue3,原因是当时项目初始化时Element UI对Vue2的支持最成熟,组件库可以直接用,不用额外适配。搭建过程很简单:
npm install -g @vue/cli vue create yiwangxunzhi-front创建项目时选择Router、Vuex,然后手动添加axios和Element UI。路由设计上我按照角色来分目录管理:
| 路由路径 | 页面组件 | 访问权限 |
|---|---|---|
/login | 登录页 | 游客 |
/register | 注册页 | 游客 |
/home | 首页职位流 | 所有登录用户 |
/job/detail/:id | 职位详情页 | 所有登录用户 |
/student/resume | 简历管理 | 学生 |
/student/applications | 我的投递记录 | 学生 |
/student/favorites | 我的收藏 | 学生 |
/company/jobManage | 职位管理 | 企业 |
/company/receivedResume | 收到的简历 | 企业 |
/admin/userManage | 用户审核 | 管理端 |
每个路由都加了meta.requiresAuth字段,配合vue-router的前置守卫做登录校验。没有token访问受保护页面时,直接重定向到登录页。这个设计非常必要,因为简历、投递记录这些页面一旦被越权访问,就是严重的数据泄露。
4.2 登录态管理与角色路由守卫
登录态管理我是用Vuex + localStorage的组合方案。用户登录成功后,后端返回token和用户角色信息,前端存到localStorage里,同时commit到Vuex的state中。后续每次axios请求,在请求拦截器里统一加上token:
service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; });响应拦截器也不要忽视。当后端返回401状态码时,说明token失效或未登录,前端要统一做登出处理并跳转登录页:
service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); localStorage.removeItem('role'); router.push('/login'); } return Promise.reject(error); } );路由守卫里的角色判断也很关键。管理员想去学生端页面,或者学生想访问企业端页面,都会被拦截。我在守卫里加上角色匹配逻辑,同时做到页面级权限控制:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.role && to.meta.role !== localStorage.getItem('role')) { next('/home'); } else { next(); } });4.3 核心页面组件设计与交互
首页职位流我用了经典的"列表+筛选"布局。左侧是工作城市和薪资区间筛选,中间是职位卡片列表,右上角是搜索框。职位卡片组件接收一个jobItem对象作为prop,内部展示职位名称、企业名称、薪资标签、学历要求、发布时间,点击卡片跳转到详情页。
一个比较值得借鉴的设计是筛选条件的联动。当用户选择城市后,薪资区间下拉框的选项会保持不变,但列表会重新请求接口。我采用的是"筛选条件统一存入data对象,通过深度监听触发列表重新加载"的方案:
data() { return { filterForm: { city: '', salaryMin: '', keyword: '', current: 1, size: 10 } } }, watch: { filterForm: { handler() { this.current = 1; this.loadJobs(); }, deep: true } }这个方案的好处是代码简洁,避免在每次筛选条件变化时都手写一堆事件处理。
职位详情页需要同时展示职位信息和公司信息,所以后端返回的VO结构里包含了企业名称、规模、地址等。页面下方还有一个"相似职位推荐"区域,我调用了/api/job/list接口并传入相同城市和行业类别,利用已有接口复用,减少后端的重复开发。
4.4 简历编辑页的复杂表单处理
学生端的简历编辑页是整个前端工作量比较大的部分。教育经历、实习经历、项目经历这三块都是动态增删的表单结构。我用Element UI的el-form配合v-for循环渲染动态表单行,每个经历项是一个子组件。提交时把所有数据组装成JSON对象,一次性提交后端保存。
这里有个实战经验分享:动态表单的校验不能只用Element UI的默认校验规则。比如教育经历里的开始时间必须早于结束时间,这种联动校验需要自定义validator函数:
const validateTimeRange = (rule, value, callback) => { const start = form.educations[index].startDate; const end = form.educations[index].endDate; if (start && end && start > end) { callback(new Error('开始时间不能晚于结束时间')); } else { callback(); } };简历附件上传我用的Element UI的el-upload组件,手动控制上传请求,上传成功后把返回的文件路径隐藏到表单数据里,等用户点"保存简历"时一并提交。这里需要特别注意:附件路径不能只是临时在页面显示,必须和简历的文本信息在同一事务里保存,否则会出现简历文本已保存、附件丢失的半成品状态。
4.5 前后端联调与接口对接
联调阶段最容易出问题的就是接口路径一致性。我自己就经历过一次:后端接口是/api/job/list,前端写的却是/api/jobs/list,排查了半天发现是URL少了一个词。所以项目里我在前端统一建了一个api文件夹,每个模块单独一个JS文件,集中管理所有接口调用:
// api/job.js import request from '@/utils/request'; export function getJobList(params) { return request({ url: '/api/job/list', method: 'get', params }); } export function getJobDetail(id) { return request({ url: `/api/job/detail/${id}`, method: 'get' }); } export function applyJob(jobId) { return request({ url: '/api/job/apply', method: 'post', data: { jobId } }); }组件里只需要import { getJobList } from '@/api/job',然后调用函数即可。这个习惯让我在后端接口改动时,只需要改一个文件,不需要全局搜索替换。
5. 常见问题排查与避坑实录
5.1 跨域问题的正确处理
SpringBoot后端默认是不允许跨域请求的,而前端的开发服务器在8080端口,后端的Tomcat在8081端口,两者端口不同必然产生跨域。我的解决方式是在后端写一个CORS配置类,全局允许跨域:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }这里有个细节必须提示:如果使用JWT拦截器,OPTIONS预检请求必须在拦截器里放行,否则预检请求先被拦截器拦截,前端看到的错误是"请求被CORS策略阻止",而不是登录失效。
5.2 日期时间格式化问题
前后端日期字段经常出现"传过去是JSON字符串,读出来变成时间戳"的问题。我在实体类的日期字段上加了@JsonFormat注解,统一格式化:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date publishTime;前端的Element UI日期选择器绑定的值是Date对象,提交时要手动转换成yyyy-MM-dd格式的字符串,否则后端接收会报格式错误。我封装了一个日期格式化工具函数,在提交时统一处理。
5.3 简历附件上传后的预览问题
学生上传的简历附件是PDF或Word格式,浏览器无法直接预览Word文件。我踩过的坑是尝试用前端插件把Word转成PDF,结果插件兼容性很差。最后采取的方案是:前端只显示附件文件名和下载按钮,不强制预览,下载时后端设置Content-Disposition响应头,让浏览器自动下载。虽然不够炫酷,但稳定可靠。
5.4 本地调试时前端静态资源404
我在application.yml里配置了静态资源映射路径/files/**,但发现部署到Linux服务器后,上传的目录相对路径会随着启动目录变化而变化。最稳妥的方式是在配置里写绝对路径,或者用System.getProperty("user.dir")拼接路径。我这里有个更规避的方案是写一个启动时的目录初始化逻辑,检查目录是否存在,不存在则自动创建:
@Configuration public class FileStorageConfig { @Bean public CommandLineRunner initUploadDir() { return args -> { File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } }; } }5.5 企业投递状态更新后学生端的实时反馈
企业端将投递状态从"待查看"改为"面试邀请"后,学生端可能需要刷新页面才能看到。我最初的方案是让学生手动刷新,但体验比较差。后来我加了消息通知表,企业在更新状态时插入一条通知记录,学生端登录时加载最新的通知数量显示在导航栏上。这个功能让系统看起来完整度更高,答辩时也容易被认可。
5.6 多角色复用一个登录接口
三个角色用一个登录接口还是分开?我建议统一使用一个登录接口,通过用户名查出用户记录,再校验密码和角色。好处是前端只需要维护一个登录页,后端只需要一个/api/auth/login接口。角色区分放在注册时就绑定的,注册页面做一个角色切换的单选按钮,选学生注册就创建sys_user加student_info记录,选企业注册就创建sys_user加company_info记录。
5.7 数据一致性问题:企业注销后职位如何处理
企业账号如果被管理员封禁,该企业名下的职位还在投递中,就会产生业务异常。我的处理是:企业封禁时,后端批量将该企业的所有职位状态改为"已下架",同时给所有投递该职位的学生的投递记录状态更新为"已关闭"。这一步用一个@Transactional事务方法解决,保证多个数据表的更新在同一个事务里完成:
@Transactional public void banCompany(Long companyId) { companyMapper.updateStatus(companyId, 0); jobMapper.updateStatusByCompanyId(companyId, 0); applicationMapper.updateStatusByCompanyId(companyId, "CLOSED"); }6. 部署上线与答辩准备要点
6.1 项目打包与部署
后端打包用Maven的package命令,生成jar包后通过java -jar启动。前端打包用npm run build,生成dist目录,把这个目录直接放在SpringBoot的static资源目录里,就不需要额外配Nginx了。这样整个系统就变成了一个单体应用,访问端口统一为后端的8081端口。
对于毕业设计来说,这种"前后端合并部署"的方式是最省事的,因为不需要讲解Nginx代理配置,也避免了跨域问题在部署环境里再次出现。如果后续想扩展前后端分离部署,再把前端放到Nginx里配置一个反向代理即可。
6.2 答辩时的高频提问点准备
根据我带过的做同类题目的经验,答辩老师最喜欢追问这几个问题:
- "为什么用SpringBoot而不用其他框架?"答:SpringBoot降低了项目初始化的复杂度,内置服务器简化部署,自动配置机制让开发更关注业务本身。
- "JWT相比Session方案有什么优势?"答:JWT无状态,适合前后端分离架构,服务端不需要存每个用户的session,天然支持跨域和分布式部署。
- "简历投递如何防止重复?"答:投递记录表对
student_id + job_id做唯一约束,后端在插入前也做一次校验,双重保障。 - "数据库索引怎么设计的?"答:投递记录表加
(student_id, job_id)联合索引,职位表的publish_time加普通索引,应用表关联字段都加了索引。 - "并发投递同一职位时如何保证数据一致?"答:利用数据库唯一索引兜底,即使后端逻辑并发下出现重复判断,数据库层面也能挡住重复记录。
6.3 项目可扩展方向
这个系统做完基础功能后,如果学有余力想做得更出彩,可以考虑扩展这几个方向:
- 职位推荐算法:根据学生的专业、技能标签做简单的基于标签匹配的推荐,不需要复杂的机器学习,用一个权重计分公式就能实现。
- 消息推送:整合WebSocket,企业更新投递状态时实时推送给学生,不再依赖刷新。
- 数据可视化:管理端的就业统计页用ECharts展示趋势图,让数据更直观。
- 简历解析:上传PDF简历后,用正则或第三方工具解析出关键字段,自动填充简历表。
我个人实际操作中最大的体会是:毕业设计不是做完了就算过关,而是要真正理解每一个功能背后的业务逻辑。当你给面试官讲"投递状态是怎么流转的""事务是怎么保证一致的""权限是怎么控制的"时,那种真实参与项目的底气,是简历上任何一句话都替代不了的。希望这篇拆解能帮你把"一网寻职"做成一个拿得出手的完整作品,而不是又一个停留在截图层面的演示项目。