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.
 
 
 

93 lines
3.7 KiB

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<Job> searchJobs(
String keyword, String city, String employmentType,
Integer salaryMin, Integer salaryMax, Long companyId) {
return (root, query, cb) -> {
List<Predicate> 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]));
};
}
}