package com.mydreamjob.job; import jakarta.persistence.criteria.Predicate; import org.springframework.data.jpa.domain.Specification; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * 岗位搜索动态查询 Specification。 * 全部使用 CriteriaBuilder 参数化查询,防止 SQL 注入。 */ public class JobSearchSpecification { private static final String STATUS = "status"; private static final String TITLE = "title"; private static final String DESCRIPTION = "description"; private static final String REQUIREMENTS = "requirements"; private static final String CITY = "city"; private static final String EMPLOYMENT_TYPE = "employmentType"; private static final String SALARY_MIN = "salaryMin"; private static final String SALARY_MAX = "salaryMax"; private static final String COMPANY_ID = "companyId"; private JobSearchSpecification() { // utility class } /** * 构建动态查询条件。 * * @param keyword 关键词(搜索标题/描述/要求) * @param city 城市精确匹配 * @param employmentType 用工类型精确匹配 * @param salaryMin 最低薪资筛选 * @param salaryMax 最高薪资筛选 * @param companyId 公司ID筛选 * @return Specification */ public static Specification searchJobs( String keyword, String city, String employmentType, Integer salaryMin, Integer salaryMax, Long companyId) { return (root, query, cb) -> { List predicates = new ArrayList<>(); // 固定:仅 PUBLISHED predicates.add(cb.equal(root.get(STATUS), "PUBLISHED")); // 关键词:搜索 title / description / requirements if (keyword != null && !keyword.isBlank()) { String pattern = "%" + keyword.trim() + "%"; Predicate keywordPred = cb.or( cb.like(cb.lower(root.get(TITLE)), pattern.toLowerCase(Locale.ROOT)), cb.like(cb.lower(root.get(DESCRIPTION)), pattern.toLowerCase(Locale.ROOT)), cb.like(cb.lower(root.get(REQUIREMENTS)), pattern.toLowerCase(Locale.ROOT)) ); predicates.add(keywordPred); } // 城市:trim 后忽略大小写精确匹配。 // 显式 lower 可避免 MySQL 的大小写不敏感排序规则与 H2 默认比较规则产生差异。 if (city != null && !city.isBlank()) { predicates.add(cb.equal(cb.lower(root.get(CITY)), city.trim().toLowerCase(Locale.ROOT))); } // 用工类型:trim 后忽略大小写精确匹配,与线上 MySQL 既有比较结果保持一致。 if (employmentType != null && !employmentType.isBlank()) { predicates.add(cb.equal( cb.lower(root.get(EMPLOYMENT_TYPE)), employmentType.trim().toLowerCase(Locale.ROOT))); } // 薪资范围:岗位 salaryMax >= 用户 salaryMin if (salaryMin != null) { predicates.add(cb.greaterThanOrEqualTo(root.get(SALARY_MAX), salaryMin)); } // 薪资范围:岗位 salaryMin <= 用户 salaryMax if (salaryMax != null) { predicates.add(cb.lessThanOrEqualTo(root.get(SALARY_MIN), salaryMax)); } // 公司筛选 if (companyId != null) { predicates.add(cb.equal(root.get(COMPANY_ID), companyId)); } return cb.and(predicates.toArray(new Predicate[0])); }; } }