forked from baskbuddy/zhangshun
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.
215 lines
21 KiB
215 lines
21 KiB
package com.mydreamjob.recommendation;
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.mydreamjob.application.ApplicationRepository;
|
|
import com.mydreamjob.audit.AuditContext;
|
|
import com.mydreamjob.audit.AuditService;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyService;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobRepository;
|
|
import com.mydreamjob.job.JobService;
|
|
import com.mydreamjob.preference.StudentPreference;
|
|
import com.mydreamjob.preference.StudentPreferenceRepository;
|
|
import com.mydreamjob.recommendation.dto.*;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
import org.springframework.data.domain.*;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.math.*;
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
|
|
/** V39 规则型岗位推荐。只读取测评聚合结果,不读取题面、答案或候选评分快照。 */
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class JobRecommendationService {
|
|
private static final ObjectMapper JSON = new ObjectMapper();
|
|
private static final Set<String> RIASEC = Set.of("R","I","A","S","E","C");
|
|
private static final Set<String> ABILITY = Set.of("LOGIC","COMMUNICATION","EXECUTION","LEARNING");
|
|
private static final Set<String> FEEDBACK = Set.of("NOT_INTERESTED","REASON_HELPFUL","REASON_NOT_HELPFUL");
|
|
private static final Set<String> DECISIONS = Set.of("APPROVE", "REJECT");
|
|
private static final Set<String> REJECT_REASONS = Set.of("TAXONOMY_MISMATCH", "WEIGHT_INVALID", "CONTENT_INCOMPLETE", "OTHER");
|
|
private final JobRecommendationTaxonomyRepository taxonomyRepository;
|
|
private final JobRecommendationProfileRepository profileRepository;
|
|
private final StudentJobRecommendationFeedbackRepository feedbackRepository;
|
|
private final JobRepository jobRepository;
|
|
private final JobService jobService;
|
|
private final CompanyService companyService;
|
|
private final UserRepository userRepository;
|
|
private final StudentPreferenceRepository preferenceRepository;
|
|
private final ApplicationRepository applicationRepository;
|
|
private final AuditService auditService;
|
|
private final JdbcTemplate jdbcTemplate;
|
|
@Value("${recommendation.assessment.enabled:false}") private boolean assessmentEnabled;
|
|
|
|
@Transactional(readOnly = true)
|
|
public List<TaxonomyResponse> taxonomies() {
|
|
return taxonomyRepository.findByEnabledTrueOrderBySortOrderAscCodeAsc().stream().map(t -> TaxonomyResponse.builder()
|
|
.code(t.getCode()).parentCode(t.getParentCode()).type(t.getTaxonomyType()).displayName(t.getDisplayName()).build()).toList();
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public RecommendationProfileResponse companyProfile(Long userId, Long jobId) {
|
|
assertCompanyOwns(userId, jobId);
|
|
return profileRepository.findByJobId(jobId).map(this::profileView)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_NOT_FOUND));
|
|
}
|
|
|
|
@Transactional
|
|
public RecommendationProfileResponse upsertCompanyProfile(Long userId, Long jobId, RecommendationProfileRequest req, AuditContext ctx) {
|
|
assertCompanyOwns(userId, jobId);
|
|
validateTaxonomy(req.getIndustryCode(), req.getJobFamilyCode());
|
|
validateMaps(req.getRiasecWeights(), req.getAbilityExpectations());
|
|
JobRecommendationProfile p = profileRepository.findByJobId(jobId).orElse(null);
|
|
if (p != null && !Objects.equals(p.getVersion(), req.getExpectedVersion())) {
|
|
throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_CONCURRENT_UPDATE);
|
|
}
|
|
if (p == null && req.getExpectedVersion() != 0L) throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_CONCURRENT_UPDATE);
|
|
if (p == null) p = JobRecommendationProfile.builder().jobId(jobId).build();
|
|
p.setIndustryCode(req.getIndustryCode()); p.setJobFamilyCode(req.getJobFamilyCode());
|
|
p.setRiasecWeightsJson(write(req.getRiasecWeights())); p.setAbilityExpectationsJson(write(req.getAbilityExpectations()));
|
|
p.setEnabled(req.getEnabled());
|
|
if (Boolean.TRUE.equals(req.getEnabled())) { p.setReviewStatus(JobRecommendationProfile.PENDING_REVIEW); p.setReviewReason(null); p.setReviewedAt(null); p.setReviewedByUserId(null); }
|
|
else p.setReviewStatus(JobRecommendationProfile.DISABLED);
|
|
p = profileRepository.saveAndFlush(p);
|
|
auditService.log(userId, "JOB_RECOMMENDATION_PROFILE_UPDATED", "JOB_RECOMMENDATION_PROFILE", p.getId(),
|
|
"{\"jobId\":" + jobId + ",\"status\":\"" + p.getReviewStatus() + "\"}", ctx);
|
|
return profileView(p);
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public Page<RecommendationProfileResponse> adminProfiles(Long adminId, int page, int size, String status) {
|
|
requireAdmin(adminId); validatePage(page, size);
|
|
Page<JobRecommendationProfile> result = status == null || status.isBlank()
|
|
? profileRepository.findAll(PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "updatedAt")))
|
|
: profileRepository.findByReviewStatusOrderByUpdatedAtDesc(status, PageRequest.of(page, size));
|
|
return result.map(this::profileView);
|
|
}
|
|
|
|
@Transactional
|
|
public RecommendationProfileResponse decide(Long adminId, Long jobId, RecommendationDecisionRequest req, AuditContext ctx) {
|
|
requireAdmin(adminId);
|
|
if (!DECISIONS.contains(req.getDecision()) || ("REJECT".equals(req.getDecision()) && !REJECT_REASONS.contains(req.getReasonCode())))
|
|
throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);
|
|
JobRecommendationProfile p = profileRepository.findByJobId(jobId)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_NOT_FOUND));
|
|
if (!Objects.equals(p.getVersion(), req.getExpectedVersion())) throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_CONCURRENT_UPDATE);
|
|
if (!JobRecommendationProfile.PENDING_REVIEW.equals(p.getReviewStatus())) throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID_REVIEW_STATE);
|
|
p.setReviewStatus("APPROVE".equals(req.getDecision()) ? JobRecommendationProfile.APPROVED : JobRecommendationProfile.REJECTED);
|
|
p.setReviewReason("REJECT".equals(req.getDecision()) ? req.getReasonCode() : null);
|
|
p.setReviewedByUserId(adminId); p.setReviewedAt(LocalDateTime.now()); p = profileRepository.saveAndFlush(p);
|
|
auditService.log(adminId, "JOB_RECOMMENDATION_PROFILE_REVIEWED", "JOB_RECOMMENDATION_PROFILE", p.getId(),
|
|
"{\"jobId\":" + jobId + ",\"status\":\"" + p.getReviewStatus() + "\"}", ctx);
|
|
return profileView(p);
|
|
}
|
|
|
|
@Transactional
|
|
public RecommendationFeedbackResponse feedback(Long studentId, Long jobId, RecommendationFeedbackRequest req, AuditContext ctx) {
|
|
requireStudent(studentId); jobService.getPublicJob(jobId);
|
|
if (!FEEDBACK.contains(req.getFeedbackType()) || req.getIdempotencyKey().length() > 64)
|
|
throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);
|
|
String key = studentId + ":" + jobId + ":" + req.getFeedbackType() + ":" + Optional.ofNullable(req.getReasonType()).orElse("");
|
|
Optional<StudentJobRecommendationFeedback> replay = feedbackRepository.findByStudentUserIdAndIdempotencyKey(studentId, req.getIdempotencyKey());
|
|
if (replay.isPresent()) {
|
|
StudentJobRecommendationFeedback old = replay.get();
|
|
if (!Objects.equals(old.getFeedbackKey(), key)) throw new BusinessException(ErrorCode.RECOMMENDATION_FEEDBACK_IDEMPOTENCY_CONFLICT);
|
|
return feedbackView(old);
|
|
}
|
|
StudentJobRecommendationFeedback f = feedbackRepository.findByStudentUserIdAndFeedbackKey(studentId, key).orElse(null);
|
|
if (f == null) f = StudentJobRecommendationFeedback.builder().studentUserId(studentId).jobId(jobId).feedbackType(req.getFeedbackType()).reasonType(req.getReasonType()).feedbackKey(key).idempotencyKey(req.getIdempotencyKey()).build();
|
|
else { f.setStatus(StudentJobRecommendationFeedback.ACTIVE); f.setIdempotencyKey(req.getIdempotencyKey()); }
|
|
try { f = feedbackRepository.saveAndFlush(f); } catch (DataIntegrityViolationException e) { throw new BusinessException(ErrorCode.RECOMMENDATION_FEEDBACK_CONCURRENT_UPDATE); }
|
|
auditService.log(studentId, "JOB_RECOMMENDATION_FEEDBACK_RECORDED", "JOB", jobId,
|
|
"{\"jobId\":" + jobId + ",\"status\":\"ACTIVE\"}", ctx);
|
|
return feedbackView(f);
|
|
}
|
|
|
|
@Transactional
|
|
public void revokeNotInterested(Long studentId, Long jobId, Long expectedVersion, AuditContext ctx) {
|
|
requireStudent(studentId);
|
|
StudentJobRecommendationFeedback f = feedbackRepository.findByStudentUserIdAndJobIdAndFeedbackTypeAndStatus(studentId, jobId, "NOT_INTERESTED", StudentJobRecommendationFeedback.ACTIVE)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.RECOMMENDATION_FEEDBACK_NOT_FOUND));
|
|
if (!Objects.equals(f.getVersion(), expectedVersion)) throw new BusinessException(ErrorCode.RECOMMENDATION_FEEDBACK_CONCURRENT_UPDATE);
|
|
f.setStatus(StudentJobRecommendationFeedback.REVOKED); feedbackRepository.saveAndFlush(f);
|
|
auditService.log(studentId, "JOB_RECOMMENDATION_NOT_INTERESTED_REVOKED", "JOB", jobId, "{\"jobId\":" + jobId + ",\"status\":\"REVOKED\"}", ctx);
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public Page<JobRecommendationItem> recommend(Long studentId, int page, int size) {
|
|
requireStudent(studentId); validatePage(page, size); LocalDateTime now = LocalDateTime.now();
|
|
Set<Long> applied = new HashSet<>(applicationRepository.findDistinctJobIdsByStudentUserId(studentId));
|
|
Set<Long> hidden = feedbackRepository.findByStudentUserIdAndStatusAndFeedbackType(studentId, StudentJobRecommendationFeedback.ACTIVE, "NOT_INTERESTED")
|
|
.stream().map(StudentJobRecommendationFeedback::getJobId).collect(Collectors.toSet());
|
|
StudentPreference pref = preferenceRepository.findByStudentUserId(studentId).orElse(null);
|
|
if (!assessmentEnabled) return fallback(page, size, applied, hidden, pref);
|
|
Map<String, BigDecimal> riasec = latestRiasec(studentId);
|
|
Map<String, BigDecimal> ability = latestAbility(studentId, now);
|
|
if (riasec.isEmpty() && ability.isEmpty() && pref == null) return fallback(page, size, applied, hidden, null);
|
|
List<Job> jobs = jobRepository.findAllById(profileRepository.findByReviewStatusAndEnabledTrue(JobRecommendationProfile.APPROVED).stream().map(JobRecommendationProfile::getJobId).toList());
|
|
Map<Long, Job> jobsById = jobs.stream().collect(Collectors.toMap(Job::getId, j -> j));
|
|
List<JobRecommendationItem> items = profileRepository.findByReviewStatusAndEnabledTrue(JobRecommendationProfile.APPROVED).stream()
|
|
.map(p -> scored(p, jobsById.get(p.getJobId()), pref, riasec, ability, applied, hidden)).filter(Objects::nonNull)
|
|
.sorted(Comparator.comparing(JobRecommendationItem::getScore).reversed().thenComparing(JobRecommendationItem::getJobId)).toList();
|
|
return slice(items, page, size);
|
|
}
|
|
|
|
private JobRecommendationItem scored(JobRecommendationProfile p, Job j, StudentPreference pref, Map<String, BigDecimal> riasec, Map<String, BigDecimal> ability, Set<Long> applied, Set<Long> hidden) {
|
|
if (j == null || !"PUBLISHED".equals(j.getStatus()) || applied.contains(j.getId()) || hidden.contains(j.getId()) || !hardPreferenceMatch(pref, j)) return null;
|
|
List<BigDecimal> components = new ArrayList<>(); List<BigDecimal> weights = new ArrayList<>(); List<String> reasons = new ArrayList<>();
|
|
BigDecimal preference = preferenceScore(pref, p, j); if (preference != null) { components.add(preference); weights.add(BigDecimal.valueOf(40)); reasons.add("岗位偏好匹配"); }
|
|
BigDecimal r = weighted(riasec, mapDecimal(p.getRiasecWeightsJson()), RIASEC); if (r != null) { components.add(r); weights.add(BigDecimal.valueOf(35)); reasons.add("职业兴趣匹配"); }
|
|
BigDecimal a = abilityScore(ability, mapDecimal(p.getAbilityExpectationsJson())); if (a != null) { components.add(a); weights.add(BigDecimal.valueOf(25)); reasons.add("能力要求匹配"); }
|
|
if (weights.isEmpty()) return null;
|
|
BigDecimal top = BigDecimal.ZERO, bottom = BigDecimal.ZERO;
|
|
for (int i=0;i<weights.size();i++) { top=top.add(components.get(i).multiply(weights.get(i))); bottom=bottom.add(weights.get(i)); }
|
|
return JobRecommendationItem.builder().jobId(j.getId()).title(j.getTitle()).city(j.getCity()).employmentType(j.getEmploymentType()).salaryMin(j.getSalaryMin()).salaryMax(j.getSalaryMax()).mode("PERSONALIZED").score(top.divide(bottom,2,RoundingMode.HALF_UP)).reasons(reasons).build();
|
|
}
|
|
private Page<JobRecommendationItem> fallback(int page,int size,Set<Long> applied,Set<Long> hidden,StudentPreference pref) {
|
|
List<JobRecommendationItem> all = jobRepository.findByStatusOrderByCreatedAtDesc("PUBLISHED").stream().filter(j -> !applied.contains(j.getId()) && !hidden.contains(j.getId()) && hardPreferenceMatch(pref,j)).map(j -> JobRecommendationItem.builder().jobId(j.getId()).title(j.getTitle()).city(j.getCity()).employmentType(j.getEmploymentType()).salaryMin(j.getSalaryMin()).salaryMax(j.getSalaryMax()).mode("FALLBACK").reasons(List.of("最新公开岗位")).build()).toList();
|
|
return slice(all,page,size);
|
|
}
|
|
private <T> Page<T> slice(List<T> all,int page,int size) { int from=Math.min(page*size,all.size()), to=Math.min(from+size,all.size()); return new PageImpl<>(all.subList(from,to), PageRequest.of(page,size),all.size()); }
|
|
private boolean hardPreferenceMatch(StudentPreference p,Job j) { if(p==null)return true; List<String> cities=list(p.getPreferredCities()); if(!cities.isEmpty()&&!cities.contains(j.getCity()))return false; return p.getExpectedSalaryMin()==null || j.getSalaryMax()==null || j.getSalaryMax()>=p.getExpectedSalaryMin(); }
|
|
private BigDecimal preferenceScore(StudentPreference preference, JobRecommendationProfile profile, Job job) {
|
|
if (preference == null) return null;
|
|
JobPreferenceScoreCalculator.Criteria criteria = new JobPreferenceScoreCalculator.Criteria(
|
|
list(preference.getPreferredCities()),
|
|
list(preference.getEmploymentTypes()),
|
|
preference.getExpectedSalaryMin(),
|
|
list(preference.getPreferredIndustries()),
|
|
list(preference.getPreferredJobTypes()));
|
|
JobPreferenceScoreCalculator.Target target = new JobPreferenceScoreCalculator.Target(
|
|
job.getCity(), job.getEmploymentType(), job.getSalaryMax(),
|
|
profile.getIndustryCode(), profile.getJobFamilyCode());
|
|
return JobPreferenceScoreCalculator.calculate(criteria, target);
|
|
}
|
|
private BigDecimal weighted(Map<String,BigDecimal> scores,Map<String,BigDecimal> expected,Set<String> allowed){if(scores.isEmpty()||expected.isEmpty())return null; BigDecimal n=BigDecimal.ZERO,d=BigDecimal.ZERO;for(var e:expected.entrySet())if(allowed.contains(e.getKey())&&scores.containsKey(e.getKey())&&e.getValue().signum()>0){n=n.add(scores.get(e.getKey()).multiply(e.getValue()));d=d.add(e.getValue());}return d.signum()==0?null:n.divide(d,2,RoundingMode.HALF_UP);}
|
|
private BigDecimal abilityScore(Map<String,BigDecimal> scores,Map<String,BigDecimal> expected){if(scores.isEmpty()||expected.isEmpty())return null;BigDecimal total=BigDecimal.ZERO;int n=0;for(var e:expected.entrySet())if(ABILITY.contains(e.getKey())&&scores.containsKey(e.getKey())&&e.getValue().compareTo(BigDecimal.ZERO)>0){total=total.add(scores.get(e.getKey()).min(e.getValue()).multiply(BigDecimal.valueOf(100)).divide(e.getValue(),2,RoundingMode.HALF_UP));n++;}return n==0?null:total.divide(BigDecimal.valueOf(n),2,RoundingMode.HALF_UP);}
|
|
private Map<String,BigDecimal> latestRiasec(Long student){return latest("SELECT r.dimension_scores_text FROM assessment_results r JOIN assessment_attempts a ON a.id=r.attempt_id JOIN assessment_definitions d ON d.id=a.definition_id WHERE a.student_user_id=? AND a.status='COMPLETED' AND d.type_code='HOLLAND_RIASEC' ORDER BY a.completed_at DESC,a.id DESC LIMIT 1",student,null,RIASEC);}
|
|
private Map<String,BigDecimal> latestAbility(Long student,LocalDateTime now){return latest("SELECT r.dimension_scores_json FROM ability_results r JOIN ability_attempts a ON a.id=r.attempt_id JOIN ability_definitions d ON d.id=a.definition_id WHERE a.student_user_id=? AND a.status='COMPLETED' AND d.type_code='WORKPLACE_ABILITY' AND r.valid_until>? ORDER BY a.completed_at DESC,a.id DESC LIMIT 1",student,now,ABILITY);}
|
|
private Map<String,BigDecimal> latest(String sql,Long student,LocalDateTime now,Set<String> expected){List<String> rows=now==null?jdbcTemplate.query(sql,(rs,n)->rs.getString(1),student):jdbcTemplate.query(sql,(rs,n)->rs.getString(1),student,now);return rows.isEmpty()?Map.of():scoreEntries(rows.get(0),expected);}
|
|
private Map<String,BigDecimal> scoreEntries(String raw,Set<String> expected){try{List<Map<String,Object>> a=JSON.readValue(raw,new TypeReference<>(){});Map<String,BigDecimal>out=new HashMap<>();for(Map<String,Object> e:a){Object key=e.getOrDefault("code",e.get("dimension"));Object score=e.get("score");BigDecimal value=score==null?null:new BigDecimal(String.valueOf(score));if(!(key instanceof String)||value==null||value.compareTo(BigDecimal.ZERO)<0||value.compareTo(BigDecimal.valueOf(100))>0||out.put((String)key,value)!=null)return Map.of();}return out.keySet().equals(expected)?out:Map.of();}catch(Exception e){return Map.of();}}
|
|
private Map<String,BigDecimal> mapDecimal(String raw){try{if(raw==null)return Map.of();Map<String,Object>m=JSON.readValue(raw,new TypeReference<>(){});Map<String,BigDecimal>o=new HashMap<>();for(var e:m.entrySet())o.put(e.getKey(),new BigDecimal(String.valueOf(e.getValue())));return o;}catch(Exception e){return Map.of();}}
|
|
private List<String> list(String raw){try{return raw==null?List.of():JSON.readValue(raw,new TypeReference<List<String>>(){});}catch(Exception e){return List.of();}}
|
|
private String write(Object o){try{return o==null?null:JSON.writeValueAsString(o);}catch(Exception e){throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);}}
|
|
private void validateTaxonomy(String industry,String family){JobRecommendationTaxonomy i=taxonomyRepository.findById(industry).orElseThrow(()->new BusinessException(ErrorCode.RECOMMENDATION_TAXONOMY_INVALID));JobRecommendationTaxonomy f=taxonomyRepository.findById(family).orElseThrow(()->new BusinessException(ErrorCode.RECOMMENDATION_TAXONOMY_INVALID));if(!Boolean.TRUE.equals(i.getEnabled())||!Boolean.TRUE.equals(f.getEnabled())||!"INDUSTRY".equals(i.getTaxonomyType())||!"JOB_FAMILY".equals(f.getTaxonomyType())||!industry.equals(f.getParentCode()))throw new BusinessException(ErrorCode.RECOMMENDATION_TAXONOMY_INVALID);}
|
|
private void validateMaps(Map<String,Integer> r,Map<String,Integer>a){if((r==null||r.isEmpty())&&(a==null||a.isEmpty()))throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);if(r!=null&&(!RIASEC.containsAll(r.keySet())||r.values().stream().anyMatch(v->v==null||v<0||v>100)))throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);if(a!=null&&(!ABILITY.containsAll(a.keySet())||a.values().stream().anyMatch(v->v==null||v<1||v>100)))throw new BusinessException(ErrorCode.RECOMMENDATION_PROFILE_INVALID);}
|
|
private void assertCompanyOwns(Long user,Long job){Company c=companyService.findCompanyByOwnerUserId(user);if(c==null)throw new BusinessException(ErrorCode.COMPANY_NOT_FOUND);jobService.assertOwnerOrThrow(c.getId(),job);}
|
|
private void requireStudent(Long id){User u=userRepository.findById(id).orElseThrow(()->new BusinessException(ErrorCode.USER_NOT_FOUND));if(!u.getRoles().contains(User.Role.STUDENT))throw new BusinessException(ErrorCode.FORBIDDEN);}
|
|
private void requireAdmin(Long id){User u=userRepository.findById(id).orElseThrow(()->new BusinessException(ErrorCode.USER_NOT_FOUND));if(!u.getRoles().contains(User.Role.ADMIN))throw new BusinessException(ErrorCode.FORBIDDEN);}
|
|
private void validatePage(int p,int s){if(p<0||s<1||s>50)throw new BusinessException(ErrorCode.RECOMMENDATION_INVALID_QUERY);}
|
|
private RecommendationProfileResponse profileView(JobRecommendationProfile p){return RecommendationProfileResponse.builder().jobId(p.getJobId()).industryCode(p.getIndustryCode()).jobFamilyCode(p.getJobFamilyCode()).riasecWeights(mapInteger(p.getRiasecWeightsJson())).abilityExpectations(mapInteger(p.getAbilityExpectationsJson())).enabled(p.getEnabled()).reviewStatus(p.getReviewStatus()).reviewReason(p.getReviewReason()).version(p.getVersion()).updatedAt(p.getUpdatedAt()).build();}
|
|
private Map<String,Integer> mapInteger(String raw){try{return raw==null?Map.of():JSON.readValue(raw,new TypeReference<Map<String,Integer>>(){});}catch(Exception e){return Map.of();}}
|
|
private RecommendationFeedbackResponse feedbackView(StudentJobRecommendationFeedback f){return RecommendationFeedbackResponse.builder().jobId(f.getJobId()).feedbackType(f.getFeedbackType()).reasonType(f.getReasonType()).status(f.getStatus()).version(f.getVersion()).build();}
|
|
}
|
|
|