package com.mydreamjob.talentinvitation; import com.mydreamjob.talentdiscovery.CandidateKeyGenerator; import com.mydreamjob.talentdiscovery.TalentDiscoveryCandidateSource; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Map; import java.util.List; /** 在当前 V41 合格候选池内重算 opaque key,绝不通过 key 反解学生 ID。 */ @Component @RequiredArgsConstructor public class TalentInvitationCandidateResolver { private final TalentDiscoveryCandidateSource candidateSource; private final CandidateKeyGenerator candidateKeyGenerator; public Resolution resolve(Long companyId, Long jobId, String candidateKey, LocalDateTime capturedNow) { if (companyId == null || jobId == null || candidateKey == null || capturedNow == null) { return Resolution.none(); } byte[] expected = candidateKey.getBytes(StandardCharsets.UTF_8); Long match = null; int matches = 0; List candidates = candidateSource.findEligibleCandidates(companyId); List candidateIds = candidates.stream().map(TalentDiscoveryCandidateSource.CandidateSeed::studentUserId).toList(); Map preferences = safeMap(candidateSource.findPreferences(candidateIds)); Map riasec = safeMap(candidateSource.findLatestRiasec(candidateIds)); Map ability = safeMap(candidateSource.findLatestAbility(candidateIds, capturedNow)); LocalDate bucket = capturedNow.toLocalDate(); for (TalentDiscoveryCandidateSource.CandidateSeed candidate : candidates) { // 候选池要求至少有一个可用推荐信号;缺失信号不得按 0 分放行。 if (!hasSignal(candidate.studentUserId(), preferences, riasec, ability)) continue; String generated = candidateKeyGenerator.generate(companyId, jobId, candidate.studentUserId(), bucket); if (MessageDigest.isEqual(expected, generated.getBytes(StandardCharsets.UTF_8))) { match = candidate.studentUserId(); matches++; } } return matches == 1 ? new Resolution(match, true) : Resolution.none(); } private boolean hasSignal(Long studentId, Map preferences, Map riasec, Map ability) { TalentDiscoveryCandidateSource.PreferenceSignal p = preferences.get(studentId); if (p != null && (p.expectedSalaryMin() != null || p.expectedSalaryMax() != null || !p.cities().isEmpty() || !p.employmentTypes().isEmpty() || !p.industries().isEmpty() || !p.jobTypes().isEmpty())) return true; TalentDiscoveryCandidateSource.ScoreSignal i = riasec.get(studentId); if (i != null && i.scores() != null && !i.scores().isEmpty()) return true; TalentDiscoveryCandidateSource.ScoreSignal a = ability.get(studentId); return a != null && a.scores() != null && !a.scores().isEmpty(); } private Map safeMap(Map value) { return value == null ? Map.of() : value; } public record Resolution(Long studentUserId, boolean matched) { public static Resolution none() { return new Resolution(null, false); } } }