本招聘系统采用基于Spring Boot的微服务架构设计,结合MySQL数据库和Redis缓存,构建高可用、可扩展的招聘平台。系统分为以下几个主要模块:
sql
CREATE TABLE `user` ( |
|
`id` bigint NOT NULL AUTO_INCREMENT, |
|
`username` varchar(50) NOT NULL, |
|
`password` varchar(100) NOT NULL, |
|
`email` varchar(100) NOT NULL, |
|
`phone` varchar(20) DEFAULT NULL, |
|
`user_type` tinyint NOT NULL COMMENT '1-求职者 2-招聘方', |
|
`status` tinyint NOT NULL DEFAULT '1' COMMENT '0-禁用 1-正常', |
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
|
PRIMARY KEY (`id`), |
|
UNIQUE KEY `idx_username` (`username`), |
|
UNIQUE KEY `idx_email` (`email`) |
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
java
@Service |
|
@RequiredArgsConstructor |
|
public class UserServiceImpl implements UserService { |
|
private final UserMapper userMapper; |
|
private final PasswordEncoder passwordEncoder; |
|
private final RedisTemplate |
|
@Override |
|
@Transactional |
|
public User register(UserRegisterDTO registerDTO) { |
|
// 校验用户名和邮箱是否已存在 |
|
if (userMapper.selectByUsername(registerDTO.getUsername()) != null) { |
|
throw new BusinessException("用户名已存在"); |
|
} |
|
if (userMapper.selectByEmail(registerDTO.getEmail()) != null) { |
|
throw new BusinessException("邮箱已被注册"); |
|
} |
|
// 创建用户 |
|
User user = new User(); |
|
BeanUtils.copyProperties(registerDTO, user); |
|
user.setPassword(passwordEncoder.encode(registerDTO.getPassword())); |
|
user.setCreateTime(LocalDateTime.now()); |
|
userMapper.insert(user); |
|
return user; |
|
} |
|
@Override |
|
public String login(UserLoginDTO loginDTO) { |
|
User user = userMapper.selectByUsername(loginDTO.getUsername()); |
|
if (user == null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { |
|
throw new BusinessException("用户名或密码错误"); |
|
} |
|
// 生成JWT令牌 |
|
Map |
|
claims.put("userId", user.getId()); |
|
claims.put("userType", user.getUserType()); |
|
return JwtUtils.generateToken(claims); |
|
} |
|
} |
java
@Configuration |
|
public class ElasticsearchConfig { |
|
@Bean |
|
public RestHighLevelClient client() { |
|
return new RestHighLevelClient( |
|
RestClient.builder(new HttpHost("elasticsearch", 9200, "http"))); |
|
} |
|
} |
|
@Service |
|
@RequiredArgsConstructor |
|
public class JobSearchService { |
|
private final RestHighLevelClient client; |
|
private final ObjectMapper objectMapper; |
|
public SearchResult searchJobs(JobSearchDTO searchDTO) { |
|
try { |
|
SearchRequest searchRequest = new SearchRequest("job_index"); |
|
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); |
|
// 构建布尔查询 |
|
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); |
|
// 关键词查询 |
|
if (StringUtils.isNotBlank(searchDTO.getKeyword())) { |
|
boolQuery.must(QueryBuilders.multiMatchQuery(searchDTO.getKeyword(), |
|
"title", "description", "requirements")); |
|
} |
|
// 城市过滤 |
|
if (StringUtils.isNotBlank(searchDTO.getCity())) { |
|
boolQuery.filter(QueryBuilders.termQuery("city.keyword", searchDTO.getCity())); |
|
} |
|
// 薪资范围过滤 |
|
if (searchDTO.getMinSalary() != null) { |
|
boolQuery.filter(QueryBuilders.rangeQuery("minSalary").gte(searchDTO.getMinSalary())); |
|
} |
|
if (searchDTO.getMaxSalary() != null) { |
|
boolQuery.filter(QueryBuilders.rangeQuery("maxSalary").lte(searchDTO.getMaxSalary())); |
|
} |
|
sourceBuilder.query(boolQuery); |
|
sourceBuilder.from((searchDTO.getPageNum() - 1) * searchDTO.getPageSize()); |
|
sourceBuilder.size(searchDTO.getPageSize()); |
|
// 排序 |
|
if (StringUtils.isNotBlank(searchDTO.getSortField())) { |
|
SortOrder order = "desc".equalsIgnoreCase(searchDTO.getSortOrder()) |
|
? SortOrder.DESC : SortOrder.ASC; |
|
sourceBuilder.sort(searchDTO.getSortField(), order); |
|
} |
|
searchRequest.source(sourceBuilder); |
|
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); |
|
return parseSearchResult(response); |
|
} catch (IOException e) { |
|
throw new RuntimeException("搜索失败", e); |
|
} |
|
} |
|
private SearchResult parseSearchResult(SearchResponse response) throws JsonProcessingException { |
|
SearchResult result = new SearchResult(); |
|
// 解析总命中数 |
|
result.setTotal(response.getHits().getTotalHits().value); |
|
// 解析职位列表 |
|
List |
|
for (SearchHit hit : response.getHits().getHits()) { |
|
Job job = objectMapper.readValue(hit.getSourceAsString(), Job.class); |
|
jobList.add(job); |
|
} |
|
result.setJobs(jobList); |
|
return result; |
|
} |
|
} |
java
@Service |
|
@RequiredArgsConstructor |
|
public class ResumeParseService { |
|
private final Tika tika; |
|
private final OcrService ocrService; |
|
public ResumeDTO parseResume(MultipartFile file) throws IOException { |
|
// 判断文件类型 |
|
String contentType = file.getContentType(); |
|
if (contentType == null) { |
|
throw new BusinessException("不支持的文件类型"); |
|
} |
|
ResumeDTO resume = new ResumeDTO(); |
|
if ("application/pdf".equals(contentType)) { |
|
// PDF文件处理 |
|
try (InputStream stream = file.getInputStream()) { |
|
// 使用Tika提取文本内容 |
|
String text = tika.parseToString(stream); |
|
// 解析文本内容 |
|
parseTextContent(text, resume); |
|
} |
|
} else if (contentType.startsWith("image/")) { |
|
// 图片文件处理(如扫描的简历) |
|
String text = ocrService.recognizeText(file.getBytes()); |
|
parseTextContent(text, resume); |
|
} else { |
|
throw new BusinessException("不支持的文件类型"); |
|
} |
|
return resume; |
|
} |
|
private void parseTextContent(String text, ResumeDTO resume) { |
|
// 使用正则表达式提取关键信息 |
|
extractBasicInfo(text, resume); |
|
extractEducation(text, resume); |
|
extractWorkExperience(text, resume); |
|
extractSkills(text, resume); |
|
} |
|
private void extractBasicInfo(String text, ResumeDTO resume) { |
|
// 提取姓名 |
|
Pattern namePattern = Pattern.compile("姓名[::]\\s*([^\\n]+)"); |
|
Matcher matcher = namePattern.matcher(text); |
|
if (matcher.find()) { |
|
resume.setName(matcher.group(1).trim()); |
|
} |
|
// 提取电话 |
|
Pattern phonePattern = Pattern.compile("电话[::]\\s*(\\d{11})"); |
|
matcher = phonePattern.matcher(text); |
|
if (matcher.find()) { |
|
resume.setPhone(matcher.group(1)); |
|
} |
|
// 提取邮箱 |
|
Pattern emailPattern = Pattern.compile("邮箱[::]\\s*([\\w.-]+@[\\w.-]+\\.\\w+)"); |
|
matcher = emailPattern.matcher(text); |
|
if (matcher.find()) { |
|
resume.setEmail(matcher.group(1)); |
|
} |
|
} |
|
// 其他解析方法... |
|
} |
java
@Service |
|
@RequiredArgsConstructor |
|
public class MatchingService { |
|
private final JobService jobService; |
|
private final ResumeService resumeService; |
|
@Async |
|
public CompletableFuture |
|
Job job = jobService.getJobById(jobId); |
|
Resume resume = resumeService.getResumeById(resumeId); |
|
MatchResult result = new MatchResult(); |
|
result.setJobId(jobId); |
|
result.setResumeId(resumeId); |
|
// 1. 基础信息匹配 |
|
matchBasicInfo(job, resume, result); |
|
// 2. 技能匹配 |
|
matchSkills(job, resume, result); |
|
// 3. 工作经历匹配 |
|
matchWorkExperience(job, resume, result); |
|
// 4. 教育背景匹配 |
|
matchEducation(job, resume, result); |
|
// 计算总分 |
|
calculateTotalScore(result); |
|
return CompletableFuture.completedFuture(result); |
|
} |
|
private void matchBasicInfo(Job job, Resume resume, MatchResult result) { |
|
// 城市匹配 |
|
if (job.getCity().equals(resume.getExpectedCity())) { |
|
result.setCityMatch(true); |
|
result.addScore("city", 10); |
|
} |
|
// 职位类型匹配 |
|
if (job.getJobType().equals(resume.getJobType())) { |
|
result.setJobTypeMatch(true); |
|
result.addScore("jobType", 10); |
|
} |
|
// 薪资匹配 |
|
if (resume.getExpectedSalary() != null) { |
|
if (resume.getExpectedSalary() >= job.getMinSalary() |
|
&& resume.getExpectedSalary() <= job.getMaxSalary()) { |
|
result.setSalaryMatch(true); |
|
result.addScore("salary", 15); |
|
} |
|
} |
|
} |
|
private void matchSkills(Job job, Resume resume, MatchResult result) { |
|
if (CollectionUtils.isEmpty(job.getRequiredSkills()) || CollectionUtils.isEmpty(resume.getSkills())) { |
|
return; |
|
} |
|
int matchCount = 0; |
|
for (String requiredSkill : job.getRequiredSkills()) { |
|
if (resume.getSkills().contains(requiredSkill)) { |
|
matchCount++; |
|
} |
|
} |
|
double matchRatio = (double) matchCount / job.getRequiredSkills().size(); |
|
int skillScore = (int) (matchRatio * 30); |
|
result.setSkillMatchRatio(matchRatio); |
|
result.addScore("skill", skillScore); |
|
} |
|
private void calculateTotalScore(MatchResult result) { |
|
int totalScore = result.getDetails().values().stream() |
|
.mapToInt(Integer::intValue) |
|
.sum(); |
|
result.setTotalScore(totalScore); |
|
// 评级 |
|
if (totalScore >= 80) { |
|
result.setMatchLevel("高度匹配"); |
|
} else if (totalScore >= 50) { |
|
result.setMatchLevel("一般匹配"); |
|
} else { |
|
result.setMatchLevel("不匹配"); |
|
} |
|
} |
|
} |
java
@Service |
|
@RequiredArgsConstructor |
|
public class JobCacheService { |
|
private final RedisTemplate |
|
private final JobService jobService; |
|
private static final String HOT_JOB_PREFIX = "hot_job:"; |
|
private static final String HOT_JOB_RANK = "hot_job_rank"; |
|
@Scheduled(fixedRate = 30 * 60 * 1000) // 每30分钟更新一次 |
|
public void updateHotJobs() { |
|
// 1. 从数据库获取最近30天点击量最高的100个职位 |
|
List |
|
// 2. 更新缓存 |
|
redisTemplate.delete(HOT_JOB_RANK); |
|
// 3. 使用ZSET存储热门职位排名 |
|
for (int i = 0; i < hotJobs.size(); i++) { |
|
Job job = hotJobs.get(i); |
|
redisTemplate.opsForZSet().add(HOT_JOB_RANK, job.getId().toString(), 100 - i); |
|
} |
|
// 4. 缓存前20个热门职位详情 |
|
for (int i = 0; i < 20 && i < hotJobs.size(); i++) { |
|
Job job = hotJobs.get(i); |
|
redisTemplate.opsForValue().set(HOT_JOB_PREFIX + job.getId(), job, 1, TimeUnit.HOURS); |
|
} |
|
} |
|
public List |
|
// 先从缓存获取排名 |
|
Set |
|
if (CollectionUtils.isEmpty(jobIds)) { |
|
return Collections.emptyList(); |
|
} |
|
// 获取职位详情 |
|
List |
|
for (String jobId : jobIds) { |
|
Job job = (Job) redisTemplate.opsForValue().get(HOT_JOB_PREFIX + jobId); |
|
if (job != null) { |
|
jobs.add(job); |
|
} |
|
} |
|
// 如果缓存不足,从数据库补充 |
|
if (jobs.size() < topN) { |
|
List |
|
.filter(id -> !redisTemplate.hasKey(HOT_JOB_PREFIX + id)) |
|
.map(Long::valueOf) |
|
.collect(Collectors.toList()); |
|
if (!needFetchIds.isEmpty()) { |
|
List |
|
for (Job job : dbJobs) { |
|
redisTemplate.opsForValue().set(HOT_JOB_PREFIX + job.getId(), job, 1, TimeUnit.HOURS); |
|
jobs.add(job); |
|
} |
|
} |
|
} |
|
return jobs; |
|
} |
|
} |
java
@Service |
|
@RequiredArgsConstructor |
|
public class JobApplyService { |
|
private final JobApplyMapper jobApplyMapper; |
|
private final ResumeService resumeService; |
|
private final NotificationService notificationService; |
|
@GlobalTransactional |
|
@Override |
|
public void applyJob(Long jobId, Long userId) { |
|
// 1. 创建申请记录 |
|
JobApply apply = new JobApply(); |
|
apply.setJobId(jobId); |
|
apply.setUserId(userId); |
|
apply.setStatus(ApplyStatus.PENDING.getCode()); |
|
apply.setCreateTime(LocalDateTime.now()); |
|
jobApplyMapper.insert(apply); |
|
// 2. 检查简历是否完整 |
|
Resume resume = resumeService.getResumeByUserId(userId); |
|
if (resume == null || !resume.isComplete()) { |
|
throw new BusinessException("请先完善简历"); |
|
} |
|
// 3. 发送通知 |
|
notificationService.sendApplyNotification(jobId, userId); |
|
// 4. 更新职位申请人数(模拟异常测试分布式事务) |
|
// int i = 1 / 0; // 测试用,取消注释可触发回滚 |
|
jobService.incrementApplyCount(jobId); |
|
} |
|
} |
dockerfile
# 基础镜像 |
|
FROM openjdk:11-jre-slim |
|
# 设置工作目录 |
|
WORKDIR /app |
|
# 复制jar包 |
|
COPY target/job-service.jar /app/job-service.jar |
|
# 暴露端口 |
|
EXPOSE 8080 |
|
# 启动命令 |
|
ENTRYPOINT ["java", "-jar", "job-service.jar"] |
yaml
apiVersion: apps/v1 |
|
kind: Deployment |
|
metadata: |
|
name: job-service |
|
spec: |
|
replicas: 3 |
|
selector: |
|
matchLabels: |
|
app: job-service |
|
template: |
|
metadata: |
|
labels: |
|
app: job-service |
|
spec: |
|
containers: |
|
- name: job-service |
|
image: registry.example.com/job-service:1.0.0 |
|
ports: |
|
- containerPort: 8080 |
|
env: |
|
- name: SPRING_PROFILES_ACTIVE |
|
value: "prod" |
|
- name: SPRING_DATASOURCE_URL |
|
valueFrom: |
|
secretKeyRef: |
|
name: db-secret |
|
key: url |
|
resources: |
|
requests: |
|
cpu: "500m" |
|
memory: "1Gi" |
|
limits: |
|
cpu: "1000m" |
|
memory: "2Gi" |
|
--- |
|
apiVersion: v1 |
|
kind: Service |
|
metadata: |
|
name: job-service |
|
spec: |
|
selector: |
|
app: job-service |
|
ports: |
|
- protocol: TCP |
|
port: 80 |
|
targetPort: 8080 |
|
type: ClusterIP |
Java招聘系统源码实现了从用户管理、职位发布、简历投递到智能匹配的完整招聘流程。系统采用微服务架构,具备良好的扩展性和可维护性。通过Elasticsearch实现高效搜索,利用Redis缓存热点数据,结合分布式事务保证数据一致性。
未来可扩展方向: