1. 项目背景与意义
随着高校毕业生人数逐年增加,求职市场竞争日益激烈。传统的高校就业信息发布方式(如公告栏、邮件群发)存在信息分散、更新不及时、匹配效率低等问题。同时,企业招聘方也面临难以精准触达目标院校和专业学生的困境。
因此,开发一个基于Spring Boot的高校求职招聘信息管理系统具有重要的现实意义:
- 对学生而言:提供一个集中、实时、个性化的招聘信息平台,方便快捷地获取与自身专业、意向匹配的岗位,并能在线投递简历、查看进度。
- 对企业而言:能够高效地发布和管理招聘需求,精准筛选符合条件的学生简历,降低招聘成本,提升人才引进效率。
- 对高校就业指导中心而言:实现招聘信息的数字化、流程化管理,便于统计就业数据、分析就业趋势,为就业指导工作提供数据支持。
本系统旨在构建一个连接学生、企业和学校的三方平台,利用现代Web技术提升求职招聘全流程的效率和体验。
2. 技术栈选型
系统采用前后端分离的架构模式,后端基于Spring Boot框架构建,具体技术栈如下:
2.1 后端技术
- 核心框架:Spring Boot 2.7.x
- 安全框架:Spring Security + JWT (JSON Web Token)
- 数据持久层:Spring Data JPA (默认使用Hibernate实现)
- 数据库:MySQL 8.0
- 缓存:Redis (用于会话管理、热点数据缓存)
- API文档:SpringDoc OpenAPI 3 (Swagger UI)
- 构建工具:Maven
- 单元测试:JUnit 5, Mockito
2.2 前端技术(建议)
- 框架:Vue 3 或 React 18
- UI组件库:Element Plus (Vue) 或 Ant Design (React)
- 状态管理:Pinia (Vue) 或 Redux Toolkit (React)
- HTTP客户端:Axios
- 构建工具:Vite
2.3 开发与部署
- 版本控制:Git
- 容器化:Docker, Docker Compose
- 持续集成/部署:Jenkins 或 GitLab CI
3. 系统核心功能模块设计
3.1 用户角色与权限
- 学生用户:注册/登录、完善个人简历、浏览/搜索/收藏职位、在线投递、查看投递进度、接收面试通知。
- 企业用户:注册/认证、发布/管理招聘职位、筛选/管理收到的简历、发送面试邀请、查看候选人信息。
- 管理员(学校就业中心):管理所有用户、审核企业资质、管理全局招聘信息、统计分析就业数据、发布公告。
3.2 核心功能模块
- 用户认证与授权模块:实现基于角色的登录、注册、JWT令牌签发与验证。
- 个人信息与简历模块:学生维护教育背景、技能、项目经历等;企业维护公司信息。
- 招聘信息管理模块:企业发布职位(职位名称、要求、薪资、地点等);管理员审核;学生浏览与搜索。
- 简历投递与处理模块:学生投递简历,企业查看、筛选(标记为“已读”、“感兴趣”、“不合适”等)。
- 消息通知模块:系统站内信、邮件通知(如投递成功、面试邀请、状态更新)。
- 数据统计与分析模块:管理员后台查看职位发布量、投递量、热门行业/岗位等统计数据。
4. 数据库核心表设计
以下是几个核心实体及其主要字段的简要设计:
-- 用户表 (统一存储学生、企业、管理员) CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `email` varchar(100) NOT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `user_type` tinyint NOT NULL COMMENT '用户类型:1-学生,2-企业,3-管理员', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 学生详细信息表 CREATE TABLE `student_profile` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '关联用户ID', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `gender` tinyint DEFAULT NULL COMMENT '性别', `school` varchar(100) DEFAULT NULL COMMENT '学校', `major` varchar(100) DEFAULT NULL COMMENT '专业', `education` varchar(20) DEFAULT NULL COMMENT '学历', `graduation_year` int DEFAULT NULL COMMENT '毕业年份', `resume_url` varchar(500) DEFAULT NULL COMMENT '简历文件URL', `skills` text COMMENT '技能标签,JSON数组存储', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`), CONSTRAINT `fk_student_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生详细信息表'; -- 企业信息表 CREATE TABLE `company` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '关联用户ID', `company_name` varchar(200) NOT NULL COMMENT '公司名称', `logo` varchar(500) DEFAULT NULL COMMENT '公司Logo URL', `industry` varchar(100) DEFAULT NULL COMMENT '所属行业', `scale` varchar(50) DEFAULT NULL COMMENT '公司规模', `address` varchar(500) DEFAULT NULL COMMENT '公司地址', `description` text COMMENT '公司简介', `verified` tinyint DEFAULT '0' COMMENT '认证状态:0-未认证,1-已认证', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_id` (`user_id`), CONSTRAINT `fk_company_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业信息表'; -- 招聘职位表 CREATE TABLE `job_position` ( `id` bigint NOT NULL AUTO_INCREMENT, `company_id` bigint NOT NULL COMMENT '发布企业ID', `title` varchar(200) NOT NULL COMMENT '职位名称', `description` text NOT NULL COMMENT '职位描述', `requirements` text COMMENT '职位要求', `salary_range` varchar(100) DEFAULT NULL COMMENT '薪资范围', `work_location` varchar(200) DEFAULT NULL COMMENT '工作地点', `job_type` tinyint DEFAULT NULL COMMENT '职位类型:1-全职,2-实习', `status` tinyint DEFAULT '1' COMMENT '状态:0-已下线,1-招聘中', `publish_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', PRIMARY KEY (`id`), KEY `idx_company_id` (`company_id`), KEY `idx_status_publish` (`status`, `publish_time`), CONSTRAINT `fk_job_company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招聘职位表'; -- 简历投递记录表 CREATE TABLE `job_application` ( `id` bigint NOT NULL AUTO_INCREMENT, `job_id` bigint NOT NULL COMMENT '职位ID', `student_id` bigint NOT NULL COMMENT '投递学生ID', `resume_url` varchar(500) DEFAULT NULL COMMENT '投递时使用的简历URL', `status` tinyint DEFAULT '0' COMMENT '状态:0-已投递,1-已查看,2-感兴趣,3-通知面试,4-不合适', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '投递时间', `feedback` text COMMENT '企业反馈', PRIMARY KEY (`id`), UNIQUE KEY `uk_job_student` (`job_id`, `student_id`), KEY `idx_student_status` (`student_id`, `status`), KEY `idx_job_status` (`job_id`, `status`), CONSTRAINT `fk_application_job` FOREIGN KEY (`job_id`) REFERENCES `job_position` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_application_student` FOREIGN KEY (`student_id`) REFERENCES `student_profile` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='职位申请记录表';5. 核心代码示例 (Spring Boot)
5.1 实体类 (JobPosition.java)
package com.example.careersystem.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "job_position") @Data public class JobPosition { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "company_id", nullable = false) private Company company; @Column(nullable = false, length = 200) private String title; @Column(nullable = false, columnDefinition = "TEXT") private String description; @Column(columnDefinition = "TEXT") private String requirements; @Column(name = "salary_range", length = 100) private String salaryRange; @Column(name = "work_location", length = 200) private String workLocation; @Column(name = "job_type") private Integer jobType; // 1-全职,2-实习 private Integer status = 1; // 0-下线,1-招聘中 @Column(name = "publish_time") private LocalDateTime publishTime = LocalDateTime.now(); // 省略 getter/setter (由 Lombok @Data 生成) }5.2 数据访问层 (JobPositionRepository.java)
package com.example.careersystem.repository; import com.example.careersystem.entity.JobPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface JobPositionRepository extends JpaRepository<JobPosition, Long> { // 根据企业ID分页查询职位 Page<JobPosition> findByCompanyId(Long companyId, Pageable pageable); // 根据状态查询职位 List<JobPosition> findByStatus(Integer status); // 复杂查询示例:根据关键词搜索职位 (标题或描述中包含) @Query("SELECT jp FROM JobPosition jp WHERE jp.status = 1 AND " + "(LOWER(jp.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(jp.description) LIKE LOWER(CONCAT('%', :keyword, '%')))") Page<JobPosition> searchByKeyword(@Param("keyword") String keyword, Pageable pageable); // 统计某企业的职位数量 Long countByCompanyId(Long companyId); }5.3 服务层 (JobPositionService.java)
package com.example.careersystem.service; import com.example.careersystem.entity.JobPosition; import com.example.careersystem.repository.JobPositionRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service public class JobPositionService { @Autowired private JobPositionRepository jobPositionRepository; // 发布新职位 @Transactional public JobPosition createJobPosition(JobPosition jobPosition) { // 这里可以添加业务逻辑,如权限校验、数据校验等 return jobPositionRepository.save(jobPosition); } // 分页查询所有招聘中的职位 public Page<JobPosition> getActivePositions(Pageable pageable) { return jobPositionRepository.findByStatus(1, pageable); } // 根据关键词搜索职位 public Page<JobPosition> searchPositions(String keyword, Pageable pageable) { if (keyword == null || keyword.trim().isEmpty()) { return getActivePositions(pageable); } return jobPositionRepository.searchByKeyword(keyword.trim(), pageable); } // 根据ID获取职位详情 public Optional<JobPosition> getPositionById(Long id) { return jobPositionRepository.findById(id); } // 更新职位信息 @Transactional public JobPosition updateJobPosition(Long id, JobPosition updatedPosition) { return jobPositionRepository.findById(id).map(position -> { position.setTitle(updatedPosition.getTitle()); position.setDescription(updatedPosition.getDescription()); position.setRequirements(updatedPosition.getRequirements()); position.setSalaryRange(updatedPosition.getSalaryRange()); position.setWorkLocation(updatedPosition.getWorkLocation()); position.setJobType(updatedPosition.getJobType()); // 注意:status 和 publishTime 通常有单独的接口控制 return jobPositionRepository.save(position); }).orElseThrow(() -> new RuntimeException("职位不存在,ID: " + id)); } // 下线职位(逻辑删除) @Transactional public void deactivatePosition(Long id) { jobPositionRepository.findById(id).ifPresent(position -> { position.setStatus(0); jobPositionRepository.save(position); }); } }5.4 控制层 (JobPositionController.java)
package com.example.careersystem.controller; import com.example.careersystem.entity.JobPosition; import com.example.careersystem.service.JobPositionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @RestController @RequestMapping("/api/positions") @Tag(name = "招聘职位管理", description = "招聘职位的增删改查与搜索接口") public class JobPositionController { @Autowired private JobPositionService jobPositionService; @PostMapping @Operation(summary = "发布新职位", description = "企业用户发布一个新的招聘职位") public ResponseEntity<JobPosition> createPosition(@Valid @RequestBody JobPosition jobPosition) { JobPosition created = jobPositionService.createJobPosition(jobPosition); return ResponseEntity.ok(created); } @GetMapping @Operation(summary = "分页获取招聘中的职位", description = "获取所有状态为“招聘中”的职位列表") public ResponseEntity<Page<JobPosition>> getActivePositions( @PageableDefault(size = 10, sort = "publishTime,desc") Pageable pageable) { Page<JobPosition> positions = jobPositionService.getActivePositions(pageable); return ResponseEntity.ok(positions); } @GetMapping("/search") @Operation(summary = "搜索职位", description = "根据关键词搜索职位(标题或描述)") public ResponseEntity<Page<JobPosition>> searchPositions( @RequestParam(required = false) String keyword, @PageableDefault(size = 10) Pageable pageable) { Page<JobPosition> positions = jobPositionService.searchPositions(keyword, pageable); return ResponseEntity.ok(positions); } @GetMapping("/{id}") @Operation(summary = "获取职位详情", description = "根据ID获取职位详细信息") public ResponseEntity<JobPosition> getPositionById(@PathVariable Long id) { return jobPositionService.getPositionById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PutMapping("/{id}") @Operation(summary = "更新职位信息", description = "更新指定ID的职位信息") public ResponseEntity<JobPosition> updatePosition(@PathVariable Long id, @Valid @RequestBody JobPosition updatedPosition) { JobPosition po