package com.mydreamjob.skill; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.skill.domain.SkillCategory; import com.mydreamjob.skill.domain.SkillIndustry; import com.mydreamjob.skill.domain.SkillOccupation; import com.mydreamjob.skill.dto.SkillCloudItem; import com.mydreamjob.skill.dto.SkillCloudNode; import com.mydreamjob.skill.dto.SkillCloudResponse; import com.mydreamjob.skill.dto.SkillCloudSkillProjection; import com.mydreamjob.skill.repository.OccupationSkillRepository; import com.mydreamjob.skill.repository.SkillCategoryRepository; import com.mydreamjob.skill.repository.SkillIndustryRepository; import com.mydreamjob.skill.repository.SkillOccupationRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.Comparator; import java.util.List; import java.util.stream.IntStream; @Service @Transactional(readOnly = true) public class SkillCloudQueryService { private static final Comparator SKILL_ORDER = (left, right) -> { int heatComparison = compareHeat(left.importance(), right.importance()); if (heatComparison != 0) { return heatComparison; } int nameComparison = compareNullable(left.nameZh(), right.nameZh()); if (nameComparison != 0) { return nameComparison; } return compareNullable(left.skillId(), right.skillId()); }; private final SkillIndustryRepository industryRepository; private final SkillOccupationRepository occupationRepository; private final SkillCategoryRepository categoryRepository; private final OccupationSkillRepository occupationSkillRepository; public SkillCloudQueryService(SkillIndustryRepository industryRepository, SkillOccupationRepository occupationRepository, SkillCategoryRepository categoryRepository, OccupationSkillRepository occupationSkillRepository) { this.industryRepository = industryRepository; this.occupationRepository = occupationRepository; this.categoryRepository = categoryRepository; this.occupationSkillRepository = occupationSkillRepository; } public SkillCloudResponse query(Long industryId, Long occupationId, Long categoryId) { long validatedIndustryId = requirePositive(industryId, "industryId"); long validatedOccupationId = requirePositive(occupationId, "occupationId"); Long validatedCategoryId = categoryId == null ? null : requirePositive(categoryId, "categoryId"); SkillIndustry industry = industryRepository.findById(validatedIndustryId) .orElseThrow(() -> badRequest("industry not found: industryId=" + validatedIndustryId)); SkillOccupation occupation = occupationRepository.findById(validatedOccupationId) .orElseThrow(() -> badRequest("occupation not found: occupationId=" + validatedOccupationId)); if (!occupation.getIndustryId().equals(validatedIndustryId)) { throw badRequest("occupation does not belong to industry: occupationId=" + validatedOccupationId + ", industryId=" + validatedIndustryId); } SkillCategory category = null; if (validatedCategoryId != null) { category = categoryRepository.findById(validatedCategoryId) .orElseThrow(() -> badRequest("category not found: categoryId=" + validatedCategoryId)); if (!category.getOccupationId().equals(validatedOccupationId)) { throw badRequest("category does not belong to occupation: categoryId=" + validatedCategoryId + ", occupationId=" + validatedOccupationId); } } List projections = validatedCategoryId == null ? occupationSkillRepository.findSkillProjectionsByOccupationId(validatedOccupationId) : occupationSkillRepository.findSkillProjectionsByOccupationIdAndCategoryId( validatedOccupationId, validatedCategoryId); List sortedProjections = projections.stream() .sorted(SKILL_ORDER) .toList(); List items = IntStream.range(0, sortedProjections.size()) .mapToObj(index -> toItem(sortedProjections.get(index), index + 1)) .toList(); return new SkillCloudResponse( toNode(industry.getId(), industry.getCode(), industry.getNameZh()), toNode(occupation.getId(), occupation.getCode(), occupation.getNameZh()), category == null ? null : toNode(category.getId(), category.getCode(), category.getNameZh()), items.size(), items ); } private SkillCloudItem toItem(SkillCloudSkillProjection projection, int rank) { return new SkillCloudItem( rank, projection.skillId(), projection.skillCode(), projection.nameZh(), projection.nameEn(), projection.sourceType(), projection.sourceCode(), projection.categoryId(), projection.categoryName(), projection.importance(), projection.importanceSourceCode(), projection.importanceElementId(), projection.importanceIsProxy() ); } private static int compareHeat(BigDecimal left, BigDecimal right) { if (left == null && right == null) { return 0; } if (left == null) { return 1; } if (right == null) { return -1; } return right.compareTo(left); } private static > int compareNullable(T left, T right) { if (left == null && right == null) { return 0; } if (left == null) { return 1; } if (right == null) { return -1; } return left.compareTo(right); } private static SkillCloudNode toNode(Long id, String code, String name) { return new SkillCloudNode(id, code, name); } private static long requirePositive(Long value, String parameterName) { if (value == null || value <= 0) { throw badRequest(parameterName + " must be a positive integer"); } return value; } private static BusinessException badRequest(String detail) { return new BusinessException(ErrorCode.BAD_REQUEST, detail); } }