You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
6.0 KiB
152 lines
6.0 KiB
package com.mydreamjob.skill;
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.mydreamjob.profile.StudentProfile;
|
|
import com.mydreamjob.profile.StudentProfileRepository;
|
|
import com.mydreamjob.skill.domain.SkillAlias;
|
|
import com.mydreamjob.skill.dto.SkillCloudItem;
|
|
import com.mydreamjob.skill.dto.SkillCloudResponse;
|
|
import com.mydreamjob.skill.dto.SkillMatchResponse;
|
|
import com.mydreamjob.skill.repository.SkillAliasRepository;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
@Slf4j
|
|
@Service
|
|
@Transactional(readOnly = true)
|
|
public class SkillMatchService {
|
|
|
|
private static final int TOP_LIMIT = 10;
|
|
|
|
private final SkillCloudQueryService skillCloudQueryService;
|
|
private final StudentProfileRepository studentProfileRepository;
|
|
private final SkillAliasRepository skillAliasRepository;
|
|
private final SkillNameNormalizer skillNameNormalizer;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public SkillMatchService(SkillCloudQueryService skillCloudQueryService,
|
|
StudentProfileRepository studentProfileRepository,
|
|
SkillAliasRepository skillAliasRepository,
|
|
SkillNameNormalizer skillNameNormalizer,
|
|
ObjectMapper objectMapper) {
|
|
this.skillCloudQueryService = skillCloudQueryService;
|
|
this.studentProfileRepository = studentProfileRepository;
|
|
this.skillAliasRepository = skillAliasRepository;
|
|
this.skillNameNormalizer = skillNameNormalizer;
|
|
this.objectMapper = objectMapper;
|
|
}
|
|
|
|
public SkillMatchResponse match(Long studentUserId,
|
|
Long industryId,
|
|
Long occupationId,
|
|
Long categoryId) {
|
|
SkillCloudResponse cloud = skillCloudQueryService.query(industryId, occupationId, categoryId);
|
|
int actualTopSize = Math.min(TOP_LIMIT, cloud.items().size());
|
|
List<SkillCloudItem> topSkills = cloud.items().subList(0, actualTopSize);
|
|
Set<String> studentSkills = loadStudentSkills(studentUserId);
|
|
Map<Long, List<SkillAlias>> aliasesBySkillId = loadAliases(topSkills);
|
|
|
|
List<SkillCloudItem> matchedSkills = new ArrayList<>();
|
|
List<SkillCloudItem> missingSkills = new ArrayList<>();
|
|
for (SkillCloudItem skill : topSkills) {
|
|
if (matches(skill, studentSkills, aliasesBySkillId.getOrDefault(skill.skillId(), List.of()))) {
|
|
matchedSkills.add(skill);
|
|
} else {
|
|
missingSkills.add(skill);
|
|
}
|
|
}
|
|
|
|
int matchScore = actualTopSize == 0
|
|
? 0
|
|
: (int) Math.round(matchedSkills.size() * 100.0 / actualTopSize);
|
|
return new SkillMatchResponse(
|
|
matchScore,
|
|
matchedSkills.size(),
|
|
actualTopSize,
|
|
matchedSkills,
|
|
missingSkills
|
|
);
|
|
}
|
|
|
|
private Set<String> loadStudentSkills(Long studentUserId) {
|
|
if (studentUserId == null) {
|
|
return Set.of();
|
|
}
|
|
|
|
StudentProfile profile = studentProfileRepository.findByUserId(studentUserId).orElse(null);
|
|
if (profile == null || profile.getSkillsJson() == null || profile.getSkillsJson().isBlank()) {
|
|
return Set.of();
|
|
}
|
|
|
|
try {
|
|
List<String> parsed = objectMapper.readValue(
|
|
profile.getSkillsJson(),
|
|
new TypeReference<List<String>>() {}
|
|
);
|
|
if (parsed == null || parsed.isEmpty()) {
|
|
return Set.of();
|
|
}
|
|
|
|
Set<String> normalizedSkills = new HashSet<>();
|
|
for (String skill : parsed) {
|
|
if (skill == null) {
|
|
continue;
|
|
}
|
|
String normalized = skillNameNormalizer.normalize(skill);
|
|
if (!normalized.isBlank()) {
|
|
normalizedSkills.add(normalized);
|
|
}
|
|
}
|
|
return Set.copyOf(normalizedSkills);
|
|
} catch (Exception exception) {
|
|
log.warn("Failed to parse student skills JSON: profileId={}, errorType={}",
|
|
profile.getId(), exception.getClass().getSimpleName());
|
|
return Set.of();
|
|
}
|
|
}
|
|
|
|
private Map<Long, List<SkillAlias>> loadAliases(List<SkillCloudItem> topSkills) {
|
|
Collection<Long> skillIds = topSkills.stream()
|
|
.map(SkillCloudItem::skillId)
|
|
.filter(java.util.Objects::nonNull)
|
|
.toList();
|
|
if (skillIds.isEmpty()) {
|
|
return Map.of();
|
|
}
|
|
|
|
Map<Long, List<SkillAlias>> aliasesBySkillId = new HashMap<>();
|
|
for (SkillAlias alias : skillAliasRepository.findAllBySkillIdInOrderByIdAsc(skillIds)) {
|
|
aliasesBySkillId.computeIfAbsent(alias.getSkillId(), ignored -> new ArrayList<>()).add(alias);
|
|
}
|
|
return aliasesBySkillId;
|
|
}
|
|
|
|
private boolean matches(SkillCloudItem skill,
|
|
Set<String> studentSkills,
|
|
List<SkillAlias> aliases) {
|
|
if (containsNormalized(studentSkills, skill.nameZh())
|
|
|| containsNormalized(studentSkills, skill.nameEn())
|
|
|| containsNormalized(studentSkills, skill.skillCode())) {
|
|
return true;
|
|
}
|
|
return aliases.stream().anyMatch(alias -> containsNormalized(studentSkills, alias.getAlias()));
|
|
}
|
|
|
|
private boolean containsNormalized(Set<String> studentSkills, String candidate) {
|
|
if (candidate == null) {
|
|
return false;
|
|
}
|
|
String normalized = skillNameNormalizer.normalize(candidate);
|
|
return !normalized.isBlank() && studentSkills.contains(normalized);
|
|
}
|
|
}
|
|
|