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

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);
}
}