简介:面向毕业设计场景的求职招聘系统 Java 源码包,适合计算机相关专业学生完成课程设计或毕设项目时参考。项目包含完整后端业务代码、配置文件、数据库建表脚本与说明文档,涵盖招聘信息发布、职位检索、简历投递等常见功能模块,可作为从零搭建类似系统的骨架。压缩包共 54 个文件,以 41 个 Java 源文件为主,辅以 XML、YAML、Properties 等配置类文件,另含 pom.xml 和 Maven 包装器,可通过命令行快速构建启动;SQL 文件提供数据库初始化脚本,MD 文档用于环境搭建与使用指引。整个包仅 93KB,轻量紧凑,便于下载与本地部署。目前已有 168 人学习下载。对需要快速理解分层架构、数据库表设计以及前后端数据交互方式的读者来说,这份源码能直接导入 IDE 运行,同时可根据自身需求修改扩展,节省从零编写的时间。
1. 一个zip包里的求职招聘系统从哪开始看
拿到一个名叫"JAVA毕业设计求职招聘系统源码+sql文件.zip"的压缩包,多数人的第一反应是打开Controller看代码,但真正决定这套求职招聘系统能跑多远的是那个.sql文件。表结构决定功能边界:能不能按薪资筛选、能不能防重复投递、能不能延展到面试日程管理,全在字段、索引和初始化数据里。下面按数据模型、Java分层、SQL导入与部署、安全与扩展这条顺序,把一套可运行的求职招聘系统拆到能让它在本地浏览器里当场跑起来的程度,适合正在做Java毕设、或需要快速搭招聘类后台的工程师。
2. SQL文件的表结构拆解:四张实体表与两张业务表
2.1 打开SQL后先看哪张表:用户表与角色字段
打开SQL文件先扫一眼所有CREATE TABLE语句。求职招聘系统的数据模型通常围绕"求职者、招聘者发布职位、投递简历、平台撮合"展开,核心表不会超过六张。
| 表名 | 职责 | 关键外键 |
|---|---|---|
| t_user | 登录账号与角色 | 无 |
| t_company | 企业信息 | company_id 关联 t_user.id |
| t_job | 职位发布 | company_id 关联 t_company.id |
| t_resume | 简历详情 | user_id 关联 t_user.id |
| t_application | 投递记录 | user_id + job_id 双外键 |
| t_favorite | 职位收藏 | user_id + job_id 双外键 |
先看t_user,因为整个系统的权限控制从这里起步。一份典型的建表语句长这样:
CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT '登录名', password varchar(100) NOT NULL COMMENT '密码,MD5/BCrypt密文', role tinyint(4) NOT NULL DEFAULT '0' COMMENT '0求职者 1招聘者 2管理员', phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';两个容易被忽略的细节。第一,role用tinyint而不用varchar,拦截器里做Integer.compare(user.getRole(), 2)比字符串比较快,也不会出现'user'和'User'并存导致权限分叉的脏数据。第二,password列宽给到100,MD5是32位、BCrypt是60位,varchar(100)意味着后期从MD5升级到BCrypt不用改表结构。SQL文件怎么查看这类细节?命令行执行mysql -u root -p -e "USE db_job; SHOW CREATE TABLE t_user\G;",比在DBeaver里看图形模式快得多。
2.2 职位表与投递表的边界设计
职位表是招聘者的核心实体,设计时最容易踩的坑是把薪资存成字符串。下面这种两个整型字段的写法更利于检索筛选:
CREATE TABLE t_job ( id int(11) NOT NULL AUTO_INCREMENT, company_id int(11) NOT NULL COMMENT '关联 t_company.id', job_name varchar(100) NOT NULL, salary_min int(6) NOT NULL DEFAULT '0' COMMENT '月薪下限,单位元', salary_max int(6) NOT NULL DEFAULT '0' COMMENT '月薪上限,单位元', location varchar(100) DEFAULT NULL, requirement text COMMENT '职位描述:学历/经验/技能', publish_time datetime DEFAULT CURRENT_TIMESTAMP, status tinyint(4) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', PRIMARY KEY (id), KEY idx_company_id (company_id), KEY idx_status_time (status, publish_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位发布表';薪资用salary_min/salary_max两个int字段,筛选时写WHERE salary_min >= 8000 AND salary_max <= 20000,走索引的范围扫描;如果拼成salary_range varchar(50)存'8k-15k',就只能LIKE模糊匹配,数据量过万之后响应时间明显拉长。idx_status_time复合索引被很多毕设漏掉,首页默认按status=1过滤再按publish_time DESC排序,没有这个索引就会出现filesort慢查询。
投递记录表是整套系统的状态机核心:
CREATE TABLE t_application ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT '求职者,关联 t_user.id', job_id int(11) NOT NULL COMMENT '职位,关联 t_job.id', resume_id int(11) DEFAULT NULL COMMENT '投递时用的简历', status tinyint(4) NOT NULL DEFAULT '1' COMMENT '1投递 2被查看 3面试 4拒绝 5入职 0取消', apply_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_job (user_id, job_id), KEY idx_status_time (status, apply_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投递记录表';uk_user_job唯一索引从数据库层面封死了同一个人投同一职位两次的脏数据,Java层即使忘了判重,插入也会报Duplicate entry。拿到手的SQL文件如果没有这条唯一约束,建议手动补一条ALTER TABLE t_application ADD UNIQUE INDEX uk_user_job (user_id, job_id);。ON UPDATE CURRENT_TIMESTAMP让状态变更时间自动刷新,Service层少写一个setter。
2.3 初始化数据、字符集与索引取舍
一套能直接演示的SQL文件还要带初始化数据,下面这种INSERT在毕设项目里很常见:
INSERT INTO t_user (id, username, password, role, phone) VALUES (1, 'admin', MD5('123456'), 2, '13800000000'), (2, 'jobseeker01', MD5('123456'), 0, '13900000000'), (3, 'hr01', MD5('123456'), 1, '13700000000'); INSERT INTO t_company (id, company_name, industry, address, user_id) VALUES (1, '星辰科技', '互联网', '北京市海淀区', 3);这份数据直接决定浏览器打开后的第一屏效果。如果SQL文件只有表结构没有INSERT,页面就是空的。导入后建议先跑一条LEFT JOIN验证关联是否打通:
SELECT u.username, u.role, c.company_name FROM t_user u LEFT JOIN t_company c ON u.id = c.user_id;字符集是个隐形坑。老项目喜欢DEFAULT CHARSET=utf8,但MySQL的utf8是3字节实现,存emoji或生僻字会报Incorrect string value,简历里又容易混入特殊字符,建议统一改成utf8mb4。已经建好的库可以用ALTER TABLE t_user CHARSET=utf8mb4, MODIFY username varchar(50) CHARACTER SET utf8mb4;逐表调整。索引取舍上,基础索引必须有,但别每个字段都加,INSERT和UPDATE时索引列的更新成本会叠加,复合索引等真实查询模式出现后再补。
3. Java源码结构:Controller-Service-Mapper三层怎么组织
3.1 先看配置文件和Controller的请求入口
解压zip后先看pom.xml确认技术栈。常见做法分两种:Spring Boot单体应用用内嵌Tomcat,mvn spring-boot:run就能跑;传统SSM项目打成war扔进外置Tomcat。看依赖里的parent声明就能区分,Spring Boot的pom会有一段类似这样的内容:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent>确认是Spring Boot后,打开src/main/resources/application.yml看数据库连接配置:
spring: datasource: url: jdbc:mysql://localhost:3306/db_job?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone=Asia/Shanghai不能省。MySQL 8默认时区与JVM不一致时,插入的create_time会差8个小时,本地开发一般直接配Asia/Shanghai。characterEncoding=utf8在这里是连接串参数,MySQL驱动会按UTF-8协商,和表结构的utf8mb4不冲突。
Controller层能看出代码风格。整洁的写法是请求映射、参数校验、结果封装放Controller,业务判断下沉到Service。如果代码里Controller直接操作数据库,那后续维护成本会很高。
3.2 投递简历这条链路的代码实现
用"投递简历"这个动作串联三层。先看Controller:
@RestController @RequestMapping("/api/apply") public class ApplicationController { @Autowired private ApplicationService applicationService; @PostMapping public Result<String> create(@RequestBody ApplyDTO dto, HttpSession session) { User user = (User) session.getAttribute("loginUser"); if (user == null) { return Result.error(401, "请先登录"); } return applicationService.apply(user.getId(), dto); } }Service层对应的实现:
@Service public class ApplicationServiceImpl implements ApplicationService { @Override @Transactional(rollbackFor = Exception.class) public Result<String> apply(Integer userId, ApplyDTO dto) { Job job = jobMapper.selectById(dto.getJobId()); if (job == null || job.getStatus() != 1) { return Result.error("职位不存在或已下架"); } Resume resume = resumeMapper.selectByUserId(userId); if (resume == null) { return Result.error("请先完善简历"); } Integer count = applicationMapper.countByUserAndJob(userId, dto.getJobId()); if (count != null && count > 0) { return Result.error("您已投递过该职位"); } Application application = new Application(); application.setUserId(userId); application.setJobId(dto.getJobId()); application.setResumeId(resume.getId()); application.setStatus(1); applicationMapper.insert(application); return Result.success("投递成功"); } }参数说明:@Transactional(rollbackFor = Exception.class)锁住整个投递过程,Spring的@Transactional默认只回滚RuntimeException,遇到受检异常不会回滚,所以显式声明更安全。job.getStatus() != 1对应SQL文件里status字段的语义。重复投递检查依赖代码里的count查询加表上的uk_user_job唯一索引双重保护,高并发下两个请求同时通过count检查的场景,靠数据库兜底。Result泛型统一包装返回结果,前端只处理一套JSON结构。
Mapper接口和XML的对应关系:
public interface ApplicationMapper { int countByUserAndJob(@Param("userId") Integer userId, @Param("jobId") Integer jobId); }<select id="countByUserAndJob" resultType="int"> SELECT COUNT(*) FROM t_application WHERE user_id = #{userId} AND job_id = #{jobId} </select>#{userId}是预编译占位符,对应JDBC的PreparedStatement参数绑定,不是字符串拼接。如果SQL文件里的表名是user、apply这类MySQL保留字,XML里要加反引号转义,否则直接报语法错误。
3.3 登录拦截与角色权限
求职招聘系统一般有三个角色:求职者、招聘者、管理员。求职者搜索职位投简历,招聘者发布职位查收简历,管理员做审核与统计。代码上推荐全局拦截器做登录校验,Controller方法上用注解区分角色。
定义一个角色注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int value() default 0; }拦截器实现:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getServletContext().getContextPath() + "/login.html"); return false; } RequireRole role = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class); if (role != null && user.getRole() != role.value()) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}"); return false; } return true; } }先判断handler是否为HandlerMethod再取注解,静态资源请求不会误伤。注册时排除登录和注册接口:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register"); } }3.4 Mapper的动态SQL写法:职位搜索
职位搜索通常要同时满足关键词、城市、薪资三个条件,全部写死会导致XML里每个方法都是一套新组合。用MyBatis动态SQL更干净:
<select id="searchJobs" resultType="com.example.entity.Job"> SELECT id, job_name, salary_min, salary_max, location FROM t_job <where> <if test="keyword != null and keyword != ''"> AND job_name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="location != null and location != ''"> AND location = #{location} </if> <if test="minSalary != null"> AND salary_max >= #{minSalary} </if> </where> ORDER BY publish_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签自动去掉第一个多余的AND,避免拼出WHERE AND job_name...的语法错误。LIKE CONCAT拼接时不要把%直接写进参数,否则空值会变成%%匹配全部。>=是XML里的转义写法,裸写>=会导致XML解析失败。分页参数#{offset}, #{pageSize}写起来直观,项目再大一点可以引入PageHelper或MyBatis-Plus的分页插件。
4. SQL文件导入与项目部署:从zip到浏览器能打开
4.1 导入前的环境检查:JDK、MySQL与sql文件目录
先在命令行确认三件事:JDK、MySQL、Maven是否到位。
java -version mvn -v mysql --version如果java -version显示的是JRE而不是JDK,Spring Boot项目在mvn编译时会找不到javac直接报错。mvn -v报错但java -version正常,通常是JAVA_HOME环境变量没配到JDK路径,需要在系统环境变量里补上JAVA_HOME并追加到PATH。sql文件如果很大,几十万行INSERT,用记事本打开会卡死,推荐用VS Code或Notepad++,只看结构时执行head -50 db_job.sql更快。
4.2 命令行导入SQL文件:mysql < db_job.sql全流程
假设SQL文件名叫db_job.sql,在解压目录执行:
mysql -u root -p < db_job.sql回车后输入MySQL密码。如果SQL文件里没有CREATE DATABASE语句,需要手动建库:
CREATE DATABASE IF NOT EXISTS db_job DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE db_job; SOURCE /path/to/db_job.sql;几个常见注意点:<是shell重定向不是MySQL命令;MySQL 8默认认证插件是caching_sha2_password,如果驱动还是老的com.mysql.jdbc.Driver,连接会报Public Key Retrieval is not allowed,改成com.mysql.cj.jdbc.Driver即可。报ERROR 1064语法错误时,先确认MySQL版本与SQL文件的兼容性,5.7的语法在8.0里部分写法会变。导入卡住时执行SHOW PROCESSLIST;看是否有别的session占着表锁。
导入完成后验证:
mysql -u root -p -e "USE db_job; SHOW TABLES; SELECT COUNT(*) FROM t_user;"4.3 application.yml数据库连接与启动报错对照表
数据库导入完不是终点,Java项目里的连接参数不一定对得上,这几类报错最常见。
| 报错信息 | 原因 | 处理方式 |
|---|---|---|
| Access denied for user 'root'@'localhost' | application.yml密码与实际不一致 | 修改spring.datasource.password |
| Unknown database 'db_job' | 库名不一致 | 与SQL文件头部CREATE DATABASE对比 |
| Communications link failure | 端口或驱动版本不对 | 确认URL端口3306,驱动换com.mysql.cj.jdbc.Driver |
| Public Key Retrieval is not allowed | MySQL8认证方式 | URL加allowPublicKeyRetrieval=true |
| Table 'db_job.hibernate_sequence' doesn't exist | JPA主键策略不匹配 | 推荐把ddl-auto改为validate继续排查 |
改完执行:
mvn spring-boot:run看到Started Application in x.xxx seconds表示启动成功。默认端口在server.port配置,浏览器访问http://localhost:8080。
4.4 用DBeaver图形化导入sql文件
不习惯命令行的路径:DBeaver新建MySQL连接,填主机、端口、用户名密码,测试连接成功后选中目标数据库,打开SQL编辑器面板,选择db_job.sql文件执行脚本。sql文件太大时,单条执行会超时报错,需要在连接驱动属性里临时开启一个选项支持一次执行多条语句,本地导入完再关掉;服务端连接不建议开启这个选项,它会让批量注入的风险变高。
导入完成后打开浏览器,用初始化数据里的管理员账号登录后台。如果在登录时看到密码错误或500,优先排查密码加密算法与代码是否一致,比如代码用DigestUtils.md5DigestAsHex,而SQL文件存的是明文或另一种哈希格式,两者对不上就会一直报错。
5. 从源码到可扩展:密码加密、权限细化与演示数据验证
5.1 把MD5换成BCrypt
毕设SQL文件里常见MD5('123456')这种写法,MD5查彩虹表即可还原,生产环境不够用。BCrypt每次加密的盐随机,相同密码每次加密结果不同。改造时引入spring-security-crypto依赖,然后把Service里的密码校验改成:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encoded = encoder.encode("123456"); boolean match = encoder.matches("123456", encoded);改动之后数据库里原来的MD5值全部失效,SQL文件的初始化数据也要同步生成BCrypt哈希,其他密文格式混在一起会导致老账号无法登录。
5.2 用一条SQL验证整条业务链路
部署完环境,建议跑一条关联查询验证数据模型闭合:
SELECT u.username AS 求职者, j.job_name AS 职位, c.company_name AS 企业, DATE_FORMAT(a.apply_time, '%Y-%m-%d') AS 投递日期, CASE a.status WHEN 1 THEN '已投递' WHEN 2 THEN '已查看' WHEN 3 THEN '面试' WHEN 4 THEN '已拒绝' WHEN 5 THEN '已入职' ELSE '取消' END AS 状态 FROM t_application a JOIN t_user u ON a.user_id = u.id JOIN t_job j ON a.job_id = j.id JOIN t_company c ON j.company_id = c.id ORDER BY a.apply_time DESC;这条语句能同时验证t_user、t_job、t_company、t_application四张表的外键关系和初始化数据是否一致。查询结果为空时,检查投递记录里插的user_id是否在t_user里实际存在,这种外键悬空问题在改过初始化数据的项目里很常见。
5.3 给SQL文件补齐数据隔离条件
招聘者后台最容易出现越权:一个招聘者看到了所有企业的职位列表。用户表里已经存了role,招聘者的user_id关联t_company.id,那么职位查询就要把当前登录用户的company_id带进SQL,加一行AND company_id = #{companyId}。这个条件加上之后,基于这套源码扩展多租户就有了雏形,后续做角色权限也好、做企业空间隔离也好,都是从这一行筛选条件开始的。
本文还有配套的精品资源,点击获取