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.
525 lines
23 KiB
525 lines
23 KiB
package com.mydreamjob.job;
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.company.VerifiedCompanyGuard;
|
|
import com.mydreamjob.job.dto.JobCreateRequest;
|
|
import com.mydreamjob.job.dto.JobSearchItem;
|
|
import com.mydreamjob.job.dto.JobStatusRequest;
|
|
import com.mydreamjob.job.dto.JobUpdateRequest;
|
|
import com.mydreamjob.job.dto.CompanyManagedJobItem;
|
|
import jakarta.persistence.EntityManager;
|
|
import jakarta.persistence.PersistenceContext;
|
|
import jakarta.persistence.TypedQuery;
|
|
import jakarta.persistence.criteria.*;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.data.domain.Page;
|
|
import org.springframework.data.domain.PageImpl;
|
|
import org.springframework.data.domain.PageRequest;
|
|
import org.springframework.data.domain.Pageable;
|
|
import org.springframework.data.domain.Sort;
|
|
import org.springframework.data.jpa.domain.Specification;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* 岗位管理 Service
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class JobService {
|
|
|
|
private final JobRepository jobRepository;
|
|
private final CompanyRepository companyRepository;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
@PersistenceContext
|
|
private EntityManager entityManager;
|
|
|
|
// ── 企业端接口 ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* 创建岗位(默认 DRAFT)
|
|
*/
|
|
@Transactional
|
|
public Job createJob(Long companyId, JobCreateRequest req) {
|
|
Job job = Job.builder()
|
|
.companyId(companyId)
|
|
.title(req.getTitle())
|
|
.description(req.getDescription())
|
|
.requirements(req.getRequirements())
|
|
.city(req.getCity())
|
|
.employmentType(req.getEmploymentType())
|
|
.salaryMin(req.getSalaryMin())
|
|
.salaryMax(req.getSalaryMax())
|
|
.experience(normalizeText(req.getExperience()))
|
|
.education(normalizeText(req.getEducation()))
|
|
.benefitsJson(benefitsToJson(req.getBenefits()))
|
|
.salaryMonths(validateSalaryMonths(req.getSalaryMonths()))
|
|
.workLocation(normalizeText(req.getWorkLocation()))
|
|
.status(JobStatus.DRAFT.name())
|
|
.build();
|
|
return jobRepository.save(job);
|
|
}
|
|
|
|
/**
|
|
* 查询岗位详情(含非 PUBLISHED 状态)
|
|
*/
|
|
public Job getJob(Long jobId) {
|
|
return jobRepository.findById(jobId)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.JOB_NOT_FOUND));
|
|
}
|
|
|
|
/**
|
|
* 更新岗位(仅岗位所属企业可操作,部分字段更新)
|
|
*/
|
|
@Transactional
|
|
public Job updateJob(Long companyId, Long jobId, JobUpdateRequest req) {
|
|
Job job = getCompanyJob(companyId, jobId);
|
|
if (!JobStatus.DRAFT.name().equals(job.getStatus())) {
|
|
requireVerifiedCompany(companyId);
|
|
}
|
|
|
|
if (req.getTitle() != null) job.setTitle(req.getTitle());
|
|
if (req.getDescription() != null) job.setDescription(req.getDescription());
|
|
if (req.getRequirements() != null) job.setRequirements(req.getRequirements());
|
|
if (req.getCity() != null) job.setCity(req.getCity());
|
|
if (req.getEmploymentType() != null) job.setEmploymentType(req.getEmploymentType());
|
|
if (req.getSalaryMin() != null) job.setSalaryMin(req.getSalaryMin());
|
|
if (req.getSalaryMax() != null) job.setSalaryMax(req.getSalaryMax());
|
|
// 新增字段:null 保持原值;空串/纯空白清空为 NULL;非空去两端空白后保存。
|
|
if (req.getExperience() != null) job.setExperience(normalizeText(req.getExperience()));
|
|
if (req.getEducation() != null) job.setEducation(normalizeText(req.getEducation()));
|
|
if (req.getWorkLocation() != null) job.setWorkLocation(normalizeText(req.getWorkLocation()));
|
|
// benefits:null 保持原值;[] 清空;非空校验后序列化保存。
|
|
if (req.getBenefits() != null) job.setBenefitsJson(benefitsToJson(req.getBenefits()));
|
|
// salaryMonths:null 保持原值(本批不提供显式清空);非 null 必须为 1..24。
|
|
if (req.getSalaryMonths() != null) job.setSalaryMonths(validateSalaryMonths(req.getSalaryMonths()));
|
|
|
|
return jobRepository.save(job);
|
|
}
|
|
|
|
/**
|
|
* 状态流转:DRAFT→PUBLISHED,PUBLISHED→CLOSED;禁止非法回退
|
|
*/
|
|
@Transactional
|
|
public Job transitionStatus(Long companyId, Long jobId, JobStatusRequest req) {
|
|
String target = req.getStatus();
|
|
|
|
// 未认证企业的发布请求必须在按 jobId 查询前拒绝,避免通过 400/404 差异探测岗位。
|
|
if (JobStatus.PUBLISHED.name().equals(target)) {
|
|
requireVerifiedCompany(companyId);
|
|
}
|
|
|
|
Job job = getCompanyJob(companyId, jobId);
|
|
String current = job.getStatus();
|
|
|
|
// 校验状态流转合法性
|
|
boolean valid = switch (current) {
|
|
case "DRAFT" -> "PUBLISHED".equals(target);
|
|
case "PUBLISHED" -> "CLOSED".equals(target);
|
|
case "CLOSED" -> false; // CLOSED 不可再流转
|
|
default -> false;
|
|
};
|
|
if (!valid) {
|
|
throw new BusinessException(ErrorCode.INVALID_JOB_STATUS_TRANSITION);
|
|
}
|
|
|
|
job.setStatus(target);
|
|
if ("PUBLISHED".equals(target)) {
|
|
job.setPublishedAt(LocalDateTime.now());
|
|
}
|
|
return jobRepository.save(job);
|
|
}
|
|
|
|
/** 用企业主键解析权威认证状态,供岗位与投递服务在已确认归属后执行统一门禁。 */
|
|
public Company requireVerifiedCompany(Long companyId) {
|
|
return VerifiedCompanyGuard.requireVerified(
|
|
companyRepository.findById(companyId).orElse(null));
|
|
}
|
|
|
|
/**
|
|
* 读取当前企业自己的岗位;对外统一将其他企业资源与不存在资源表示为 JOB_NOT_FOUND,
|
|
* 不向调用方泄露跨企业资源是否存在。
|
|
*/
|
|
public Job getCompanyJob(Long companyId, Long jobId) {
|
|
return jobRepository.findByIdAndCompanyId(jobId, companyId)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.JOB_NOT_FOUND));
|
|
}
|
|
|
|
/** 查询当前企业自己的全部岗位状态;不调用发布门禁,也不产生写入副作用。 */
|
|
@Transactional(readOnly = true)
|
|
public Page<CompanyManagedJobItem> listCompanyJobs(Long companyId, String status,
|
|
String keyword, String city,
|
|
int page, int size) {
|
|
if (companyId == null || companyId <= 0 || page < 0 || size < 1 || size > 100) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
if (status != null) {
|
|
try {
|
|
JobStatus.valueOf(status);
|
|
} catch (IllegalArgumentException exception) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
}
|
|
String normalizedKeyword = normalizeFilter(keyword, 120);
|
|
String normalizedCity = normalizeFilter(city, 80);
|
|
Pageable pageable = PageRequest.of(page, size, Sort.by(
|
|
Sort.Order.desc("createdAt"), Sort.Order.desc("id")));
|
|
return jobRepository.findAll(CompanyJobManagementSpecification.forCompany(
|
|
companyId, status, normalizedKeyword, normalizedCity), pageable)
|
|
.map(CompanyManagedJobItem::from);
|
|
}
|
|
|
|
private String normalizeFilter(String value, int maxLength) {
|
|
if (value == null || value.isBlank()) return null;
|
|
String normalized = value.trim();
|
|
if (normalized.length() > maxLength) throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
return normalized;
|
|
}
|
|
|
|
// ── 公开接口 ────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 公开岗位列表:仅返回 PUBLISHED,支持筛选和分页
|
|
*
|
|
* <p>DEF-01:关键词在此处规范化,使其与 {@code GET /jobs/search}
|
|
* ({@code JobSearchSpecification} 的 {@code keyword.trim()} + {@code isBlank()} 判空)
|
|
* 的输入语义一致 —— 只去两端空白,空串与纯空白等同「未传关键词」,
|
|
* 不折叠中间空格、不改大小写,也不改变任何过滤/排序/分页语义。</p>
|
|
*/
|
|
public Page<Map<String, Object>> listPublishedJobs(String keyword, String city,
|
|
String employmentType,
|
|
Integer salaryMin, Integer salaryMax,
|
|
int page, int size) {
|
|
if (salaryMin != null && salaryMax != null && salaryMin > salaryMax) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
String normalizedKeyword = keyword == null || keyword.isBlank() ? null : keyword.trim();
|
|
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
|
Page<Job> result = jobRepository.findPublishedJobs(normalizedKeyword, city, employmentType,
|
|
salaryMin, salaryMax, pageable);
|
|
|
|
// 批量查询企业信息后组装,避免逐条查询造成 N+1;空页时不发起企业查询。
|
|
List<Job> jobs = result.getContent();
|
|
Set<Long> companyIds = jobs.stream()
|
|
.map(Job::getCompanyId)
|
|
.collect(Collectors.toSet());
|
|
Map<Long, Company> companyMap = companyIds.isEmpty()
|
|
? Map.of()
|
|
: companyRepository.findAllById(companyIds).stream()
|
|
.collect(Collectors.toMap(Company::getId, c -> c));
|
|
List<Map<String, Object>> items = jobs.stream()
|
|
.map(job -> toPublicView(job, companyMap.get(job.getCompanyId())))
|
|
.toList();
|
|
return new PageImpl<>(items, result.getPageable(), result.getTotalElements());
|
|
}
|
|
|
|
/**
|
|
* 公开岗位详情:返回 PUBLISHED 岗位;CLOSED 岗位也允许查看但不可投递(本阶段不处理投递)
|
|
*/
|
|
public Map<String, Object> getPublicJob(Long jobId) {
|
|
Job job = getJob(jobId);
|
|
if (!"PUBLISHED".equals(job.getStatus()) && !"CLOSED".equals(job.getStatus())) {
|
|
// DRAFT 岗位对公众不可见
|
|
throw new BusinessException(ErrorCode.JOB_NOT_FOUND);
|
|
}
|
|
Company company = companyRepository.findById(job.getCompanyId()).orElse(null);
|
|
return toPublicView(job, company);
|
|
}
|
|
|
|
// ── 辅助方法 ────────────────────────────────────────────────
|
|
|
|
public Job assertOwnerOrThrow(Long companyId, Long jobId) {
|
|
Job job = getJob(jobId);
|
|
if (!companyId.equals(job.getCompanyId())) {
|
|
throw new BusinessException(ErrorCode.JOB_OWNER_MISMATCH);
|
|
}
|
|
return job;
|
|
}
|
|
|
|
/**
|
|
* 企业端视图(含完整字段)
|
|
*/
|
|
public Map<String, Object> toView(Job job) {
|
|
Map<String, Object> view = new HashMap<>();
|
|
view.put("id", job.getId());
|
|
view.put("companyId", job.getCompanyId());
|
|
view.put("title", job.getTitle());
|
|
view.put("description", job.getDescription());
|
|
view.put("requirements", job.getRequirements());
|
|
view.put("city", job.getCity());
|
|
view.put("employmentType", job.getEmploymentType());
|
|
view.put("salaryMin", job.getSalaryMin());
|
|
view.put("salaryMax", job.getSalaryMax());
|
|
view.put("experience", job.getExperience());
|
|
view.put("education", job.getEducation());
|
|
view.put("benefits", benefitsFromJson(job.getBenefitsJson()));
|
|
view.put("salaryMonths", job.getSalaryMonths());
|
|
view.put("workLocation", job.getWorkLocation());
|
|
view.put("status", job.getStatus());
|
|
view.put("publishedAt", job.getPublishedAt());
|
|
view.put("createdAt", job.getCreatedAt());
|
|
view.put("updatedAt", job.getUpdatedAt());
|
|
return view;
|
|
}
|
|
|
|
/**
|
|
* 公开视图(精简字段)
|
|
*/
|
|
public Map<String, Object> toPublicView(Job job) {
|
|
Map<String, Object> view = new HashMap<>();
|
|
view.put("id", job.getId());
|
|
view.put("companyId", job.getCompanyId());
|
|
view.put("title", job.getTitle());
|
|
view.put("description", job.getDescription());
|
|
view.put("requirements", job.getRequirements());
|
|
view.put("city", job.getCity());
|
|
view.put("employmentType", job.getEmploymentType());
|
|
view.put("salaryMin", job.getSalaryMin());
|
|
view.put("salaryMax", job.getSalaryMax());
|
|
view.put("experience", job.getExperience());
|
|
view.put("education", job.getEducation());
|
|
view.put("benefits", benefitsFromJson(job.getBenefitsJson()));
|
|
view.put("salaryMonths", job.getSalaryMonths());
|
|
view.put("workLocation", job.getWorkLocation());
|
|
view.put("status", job.getStatus());
|
|
view.put("publishedAt", job.getPublishedAt());
|
|
view.put("createdAt", job.getCreatedAt());
|
|
return view;
|
|
}
|
|
|
|
/**
|
|
* 公开视图(含企业摘要)。企业不存在时 company 为 null,不编造数据也不抛异常。
|
|
*/
|
|
public Map<String, Object> toPublicView(Job job, Company company) {
|
|
Map<String, Object> view = toPublicView(job);
|
|
view.put("company", toCompanyInfo(company));
|
|
return view;
|
|
}
|
|
|
|
// ── 字段归一化与校验 ────────────────────────────────────────
|
|
|
|
/** 空串/纯空白清空为 null;非空去两端空白后返回;null 原样返回。 */
|
|
private static String normalizeText(String value) {
|
|
if (value == null) return null;
|
|
String trimmed = value.trim();
|
|
return trimmed.isEmpty() ? null : trimmed;
|
|
}
|
|
|
|
private static final int MAX_BENEFITS = 20;
|
|
private static final int MAX_BENEFIT_LENGTH = 40;
|
|
|
|
/**
|
|
* benefits → benefits_json。null/空数组返回 null(清空);非空逐项 trim、拒绝空白项、
|
|
* 长度 ≤40、去重保序、最多 20 项,违者 BAD_REQUEST;序列化为 JSON 数组字符串。
|
|
*/
|
|
private String benefitsToJson(List<String> benefits) {
|
|
if (benefits == null || benefits.isEmpty()) return null;
|
|
Set<String> unique = new LinkedHashSet<>();
|
|
for (String raw : benefits) {
|
|
if (raw == null || raw.trim().isEmpty()) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
String value = raw.trim();
|
|
if (value.length() > MAX_BENEFIT_LENGTH) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
unique.add(value);
|
|
}
|
|
List<String> normalized = new ArrayList<>(unique);
|
|
if (normalized.size() > MAX_BENEFITS) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
try {
|
|
return objectMapper.writeValueAsString(normalized);
|
|
} catch (JsonProcessingException exception) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
}
|
|
|
|
/** benefits_json → 列表。null 返回空数组;反序列化失败抛 BAD_REQUEST 而非 500。 */
|
|
private List<String> benefitsFromJson(String benefitsJson) {
|
|
if (benefitsJson == null || benefitsJson.isBlank()) return List.of();
|
|
try {
|
|
List<String> parsed = objectMapper.readValue(benefitsJson,
|
|
new TypeReference<List<String>>() {});
|
|
return parsed == null ? List.of() : parsed;
|
|
} catch (JsonProcessingException exception) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
}
|
|
|
|
/** salaryMonths 非 null 时必须为 1..24 的整数,0 视为非法。 */
|
|
private static Integer validateSalaryMonths(Integer salaryMonths) {
|
|
if (salaryMonths == null) return null;
|
|
if (salaryMonths < 1 || salaryMonths > 24) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
return salaryMonths;
|
|
}
|
|
|
|
// ── 高级搜索 ────────────────────────────────────────────────
|
|
|
|
private static final Set<String> ALLOWED_SORTS = Set.of("latest", "salary", "relevance");
|
|
private static final int MAX_PAGE_SIZE = 50;
|
|
|
|
/**
|
|
* 高级搜索:仅返回 PUBLISHED 岗位,支持多条件筛选、排序、分页。
|
|
* 企业信息通过批量查询组装,避免 N+1。
|
|
* sort=salary 时通过 CriteriaQuery 显式构建 CASE 排序,确保 salaryMax=NULL 排在最后。
|
|
*/
|
|
public Page<JobSearchItem> searchJobs(String keyword, String city, String employmentType,
|
|
Integer salaryMin, Integer salaryMax, Long companyId,
|
|
String sort, int page, int size) {
|
|
// 参数校验
|
|
if (salaryMin != null && salaryMax != null && salaryMin > salaryMax) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
if (sort != null && !ALLOWED_SORTS.contains(sort)) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
if (page < 0) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
if (size < 1 || size > MAX_PAGE_SIZE) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
|
|
var spec = JobSearchSpecification.searchJobs(keyword, city, employmentType,
|
|
salaryMin, salaryMax, companyId);
|
|
|
|
Page<Job> jobPage;
|
|
if ("salary".equals(sort)) {
|
|
// salary 排序:显式 CASE WHEN 确保 NULL 排在最后
|
|
jobPage = findWithSalarySort(spec, page, size);
|
|
} else {
|
|
// latest 和 relevance 都按 latest 排序
|
|
Sort sortObj = Sort.by(
|
|
new Sort.Order(Sort.Direction.DESC, "createdAt"),
|
|
new Sort.Order(Sort.Direction.DESC, "id"));
|
|
Pageable pageable = PageRequest.of(page, size, sortObj);
|
|
jobPage = jobRepository.findAll(spec, pageable);
|
|
}
|
|
|
|
// 批量查询企业信息
|
|
Set<Long> companyIds = jobPage.getContent().stream()
|
|
.map(Job::getCompanyId)
|
|
.collect(Collectors.toSet());
|
|
Map<Long, Company> companyMap = companyIds.isEmpty()
|
|
? Map.of()
|
|
: companyRepository.findAllById(companyIds).stream()
|
|
.collect(Collectors.toMap(Company::getId, c -> c));
|
|
|
|
return jobPage.map(job -> toSearchItem(job, companyMap.get(job.getCompanyId())));
|
|
}
|
|
|
|
/**
|
|
* 使用 CriteriaQuery + CASE WHEN 实现 salaryMax DESC NULLS LAST。
|
|
*/
|
|
Page<Job> findWithSalarySort(Specification<Job> spec, int page, int size) {
|
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
|
|
|
// 查询
|
|
CriteriaQuery<Job> cq = cb.createQuery(Job.class);
|
|
Root<Job> root = cq.from(Job.class);
|
|
Predicate predicate = spec.toPredicate(root, cq, cb);
|
|
if (predicate != null) {
|
|
cq.where(predicate);
|
|
}
|
|
// CASE WHEN salaryMax IS NULL THEN 1 ELSE 0 END → NULL 排最后
|
|
cq.orderBy(
|
|
cb.asc(cb.selectCase()
|
|
.when(cb.isNull(root.get("salaryMax")), 1)
|
|
.otherwise(0)),
|
|
cb.desc(root.get("salaryMax")),
|
|
cb.desc(root.get("id"))
|
|
);
|
|
|
|
TypedQuery<Job> query = entityManager.createQuery(cq);
|
|
query.setFirstResult(page * size);
|
|
query.setMaxResults(size);
|
|
List<Job> jobs = query.getResultList();
|
|
|
|
// 计数
|
|
CriteriaQuery<Long> countCq = cb.createQuery(Long.class);
|
|
Root<Job> countRoot = countCq.from(Job.class);
|
|
Predicate countPredicate = spec.toPredicate(countRoot, countCq, cb);
|
|
if (countPredicate != null) {
|
|
countCq.where(countPredicate);
|
|
}
|
|
countCq.select(cb.count(countRoot));
|
|
Long total = entityManager.createQuery(countCq).getSingleResult();
|
|
|
|
return new PageImpl<>(jobs, PageRequest.of(page, size), total);
|
|
}
|
|
|
|
private JobSearchItem toSearchItem(Job job, Company company) {
|
|
return JobSearchItem.builder()
|
|
.id(job.getId())
|
|
.title(job.getTitle())
|
|
.description(job.getDescription())
|
|
.requirements(job.getRequirements())
|
|
.city(job.getCity())
|
|
.employmentType(job.getEmploymentType())
|
|
.salaryMin(job.getSalaryMin())
|
|
.salaryMax(job.getSalaryMax())
|
|
.experience(job.getExperience())
|
|
.education(job.getEducation())
|
|
.benefits(benefitsFromJson(job.getBenefitsJson()))
|
|
.salaryMonths(job.getSalaryMonths())
|
|
.workLocation(job.getWorkLocation())
|
|
.status(job.getStatus())
|
|
.publishedAt(job.getPublishedAt())
|
|
.createdAt(job.getCreatedAt())
|
|
.company(toCompanyInfo(company))
|
|
.build();
|
|
}
|
|
|
|
/** 公开企业摘要(含 id/name/logoUrl/description/industry/verified/companySize);企业不存在返回 null。 */
|
|
private JobSearchItem.CompanyInfo toCompanyInfo(Company company) {
|
|
if (company == null) return null;
|
|
return JobSearchItem.CompanyInfo.builder()
|
|
.id(company.getId())
|
|
.name(company.getName())
|
|
.logoUrl(company.getLogoUrl())
|
|
.description(company.getDescription())
|
|
.industry(company.getIndustry())
|
|
.verified(company.getVerified())
|
|
.companySize(company.getCompanySize())
|
|
.build();
|
|
}
|
|
|
|
// ── 筛选选项 ────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 获取筛选选项:仅基于 PUBLISHED 岗位,使用数据库聚合查询,不加载全部岗位到内存。
|
|
*/
|
|
public Map<String, Object> getFilterOptions() {
|
|
List<String> cities = jobRepository.findDistinctCitiesPublished();
|
|
List<String> employmentTypes = jobRepository.findDistinctEmploymentTypesPublished();
|
|
Integer minSalary = jobRepository.findMinSalaryPublished();
|
|
Integer maxSalary = jobRepository.findMaxSalaryPublished();
|
|
|
|
Map<String, Object> salaryRange = new HashMap<>();
|
|
salaryRange.put("min", minSalary);
|
|
salaryRange.put("max", maxSalary);
|
|
|
|
Map<String, Object> result = new HashMap<>();
|
|
result.put("cities", cities != null ? cities : Collections.emptyList());
|
|
result.put("employmentTypes", employmentTypes != null ? employmentTypes : Collections.emptyList());
|
|
result.put("salaryRange", salaryRange);
|
|
return result;
|
|
}
|
|
}
|
|
|