尧图精选

SpringBoot+Vue高校招聘系统实战:从联调避坑到PDF导出

🕒 发布时间:2026/10/1 4:43:21 📁 来源:尧图网络
简介本资源是一套面向计算机专业本科生的高分毕业设计项目——高校就业招聘系统适用于毕设选题、课程设计及期末大作业场景解决高校就业服务信息化落地的实际需求。资源包共980个文件含169个Java后端逻辑文件、161个JavaScript交互脚本、87个Vue组件、86个JPG/GIF图片资源、74个HTML页面及52个CSS样式文件涵盖前后端完整代码、MySQL 5.7数据库脚本SQL、Navicat建库说明、IDEAMaven开发部署配置以及配套论文文档压缩包大小27.58MB结构规范开箱即用。目前已有57人学习下载资源经严格调试提供3个批处理脚本install.bat/run.bat/build.bat简化环境搭建前端含Element UI与Bootstrap双样式体系后端SpringBoot模块划分清晰支持招聘发布、简历投递、面试安排、数据统计等核心业务闭环。1. 高校就业招聘系统不是“又一个 CRUD”而是 SpringBoot Vue 联调实战的完整闭环从数据库建模到 PDF 导出、从登录鉴权到岗位推荐逻辑它把毕设里最常翻车的 7 类场景全打穿了你肯定见过那种“SpringBoot 写个 UserControllerVue 调个 /api/user/list然后截图交毕设”的项目——但这个高校就业招聘系统不是。它真实还原了校招场景企业端要审核资质、发布带附件的招聘简章PDF/Word、设置截止时间学生端要上传简历支持 .docx/.pdf、投递时自动关联专业匹配度管理员要导出 Excel 统计报表、生成带水印的 PDF 录用通知书后台还要跑定时任务清理过期岗位、用 MyBatis-Plus 分页插件扛住千人并发查询。整套源码跑起来不是“Hello World”而是能直接部署到学校二级学院服务器上跑一整个招聘季。适合两类人一是大四正在赶毕设、被导师卡在“功能太单薄”“没业务逻辑”“联调总 404”的同学二是刚转 Java 全栈、想拿一个有真实业务厚度的项目练手的转行者。它不教你怎么写 Hello World它教你怎么让 Vue 页面里的“投递按钮”点下去后MySQL 里真多一条带时间戳和状态码的记录且 SpringBoot 的全局异常处理器能捕获到 PDF 解析失败并返回友好提示。2. 搭建前必须搞清的三道坎为什么选 MyBatis-Plus 而不是纯 MyBatis为什么 Vue 用 Composition API 而非 Options API为什么 MySQL 表设计里加了is_deleted却没用逻辑删除2.1 数据层选型MyBatis-Plus 是怎么把“查岗位统计投递数按专业筛选”压缩成一行代码的这个系统里最常被问爆的接口是/api/job/list?major计算机科学与技术status1—— 它要查出所有“已发布”的岗位并对每个岗位附带“该专业学生投递总数”。如果用原生 MyBatis你得手写 XMLresultMap嵌套查询再写 DAO 层方法最后在 Service 里手动循环统计。而本项目直接用 MyBatis-Plus 的QueryWrapperLambdaQueryWrapper实现// JobServiceImpl.java public PageJobVO listJobs(PageJob page, String major, Integer status) { LambdaQueryWrapperJob wrapper new LambdaQueryWrapper(); wrapper.eq(Job::getStatus, status) .like(StringUtils.isNotBlank(major), Job::getMajorRequire, major); // 关键用自定义 SQL 注入器实现 JOIN 统计而非 N1 查询 return jobMapper.selectPageWithApplyCount(page, wrapper); }注意selectPageWithApplyCount不是 MP 自带方法是项目在JobMapper.xml里手写的select idselectPageWithApplyCount里面用LEFT JOIN apply_record ON job.id apply_record.job_id GROUP BY job.id。MP 只负责分页和条件拼接统计逻辑仍需 SQL 控制——这是很多新手误以为“MP 能自动 JOIN”的坑点。2.2 前端架构Composition API 怎么解决“学生简历上传预览校验”这个经典三连击Vue 部分没用export default { data() { ... } }这种 Options API而是用setup()ref/reactive管理状态。比如简历上传模块!-- StudentResumeUpload.vue -- script setup import { ref, reactive } from vue import { uploadResume, previewResume } from /api/resume const file ref(null) const previewUrl ref() const formData reactive({ name: , phone: , email: }) const handleFileChange (e) { file.value e.target.files[0] if (file.value) { // 立即预览不上传用 FileReader 读取 base64 const reader new FileReader() reader.onload () previewUrl.value reader.result reader.readAsDataURL(file.value) } } const submitResume async () { const formDataToSend new FormData() formDataToSend.append(file, file.value) formDataToSend.append(name, formData.name) // ...其他字段 await uploadResume(formDataToSend) // 调用 axios 封装的上传接口 } /script逻辑说明这里previewUrl用ref是因为它是单一值formData用reactive是因为它包含多个响应式字段。关键点在于handleFileChange里没直接调用uploadResume而是先本地预览——这避免了用户传错文件后才发现无法预览的尴尬。而uploadResume接口实际接收的是FormData对象后端用RequestParam MultipartFile file接收不是 JSON。2.3 数据库设计is_deleted字段为什么设为 TINYINT(1) 而不是 BIT为什么job表里有major_require却不用 JSON 类型MySQL 表结构里所有实体表job,student,company都有is_deleted TINYINT(1) DEFAULT 0 COMMENT 逻辑删除0-未删1-已删。没用BIT(1)是因为早期 MySQL 版本如 5.7对 BIT 类型的 JDBC 驱动支持不稳定TINYINT更稳妥。更重要的是项目在MyMetaObjectHandler中统一注入了自动填充逻辑Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, isDeleted, Integer.class, 0); // 默认 0 this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); } }参数说明strictInsertFill是 MP 3.4.0 的新 API比旧版fillStrategy更明确。isDeleted字段名必须和实体类属性名一致不是数据库列名is_deletedMP 会自动映射。而major_require字段存的是逗号分隔字符串如计算机科学与技术,软件工程没用 JSON 类型是因为① MySQL 5.7 默认不开启 JSON 函数支持② 后端用String.split(,)做模糊匹配足够快③ 避免 JSON 字段在 Navicat 里显示乱码毕设答辩现场打开 Navicat 演示时很关键。3. 启动就报错80% 的失败源于这 5 个环境配置细节JDK 版本锁死、Vue 端口冲突、MySQL 时区、MyBatis-Plus 的TableName、以及那个藏在application-dev.yml里的密码明文3.1 JDK 版本必须是 1.8SpringBoot 2.7.x 不兼容 JDK 17 的三个具体表现项目pom.xml明确指定properties java.version1.8/java.version spring-boot.version2.7.18/spring-boot.version /properties如果你强行用 JDK 17 启动会遇到Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverterJAXB 在 JDK 9 被移除而项目里JwtUtil用它做 Base64 编码Failed to load ApplicationContextSpring Boot 2.7.x 的spring-boot-starter-web依赖的 Tomcat 9.0.x 不支持 JDK 17 的某些字节码指令Data注解失效Lombok 1.18.22本项目所用对 JDK 17 的注解处理器支持不完善导致toString()方法不生成。血泪经验我第一次跑崩就是因为java -version显示 17却没看pom.xml。解决方案只有两个① 用 SDKMAN 切换 JDK 1.8② 或升级 SpringBoot 到 3.x但需重写所有 Controller 的RequestBody解析逻辑不推荐毕设阶段折腾。3.2 Vue 端口被占用vue.config.js里devServer.port和proxy必须同步改前端启动命令是npm run serve默认端口 8080。但如果你电脑上开着 IntelliJ IDEA 的内置 Tomcat默认 8080或 Chrome 扩展占用了端口就会报Error: listen EADDRINUSE: address already in use :::8080此时不能只改vue.config.js// vue.config.js module.exports { devServer: { port: 8081, // 改这里 proxy: { /api: { target: http://localhost:8080, // 这里也必须同步改成 8081错必须保持 8080 changeOrigin: true, pathRewrite: { ^/api: } } } } }关键点proxy.target指向的是后端 SpringBoot 的端口默认server.port8080不是前端端口所以devServer.port改成 8081 后proxy.target仍应是http://localhost:8080否则请求会 404。很多同学把target也改成8081结果 Vue 页面能打开但所有/api/xxx请求都跨域失败。3.3 MySQL 时区错误The server time zone value й׼ʱ is unrecognized的根因和两行解决命令启动 SpringBoot 时控制台报这个错本质是 MySQL 服务端时区和 JDBC 连接串不匹配。项目application-dev.yml里连接串是spring: datasource: url: jdbc:mysql://localhost:3306/job_recruit?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8但如果你的 MySQL 服务端时区是SYSTEM即操作系统时区而系统时区是中文如CSTJDBC 就解析不了。不要改serverTimezoneGMT%2B8那是治标。正确做法是进 MySQL 执行-- 查看当前时区 SELECT global.time_zone, session.time_zone; -- 强制设为 08:00不是 Asia/Shanghai后者在某些 MySQL 版本不识别 SET GLOBAL time_zone 08:00; SET time_zone 08:00; -- 永久生效修改 my.cnfLinux或 my.iniWindows -- [mysqld] 下加一行default-time-zone 08:00提示执行SET GLOBAL需要 root 权限。如果没权限就只能在连接串里加serverTimezoneGMT%2B8但务必确保%2B是 URL 编码的不是直接写会被解析为空格。3.4 MyBatis-Plus 的TableName必须和application.yml里的table-prefix严格对应实体类Job.java顶部有TableName(job) public class Job extends ModelJob { ... }而application.yml里配置了mybatis-plus: global-config: db-config: table-prefix: job_ # 注意这里是 job_不是 job这就矛盾了TableName(job)会覆盖table-prefix导致 MP 生成的 SQL 是SELECT * FROM job但实际表名是job_info项目里真实表名。必须删掉TableName或改成TableName(job_info)。项目源码里Job.java实际是// 正确写法 TableName(job_info) public class Job extends ModelJob { ... }避坑逻辑MP 的table-prefix是全局前缀TableName是单表覆盖。当两者共存时TableName优先级更高。毕设答辩时老师如果问“为什么你的 SQL 查的是 job_info 而不是 job”你得答出这个优先级规则。3.5application-dev.yml里的 MySQL 密码是明文别慌这是故意留的调试入口文件里写着spring: datasource: username: root password: 123456 # ← 这里是明文这不是疏忽而是为了降低毕设部署门槛。真实生产环境必须用spring.cloud.config或jasypt加密但毕设阶段导师更关心你能不能跑起来。但必须立刻改掉① 本地开发直接改成本机 MySQL 密码② 提交 Git把application-dev.yml加入.gitignore用application-prod.yml替代③ 如果用 IDEA 启动可以在Run Configuration → Environment variables里设SPRING_DATASOURCE_PASSWORDyour_real_pwd比改 YAML 更安全。4. 避坑联调时最常卡死的 4 类现象以及我用日志定位的真实排查路径4.1 现象Vue 点“登录”按钮Network 里看到/api/login返回 200但页面没跳转控制台也没报错原因Axios 响应拦截器里response.data.code ! 200时没return Promise.reject()导致后续.then()一直挂起。解决检查utils/request.js// 错误写法只 console.log没 reject if (response.data.code ! 200) { Message.error(response.data.msg) // ❌ 缺少 return Promise.reject(new Error(...)) } // 正确写法 if (response.data.code ! 200) { Message.error(response.data.msg) return Promise.reject(new Error(response.data.msg)) // ✅ 必须 return }4.2 现象SpringBoot 启动成功但访问http://localhost:8080/api/job/list返回 404原因Controller 类没加RestController或方法没加GetMapping或RequestMapping(/api)写在了类上但没继承BaseController。解决① 确认JobController.java顶部有RestController② 确认方法上有GetMapping(/list)③ 确认RequestMapping(/api)在父类BaseController上且JobController继承它④ 最后用curl -v http://localhost:8080/actuator/mappings查看所有注册的 endpoint确认/api/job/list是否在列表中。4.3 现象学生上传 PDF 简历后后端MultipartFile接收到的file.getSize()是 0原因Vue 的FormData构造时append(file, fileInput.files[0])的fileInputDOM 元素没正确绑定或enctypemultipart/form-data没写在form标签上。解决① 检查 HTML 模板是否漏了enctypeform submit.preventsubmitResume enctypemultipart/form-data input typefile changehandleFileChange / /form② 在handleFileChange里加断点确认e.target.files[0]是否为 null③ 后端用log.info(File size: {}, file.getSize())打印大小确认是否真为 0。4.4 现象MySQL 插入中文乱码student.name字段存进去是??原因MySQL 服务端字符集是latin1而项目连接串没强制指定characterEncodingutf8。解决① 登录 MySQL 执行SHOW VARIABLES LIKE character_set%;确认character_set_server是utf8mb4② 如果不是修改my.cnf[mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci [client] default-character-set utf8mb4③ 重启 MySQL并重新创建数据库CREATE DATABASE job_recruit CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;④ 最后确认连接串里有?useUnicodetruecharacterEncodingutf8。5. 让 PDF 导出真正可用用 Apache POI iText7 生成带水印、页眉页脚、表格自动分页的录用通知书5.1 为什么不用 JasperReportsPOI iText7 的组合更适合毕设场景的三个理由JasperReports 配置复杂、学习曲线陡峭而本项目导出 PDF 的核心需求是① 动态填充学生姓名、岗位名称、入职日期② 加公司 LOGO 水印③ 每页底部加页码和“机密”字样④ 表格数据超长时自动分页。POI 处理 WordiText7 处理 PDF分工明确WordExportUtil.java用 POI 读取template.docx含${name},${position}占位符替换后生成.docxPdfExportUtil.java用 iText7 把.docx转 PDF并加水印、页眉页脚。关键优势iText7 的PdfCanvas可以精确控制每一页的绘制而 JasperReports 的分页逻辑需要大量 XML 配置毕设答辩时老师问“你怎么控制页眉位置”你答“用showTextAligned()坐标定位”比答“在 jrxml 里设pageHeader”更显功底。5.2 水印实现不是贴图而是用PdfCanvas在每页底层画斜文字public static void addWatermark(PdfDocument pdfDoc, String text) { for (int i 1; i pdfDoc.getNumberOfPages(); i) { PdfPage page pdfDoc.getPage(i); PageSize pageSize page.getPageSize(); PdfCanvas canvas new PdfCanvas(page.newContentStreamBefore(), page.getResources(), page); // 设置字体必须用支持中文的字体如 simsun.ttc PdfFont font PdfFontFactory.createFont(simsun.ttc, Identity-H, true); canvas.saveState(); canvas.setFillColor(ColorConstants.LIGHT_GRAY); canvas.beginText() .setFontAndSize(font, 60) .moveText(pageSize.getWidth() / 2 - 200, pageSize.getHeight() / 2 100) .showText(text) .endText(); canvas.restoreState(); } }参数说明moveText(x, y)的坐标原点在左下角pageSize.getWidth()/2 - 200是让文字居中偏左60是字体大小ColorConstants.LIGHT_GRAY避免遮挡正文。字体文件simsun.ttc必须放在resources/fonts/目录下否则抛IOException。5.3 表格自动分页用Table的setKeepTogether(true)和setNextRenderer()应对跨页断裂学生投递记录表可能有 50 行一页放不下。iText7 默认会把表格硬切开导致表头丢失。解决方案Table table new Table(UnitValue.createPercentArray(new float[]{1, 2, 1})) .setWidth(UnitValue.createPercentValue(100)) .setKeepTogether(true); // 关键整表不跨页 // 如果还是跨页手动指定下一页渲染器 table.setNextRenderer(new TableNextRenderer(table));避坑点setKeepTogether(true)只对小表格有效≤ 10 行。超过时必须用TableNextRenderer它继承TableRenderer重写getNextRenderer()方法在drawChildren()里判断剩余空间不足时新建一页。项目源码里CustomTableRenderer.java就实现了这个逻辑。5.4 页眉页脚用HeaderFooterEventHandler在每页固定位置写内容public class HeaderFooterEventHandler implements IEventHandler { Override public void handleEvent(Event event) { PdfDocumentEvent docEvent (PdfDocumentEvent) event; PdfPage page docEvent.getPage(); PdfCanvas canvas new PdfCanvas(page.getLastContentStream(), page.getResources(), page); Rectangle pageSize page.getPageSize(); // 页眉右上角写“高校就业中心” canvas.beginText() .setFontAndSize(PdfFontFactory.createFont(), 10) .moveText(pageSize.getRight() - 50, pageSize.getTop() - 20) .showText(高校就业中心) .endText(); // 页脚居中写“第 X 页共 Y 页” int pageNumber page.getPageNumber(); int totalPages docEvent.getDocument().getNumberOfPages(); canvas.beginText() .moveText(pageSize.getWidth() / 2 - 30, pageSize.getBottom() 20) .showText(第 pageNumber 页共 totalPages 页) .endText(); } }逻辑说明IEventHandler是 iText7 的事件机制handleEvent在每页渲染完成时触发。page.getLastContentStream()确保内容画在最顶层不会被表格遮盖。pageSize.getTop()是页面顶部坐标-20是向下偏移 20 点避免紧贴边缘。6. 从那以后我每次部署毕设项目都强制走一遍这 3 个验证动作curl 测试核心接口、Navicat 查看数据一致性、用 Chrome DevTools 捕获跨域请求我带过 12 届毕设发现 90% 的“答辩现场崩溃”源于三个动作没做①不测接口只刷页面curl -X POST http://localhost:8080/api/login -H Content-Type: application/json -d {username:admin,password:123456}看返回code:200和token字段是否存在②不查数据库只信前端显示用 Navicat 连上job_recruit执行SELECT COUNT(*) FROM student WHERE is_deleted 0;对比 Vue 页面“学生总数”是否一致③不抓包只看 Network 面板在 Chrome 里打开F12 → Network → Filter: XHR点“投递简历”确认POST /api/apply请求头有Authorization: Bearer xxx且响应体里code是 200 而非 500。这三个动作加起来不超过 3 分钟但它能提前暴露JWT token 没注入 header、MyBatis-Plus 的TableLogic没生效、Vue 的axios.defaults.headers.common[Authorization]没设对。去年有个学生答辩时老师让他现场演示“企业发布岗位”他点完按钮页面没反应手忙脚乱查控制台——其实只要提前curl一下/api/company/post-job就会发现403 Forbidden原因是PreAuthorize(hasRole(COMPANY))的角色没配对。现在我自己的项目无论多小都会在README.md里写清楚这三步验证命令。不是为了炫技而是让“能跑”变成“确定能跑”。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →