☰
毕业生网络招聘系统毕设全攻略:从数据库设计到答辩演示
2026/10/9 5:48:26 网站建设 项目流程

简介:面向Java方向毕业设计,这套基于WEB的毕业生网络招聘信息系统资料包,针对传统招聘耗时、目的性弱、效率低等痛点,采用B/S三层结构,借助JSP、JavaBean、JDBC完成求职者、用人单位、管理员三类角色的业务闭环,适合高校计算机相关专业学生作为课程设计或毕设蓝本参考。压缩包共312个文件,体积仅5.61MB,涵盖105个Java源码及对应class文件、44个JSP页面、17个CSS样式、16个Jar依赖库、3个Word文档和1个SQL脚本,代码与配置齐全。已有1269人学习下载。除可运行源码外,还包含开题报告、中期检查与答辩相关Word文档,以及数据库初始化脚本和项目工程文件,方便直接导入IDE运行。同时为毕业设计说明书撰写、项目结构梳理与系统二次开发提供清晰参照。

1. 毕业生网络招聘信息系统这个题目,是 WEB 工程里最容易答崩的那一类

毕业生网络招聘信息系统这个题目,是高校毕设清单里看着简单、答辩时最容易翻车的 WEB 工程之一。它从来不是简单的“职位表 + 简历表”增删改查,而是学生、企业、管理员三套角色视图、一条完整投递状态链的多角色业务系统。这篇文章面向正在选毕设题目、或手里已有 web 项目但讲不清源码的人,按我带学生做这类题目的顺序,从数据库设计、Spring Boot 接口、服务器部署,一直讲到开题报告、中期检查和答辩怎么准备。标题里带的源码、答辩、开题报告和中期检查四个词,我会逐个拆给你看。

2. 先画边界再建表:三套角色、四种状态、八张表

系统设计这步我一般要求先出角色功能清单,再往表结构走。很多毕设源码翻车,都是表结构设计阶段就埋了雷:企业信息挂在 user 表里、投递记录只存一个状态字符串、简历文件直接 base64 塞进数据库。这些设计不是不能跑,而是到了“企业端要看是谁投的简历”“管理员要统计各专业投递量”这类典型功能时,查询根本写不出来,或者要写一堆不规范的子查询。

2.1 先用一页纸把三套角色边界定下来

我的做法是先画一张权限二维表,行是功能模块,列是三类角色,交叉格里打勾。学生端能注册、维护个人信息、投递简历、收藏职位、查看投递状态;企业端能发职位、浏览收到的简历、变更投递状态、安排面试时间;管理员端管用户启禁用、审核职位上下架、发公告。这张表画清楚,后边的 Controller 路径前缀和拦截器规则就有了依据,你就不会再出现“学生能删企业职位”这种答辩时被当场点破的失误。

一个常见的选型问题是:同样做这个 web 项目,选 java 课程设计里最常见的 Spring Boot,还是用 php 源码直接改,或者用 python 的 django web 应用开发框架快速搭?我建议毕设优先 Spring Boot + MyBatis。它把你需要讲的架构概念——IOC、AOP、拦截器、ORM——全部明明白白暴露在源码里,答辩时能说出来的技术点比另外两个方案多得多。django 和 php 都适合快速出活,但讲到“底层运行原理”时容易变成背概念。

2.2 用户表统一、业务表拆分:建表前的两个决策

第一个决策是登录用户怎么存。常见做法是一张 user 表统一放登录账号,role 字段用 1 管理员、2 学生、3 企业区分;学生和企业的基础资料各自再建表,通过 user_id 关联。不要在 student 表里重复存 username 和 password,否则以后做“修改密码”要同时改两张表,而且管理员禁用账号时不知道该禁哪张表。把密码单独收在 user 表还有一个好处:加密逻辑只做一遍。密码不要明文存,哪怕毕设也要用 BCrypt 加密。你自己做实验数据无所谓,答辩老师一旦看到明文密码,印象分会掉一大截。

第二个决策是物理外键用不用。我的建议是不建物理外键,用逻辑外键加索引。物理外键在插入和删除时要维护约束,毕设里的假数据经常要先删职位再删企业、删了学生又删不掉投递记录,被外键卡住时老师就在旁边看你排查,场面很难看。用逻辑外键配合索引,查询性能有保证,代码里也能自己控制删除顺序,灵活性高很多。

2.3 八张表建表 SQL:一次能跑通的核心版本

下面是我一般会用的核心建表 SQL。student 和 company 的字段按各自需要增减,但身份证这类敏感字段毕设里能不加就不加,少一个字段少一堆校验代码。

-- 登录用户表:所有角色共用一张表,role 区分身份 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(255) NOT NULL COMMENT 'BCrypt加密后的密码', `role` tinyint NOT NULL DEFAULT '2' COMMENT '角色:1管理员 2学生 3企业', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1启用 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录用户表'; -- 学生信息表:user_id 关联 user.id,业务字段单独存放 CREATE TABLE `student` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL COMMENT '关联用户表', `name` varchar(50) DEFAULT NULL, `school` varchar(100) DEFAULT NULL, `major` varchar(100) DEFAULT NULL COMMENT '专业', `education` varchar(20) DEFAULT NULL COMMENT '学历', `phone` varchar(20) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; -- 企业信息表 CREATE TABLE `company` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL COMMENT '关联用户表', `company_name` varchar(100) NOT NULL, `industry` varchar(50) DEFAULT NULL COMMENT '所属行业', `city` varchar(50) DEFAULT NULL, `introduction` text COMMENT '公司简介', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业信息表'; -- 职位表:company_id 关联企业,status 控制上下架 CREATE TABLE `position` ( `id` int NOT NULL AUTO_INCREMENT, `company_id` int NOT NULL COMMENT '发布企业id', `title` varchar(100) NOT NULL COMMENT '职位名称', `city` varchar(50) DEFAULT NULL, `salary_min` int DEFAULT NULL COMMENT '薪资下限,单位K', `salary_max` int DEFAULT NULL, `education` varchar(20) DEFAULT NULL COMMENT '学历要求', `description` text COMMENT '职位描述', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1发布中 0已下线', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_company_id` (`company_id`), KEY `idx_city` (`city`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位表';
-- 简历文件表:只存文件路径和元信息,不存文件内容 CREATE TABLE `resume` ( `id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL COMMENT '学生信息表id', `file_name` varchar(255) DEFAULT NULL COMMENT '原始文件名', `file_path` varchar(255) NOT NULL COMMENT '服务器保存路径', `file_size` bigint DEFAULT NULL COMMENT '单位字节', `upload_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_student_id` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='简历文件表'; -- 投递记录表:一次投递一条记录,status 是业务核心列 CREATE TABLE `delivery` ( `id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `position_id` int NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0已投递 1被查看 2通知面试 3已录用 4已淘汰', `interview_time` datetime DEFAULT NULL COMMENT '面试安排时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_student_id` (`student_id`), KEY `idx_position_id` (`position_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投递记录表'; -- 收藏职位表:唯一约束保证一个学生对一个职位只能收藏一次 CREATE TABLE `favorite` ( `id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `position_id` int NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_position` (`student_id`, `position_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位收藏表';

说明:用户表用 role tinyint 而不是 varchar,是因为角色枚举以后做权限判断只需要比较数字,写进拦截器代码里也短。position 表给 city 建索引,是因为毕业生招聘系统里“城市筛选”几乎是必做功能,不带索引的数据量虽然小,但这种建模习惯能让答辩评委会觉得你懂索引的适用场景。resume 表只存路径不存文件内容,这非常关键,后面部署章节会回来说它。favorite 表加了唯一约束,收藏按钮就能用 insert ignore 实现“重复点击不报错”。

到这里八张表已经出现七张,第八张 announcement 公告表结构更简单,字段就 id、title、content、create_time。带着这套表结构,你可以对照手里的源码检查:如果源码把企业名称直接写到 position 表里而不是通过 company_id 关联,那说明当时是图省事,答辩时老师追问第三张表就很容易露馅。

2.4 投递状态机:从 0 到 4 的合法流转

投递状态是这个系统的业务核心,不能用随意字符串,我习惯用 tinyint 加数字状态。0 已投递、1 被查看、2 通知面试、3 已录用、4 已淘汰。合法流转规则是:投递后是 0;企业查看简历后从 0 变 1;企业安排面试后从 0 或 1 变 2;面试完成后从 2 变 3 或 4。注意 4 已淘汰不允许从 0 直接跳过来,因为简历没被查看就淘汰,对企业端来说不合业务流程,也会让学生端界面出现逻辑错乱。

状态变更在代码里用条件更新实现:

-- 企业安排面试:只有当前状态是0或1时才允许更新到2 UPDATE delivery SET status = 2, interview_time = '2025-06-10 10:00:00' WHERE id = #{deliveryId} AND status IN (0, 1);

这条 SQL 的精髓在 WHERE 条件里的status IN (0, 1)。它保证两个人同时操作同一条投递记录时,后执行的那条更新会因为条件不匹配而影响 0 行,天然避免了状态被覆盖。如果只在代码里做 if 判断再用无条件 UPDATE,并发情况下会出现很隐蔽的丢状态问题——这是很多教学项目源码不会写的细节,但面试官和答辩评委都爱问。

3. 用 Spring Boot 把核心接口写出来:拦截器、动态 SQL 与文件上传

设计表结构只是第一步,真正让系统能跑起来的还是接口层。这一章我直接给三段最核心的代码:登录拦截与角色路由、职位多条件搜索、简历上传。这三段代码覆盖了招聘系统 70% 的日常操作,把它们弄明白,剩下的页面就是数据展示。

3.1 项目骨架与依赖清单

在 IDEA 2024 里创建 web 项目,直接用 Spring Initializr,选 Java 8 或 17 都行,关键是要把下面这几个依赖加进 pom.xml。

<dependencies> <!-- Web启动器:内嵌Tomcat,页面和REST接口都靠它 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Thymeleaf模板引擎:服务端渲染页面,毕设演示最简单 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MyBatis与Spring Boot整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.23</version> </dependency> <!-- Lombok:减少getter/setter样板代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

版本号以你创建工程时 IDEA 拉到的稳定版为准。mybatis-spring-boot-starter 2.3.2 对应 Spring Boot 2.7 系列;如果你建出来的是 Spring Boot 3.x,要换 3.x 的 mybatis starter,不然启动时会报组件扫描失败。Lombok 不是必须,但建议保留,能让实体类短一半。

3.2 application.yml 里最容易影响联调的三个参数

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/recruit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 20MB max-request-size: 25MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.recruit.entity

这里的 serverTimezone=Asia/Shanghai 是必填项,MySQL 8.x 驱动如果拿不到时区会报 CST 相关异常。max-file-size 默认只有 1MB,不调大后面传简历必然翻车,我见过太多源码在这卡住。Druid 连接池在本地环境记得给初始连接数设小一点,默认配置在低配电脑上启动会慢好几秒。

3.3 登录拦截与角色路由:不用注解的最小实现

登录状态用 Session 存,拦截器负责两件事:没登录的重定向到登录页,登录了但角色不对的重定向到 403 页。我一般不用自定注解,直接按 URL 前缀判角色,简单直接。

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从Session里拿登录用户,拿不到说明未登录 User loginUser = (User) request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 路径前缀匹配角色:/admin 开头必须是管理员,/company 开头必须是企业 String uri = request.getRequestURI(); if (uri.startsWith("/admin") && loginUser.getRole() != 1) { response.sendRedirect(request.getContextPath() + "/403"); return false; } if (uri.startsWith("/company") && loginUser.getRole() != 3) { response.sendRedirect(request.getContextPath() + "/403"); return false; } return true; } }

注意 role 是 tinyint,这里直接用!= 1比较,所以建表时用数字枚举而不是字符串,在 Java 代码里会舒服很多。注册拦截器时有一个最常见的坑,下面这段配置:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/img/**"); } }

excludePathPatterns 里必须放行静态资源目录,否则浏览器加载样式表也被拦截,页面会变成纯 HTML 无排版。还有一点,Thymeleaf 渲染的页面里如果有静态资源引用路径拼错,浏览器会请求到/static/xxx而不是/css/xxx,同样会被拦截器拦下来跳登录页,表现就是“页面偶尔正常偶尔乱”,排查方向往往错。

3.4 职位搜索接口:MyBatis 动态 SQL 的三种写法对比

毕业生找工作时最常用的功能是按关键词、城市、薪资范围筛职位。这个查询条件不固定,不能用写死的 SELECT 语句,我一般用 MyBatis 的动态 SQL。

<select id="searchPositions" resultType="com.example.recruit.entity.Position"> select * from position <where> <if test="keyword != null and keyword != ''"> and (title like concat('%', #{keyword}, '%') or company_name like concat('%', #{keyword}, '%')) </if> <if test="city != null and city != ''"> and city = #{city} </if> <if test="salaryMin != null"> and salary_min &gt;= #{salaryMin} </if> </where> order by create_time desc </select>

参数说明:keyword 做模糊匹配,用 concat 拼接百分号而不是在 Java 代码里拼好再传,是为了防止用户输入百分号或下划线时把 LIKE 语义搅乱;city 用等值匹配,因为城市一般做下拉框而不是自由输入;salaryMin 是整数,注意这里用了&gt;=而不是>=,XML 文件里裸写大于号会解析报错,这是 MyBatis XML 最常见的语法坑。

另外两种写法供参考:一是用注解 @Select 直接拼字符串,只适合条件固定的接口;二是 MyBatis-Plus 的 LambdaQueryWrapper,代码里用 Java 方法链拼条件,能少写一半 XML,但如果你想让答辩评委看到你对 SQL 的控制力,动态 XML 是更好的选择。

3.5 简历上传:文件存储与访问这条路要打通

简历文件必须存到服务器磁盘,数据库只存路径。上传接口的核心代码:

@PostMapping("/resume/upload") public String upload(@RequestParam("file") MultipartFile file, @Value("${file.upload-dir}") String uploadDir, HttpSession session) { if (file.isEmpty()) { return "上传失败,文件为空"; } // 原始文件名只保留后缀,主名用UUID重生成,避免中文名和特殊字符问题 String originalFilename = file.getOriginalFilename(); String suffix = ""; if (originalFilename != null && originalFilename.contains(".")) { suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); } String filename = UUID.randomUUID().toString().replace("-", "") + suffix; // 目录不存在就创建,这是部署到Linux后最容易忽略的一步 File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(dir, filename)); } catch (IOException e) { e.printStackTrace(); return "上传失败"; } // 把 /files/xxx.pdf 这个访问路径存进resume表,页面直接拼URL预览 return "/files/" + filename; }

uploadDir 从配置文件读取而不是写死在代码里,是为了 Windows 本地和 Linux 服务器用两套配置。文件重命名用 UUID 是为了避免两个学生上传同名简历互相覆盖。transferTo 是 MultipartFile 的标准落盘方法,注意 transferTo 的目标父目录必须存在,否则会抛 IOException。

文件访问还需要一个映射配置,把/files/**指向磁盘目录:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir + "/"); } }

这段配置让浏览器可以直接访问服务器磁盘上的简历 PDF,实现“上传后点开预览”。addResourceLocations 必须以file:开头,且末尾要带/,两个细节缺一个都 404。很多源码在这用相对路径,本地跑正常,一部署就挂,原因就是相对路径的基准目录在 jar 启动时和 IDE 运行时不一样。

4. 从 IDEA 到一台能访问的服务器:本地 jar 启动、配置项与验收清单

代码在 IDE 里能跑,离“系统完成”还很远。我的验收标准很简单:不用 IDEA,用命令行能启动;换一台干净的机器能启动;部署到 Linux 服务器后,浏览器从外网能访问。这一章就是沿着这三条线走。

4.1 本地先过三关:jar 启动、端口占用、配置切换

mvn clean package -DskipTests java -jar target/recruit-0.0.1-SNAPSHOT.jar --server.port=8080

-DskipTests 跳过测试,不是因为测试没用,而是很多毕设工程的测试类里还引用着测试数据库,跑不起来会卡住打包。jar 启动成功后,本地浏览器访问 http://localhost:8080/login。如果端口被占用,换一个端口重新启动:java -jar target/recruit-0.0.1-SNAPSHOT.jar --server.port=8081。不要用 IDE 里 Run 一次就以为跑通了,jar 包启动才等同于服务器上的运行方式。

4.2 部署到服务器前必改的五个配置项

这五个配置不改,部署必出错。列成清单,逐条对。

配置项本地值服务器值不改的后果
server.port808080 或保持 8080 + Nginx 转发外网访问要带端口号
spring.datasource.urllocalhost:3306127.0.0.1:3306连不上数据库
file.upload-dirD:/temp/upload/opt/recruit/uploadLinux 上找不到路径
druid 连接池默认调小初始连接数低配服务器启动慢
日志级别root: infocom.example.recruit: debug出问题时日志看不清

其中数据库地址最容易写错:服务器上的 localhost 指的是服务器自己,不是你的电脑。很多人本地指向 localhost 能跑,部署后忘了改,页面一直报连接数据库失败。上传路径的改动我习惯放到 application.yml 里用${file.upload-dir}占位,启动时用命令行覆盖:

java -jar /opt/recruit/recruit.jar \ --file.upload-dir=/opt/recruit/upload \ --spring.datasource.url="jdbc:mysql://127.0.0.1:3306/recruit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"

这样不用重新打包就能在不同环境切换配置,也是答辩时可以讲一句的“部署技巧”。日志级别调到 debug 后,MyBatis 会打印每条 SQL 和参数,线上排查状态不对、查不到数据时省一大半时间。

4.3 Nginx 转发与服务器安全默认项

如果只想让系统能访问,jar 直接监听 80 端口就够了。但一台服务器以后可能还要跑其他 web 工程,我一般会在前面加一层 Nginx 做域名转发和静态资源缓存。

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

这段配置把 80 端口收到的请求转发到本机 8080 的 jar。服务器安全方面有几个默认动作:安全组只放行 80 和 443,MySQL 的 3306 端口绝不对公网开放;Linux 防火墙同样只放行必要端口;数据库密码换成强密码,不要用 root/123456 的组合放到公网服务器上。这些都是毕设答辩加分项,因为评委最常问的就是“你这系统放到公网安全吗”。

4.4 验证系统“真的能用”的四个验收动作

部署完不能只看首页能开就结束。我按用户真实操作路径列四个验收动作,任何一个失败都说明系统还差最后一公里。

验收动作预期结果失败排查方向
未登录访问 /student/home跳转登录页拦截器 exclude 配置是否放行过多
学生登录后投递一个职位个人中心显示“已投递”delivery 表是否插入成功
企业登录后变更投递状态学生端能看到“通知面试”动态 SQL 的 status 判断条件
上传简历后点击预览浏览器打开 PDF文件映射配置和磁盘权限

第四个动作最容易在 Linux 上失败。如果页面能上传但 URL 访问 404,先检查 uploadDir 目录是否存在且有写权限,再检查映射配置。这个排查过程如果你写成文档放在中期检查报告里,评委一眼就能看出你是真部署过还是只在 IDEA 里点过运行。

5. 避坑与排查:五个必踩的坑,每个都讲现象、原因、解决

这一章是我带学生做这个题目时实际遇到最多的五类问题,不是从报错文档里抄来的。每个按“现象 → 原因 → 解决”写,你照着排查比自己翻日志快得多。

5.1 上传 1MB 以上的简历报 500,页面没有明确错误提示

现象:PDF 小于 1MB 能传上去,大一点就报服务器内部错误,控制台出现 MaxUploadSizeExceededException。

原因:Spring Boot 内嵌 Tomcat 的 multipart 默认限制单文件 1MB,超过直接抛异常。

解决:在 application.yml 里调大限制,同时设置 max-request-size 比单文件上限稍大:

spring: servlet: multipart: max-file-size: 20MB max-request-size: 25MB

5.2 本地正常,部署到 Linux 后上传文件报 java.io.FileNotFoundException

现象:本地 Windows 上传文件没问题,部署到 Linux 云服务器后一上传就报 FileNotFoundException 或 No such file or directory。

原因:源码里把上传路径写死了,比如D:/temp/upload,或者用了相对路径。Linux 上没有 D 盘,相对路径的基准目录在 jar 启动时又经常不在预期位置。

解决:上传目录做成配置项,代码里用@Value("${file.upload-dir}")注入,启动时通过命令行参数指定绝对路径/opt/recruit/upload,并确保目录存在且有写权限。

5.3 登录后无限跳回登录页,像进入了死循环

现象:输对账号密码,请求页面后马上又跳回登录页,或者页面反复刷新。浏览器地址栏能看到 login 和主页来回跳。

原因:拦截器把登录页本身也拦截了。常见的两个触发点:excludePathPatterns 里漏了/login;或者登录请求是 POST/login,但页面跳转用的 GET/login没放行。

解决:检查拦截器注册配置,确保/login、/register、静态资源目录/css/**、/js/**、/img/**都在放行列表里。放行规则写成:

.excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/img/**", "/files/**")

这里把/files/**也放行,因为简历 PDF 预览的请求不该被登录拦截器拦,否则企业点开简历附件会被弹回登录页。

5.4 MyBatis 动态 SQL 里写 大于等于 直接报错

现象:XML 里写了and salary_min >= #{salaryMin},启动时提示 Error creating document instance 或解析 XML 失败。

原因:XML 文件里裸写<和>是非法字符,MyBatis 解析 mapper XML 时会当成标签处理。

解决:大于号和小于号都要转义。>转成&gt;,<转成&lt;,或者直接把整个比较条件包进 CDATA:

<if test="salaryMin != null"> and salary_min <![CDATA[ >= ]]> #{salaryMin} </if>

注意<if test="salaryMin != null">里的!=是合法的,因为它在属性值里而不是元素的文本内容里。分不清就统一用&gt;=这种转义写法,最不容易出错。

5.5 部署后启动报 CommunicationsException 或时区异常

现象:jar 在本地能启动,部署到服务器后连数据库报com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure,或者The server time zone value '�й���ʱ��' is unrecognized。

原因:两个问题叠加最常见。一是数据库地址写错,服务器上 localhost 指向自己;二是 JDBC URL 缺 serverTimezone 参数,MySQL 8 驱动拿不到 JVM 默认时区就会抛异常。

解决:数据库地址改成 127.0.0.1,URL 加上serverTimezone=Asia/Shanghai,驱动类用com.mysql.cj.jdbc.Driver。如果用的是 MySQL 5.7,驱动选com.mysql.jdbc.Driver,同样需要指定字符集:

jdbc:mysql://127.0.0.1:3306/recruit?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

6. 开题报告、中期检查与答辩准备:源码之外决定分数的那三张纸

标题里源码、答辩、开题报告、中期检查四件套,源码部分前面讲完了。最后一章专门说文档和答辩,因为代码能跑只是及格,这三张纸才是拉分项。

6.1 开题报告:把“为什么做”讲出工作量

开题报告最常见的错误是写成“我国就业形势严峻,所以做一个招聘系统”。这不是开题,是作文。我一般围绕三块写:选题背景里重点写“现有校招平台对学生和中小企业不友好”这两个切入点;国内外现状不空谈,去找两个同类系统说它们缺什么,缺的正好是你做的;技术路线画一条从 Web 前端到 Spring Boot 后端再到 MySQL 的数据流。这三块能写满两页,方案可行性分析才站得住。

6.2 中期检查:进度表比描述性文字有用

中期检查主持人最关心“你做了没有”。一张表三列,完胜大段描述:功能模块、完成状态、演示截图。我用过的表格大概这样:

模块状态验证方式
数据库设计与账号体系已完成建表脚本 + 三类角色登录演示
职位发布与搜索已完成企业端发布,学生端搜索
投递与状态流转完成 80%投递已通,面试通知待联调

剩余空间写一句“遇到的问题与解决”,比如 5.1 的上传限制问题,这就是真实的进度痕迹。

6.3 答辩演示:十分钟里让评委看到状态机

我建议的节奏是 5 分钟演示 + 4 分钟讲设计 + 1 分钟讲难点。演示要从学生视角走:登录、搜职位、投递、看状态;再切换企业账号,对同一份投递做“查看 → 面试 → 录用”。这三次状态变更把投递状态机完整演出来了,比讲十个 CRUD 页面都有说服力。代码部分不要逐行念,挑拦截器里 URI 前缀判角色那段,以及 2.4 的条件 UPDATE,这是整个系统设计浓度最高的两处。

6.4 现场演示的三个保护动作

演示现场最容易翻车的三件事,提前处理掉:用本地环境而不是线上服务器,现场网络不可控,本地最稳;准备一套演示账号和学生甲、企业乙的固定数据,角色齐全且状态各异;关闭浏览器通知和无关窗口。另外,不用依赖外部链接的字体库,把 CSS 和 JS 都放在项目里,断网也能完整渲染。第一次答辩我就吃过亏,现场网络一抖动页面白屏了大半,后来凡是演示,我都默认按断网环境准备。

至于功能扩展,这题目还能往深处做的方向不少:简历解析、职位推荐、web 端实时视频面试,都可以写进展望。但基础版本先把投递状态机和文件上传走通,这套骨架树立起来,后面加任何模块都有地方挂。这是我带这题目几年下来最大的体会,也希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询