feat(查询): 实现通用过滤条件构建与解析功能

新增参数常量定义和查询条件构建工具,支持复杂条件组合
重构EntityService基类以支持通用过滤条件解析
优化SpecificationUtils工具类,增加搜索文本处理方法
This commit is contained in:
2025-12-13 16:48:54 +08:00
parent 72edb07798
commit 5d6fb961b6
5 changed files with 599 additions and 119 deletions

View File

@@ -10,9 +10,14 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import com.ecep.contract.ds.MyRepository;
import com.ecep.contract.model.IdentityEntity;
import com.ecep.contract.model.Voable;
import com.ecep.contract.util.SpecificationUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.criteria.Path;
import lombok.extern.slf4j.Slf4j;
/**
* 实体服务基类
@@ -22,6 +27,7 @@ import com.fasterxml.jackson.databind.JsonNode;
* @param <VO> VO类型
* @param <ID> 主键类型
*/
@Slf4j
public abstract class EntityService<T extends Voable<VO>, VO, ID> {
/**
* 获取实体数据访问层接口
@@ -52,11 +58,14 @@ public abstract class EntityService<T extends Voable<VO>, VO, ID> {
protected abstract Specification<T> buildParameterSpecification(JsonNode paramsNode);
public Page<VO> findAll(JsonNode paramsNode, Pageable pageable) {
Specification<T> spec = null;
if (paramsNode.has(ParamConstant.KEY_SEARCH_TEXT)) {
spec = getSpecification(paramsNode.get(ParamConstant.KEY_SEARCH_TEXT).asText());
Specification<T> spec = SpecificationUtils.applySearchText(paramsNode, this::getSpecification);
JsonNode filterNode = paramsNode.get(ParamConstant.KEY_FILTER);
if (filterNode != null) {
Specification<T> childSpec = buildFilterCondition(filterNode);
if (childSpec != null) {
spec = SpecificationUtils.and(spec, childSpec);
}
}
spec = SpecificationUtils.and(spec, buildParameterSpecification(paramsNode));
return findAll(spec, pageable).map(T::toVo);
}
@@ -81,4 +90,144 @@ public abstract class EntityService<T extends Voable<VO>, VO, ID> {
Specification<T> spec = getSpecification(searchText);
return getRepository().findAll(spec, Pageable.ofSize(10)).getContent();
}
private Specification<T> buildFilterCondition(JsonNode filterNode) {
String operatorStr = filterNode.get(ParamConstant.KEY_OPERATOR).asText();
if (isAndOrOperator(operatorStr)) {
if (filterNode.has(ParamConstant.KEY_CONDITIONS)) {
JsonNode conditionsNode = filterNode.get(ParamConstant.KEY_CONDITIONS);
if (conditionsNode.isArray()) {
List<Specification<T>> specs = new java.util.ArrayList<>();
for (JsonNode conditionNode : conditionsNode) {
Specification<T> childSpec = buildFilterCondition(conditionNode);
if (childSpec != null) {
specs.add(childSpec);
}
}
if (!specs.isEmpty()) {
ParamConstant.Operator op = ParamConstant.Operator.AND;
if (ParamConstant.Operator.OR.name().equalsIgnoreCase(operatorStr)) {
op = ParamConstant.Operator.OR;
}
return (op == ParamConstant.Operator.AND) ? Specification.allOf(specs)
: Specification.anyOf(specs);
}
} else {
log.debug("filterNode 中没有 conditions 数组");
}
}
return null;
}
String field = filterNode.get(ParamConstant.KEY_FIELD).asText();
if (!StringUtils.hasText(field)) {
log.debug("filterNode 中没有 field 字段");
return null;
}
if (ParamConstant.KEY_equal.equals(operatorStr)) {
return buildEqualSpecification(field, filterNode);
}
if (ParamConstant.KEY_BETWEEN.equals(operatorStr)) {
return buildBetweenSpecification(field, filterNode);
}
if (ParamConstant.KEY_like.equals(operatorStr)) {
// TODO 实现 LIKE 操作符的逻辑
return null;
}
if (ParamConstant.KEY_in.equals(operatorStr)) {
// TODO 实现 IN 操作符的逻辑
return null;
}
if (ParamConstant.KEY_notIn.equals(operatorStr)) {
// TODO 实现 NOT IN 操作符的逻辑
return null;
}
if (ParamConstant.KEY_greaterThan.equals(operatorStr)) {
// TODO 实现大于操作符的逻辑
return null;
}
if (ParamConstant.KEY_lessThan.equals(operatorStr)) {
// TODO 实现小于操作符的逻辑
return null;
}
return null;
}
/**
* 等于操作符的逻辑
*
* @param field
* @param filterNode
* @return
*/
private Specification<T> buildEqualSpecification(String field, JsonNode filterNode) {
// 等于操作符的逻辑
JsonNode valueNode = filterNode.get(ParamConstant.KEY_VALUE);
if (valueNode == null || valueNode.isNull()) {
return (root, query, cb) -> cb.isNull(root.get(field));
}
return (root, query, cb) -> {
// 支持 company.id 这种嵌套属性路径
String[] fieldPath = field.split("\\.");
Path<?> path = root;
for (String segment : fieldPath) {
path = path.get(segment);
}
Class<?> clz = path.getJavaType();
if (IdentityEntity.class.isAssignableFrom(clz) && valueNode.isNumber()) {
return cb.equal(path.get("id"), valueNode.asInt());
}
ObjectMapper objectMapper = SpringApp.getBean(ObjectMapper.class);
Object value = objectMapper.convertValue(valueNode, clz);
// Object value = valueNode.isTextual() ? valueNode.asText()
// : valueNode.isNumber() ? valueNode.numberValue()
// : valueNode.isBoolean() ? valueNode.asBoolean() : valueNode;
return cb.equal(path, value);
};
}
/**
* BETWEEN 操作符的逻辑
*
* @param field
* @param filterNode
* @return
*/
private <X extends Comparable<? super X>> Specification<T> buildBetweenSpecification(String field,
JsonNode filterNode) {
// BETWEEN 操作符:要求 value 为数组,且长度=2
JsonNode valueNode = filterNode.get(ParamConstant.KEY_VALUE);
if (valueNode == null || !valueNode.isArray() || valueNode.size() != 2) {
log.debug("BETWEEN 操作符需要 value 为包含两个元素的数组");
return null;
}
JsonNode lowNode = valueNode.get(0);
JsonNode highNode = valueNode.get(1);
if (lowNode == null || highNode == null || lowNode.isNull() || highNode.isNull()) {
log.debug("BETWEEN 操作符的 value 数组元素不能为空");
return null;
}
return (root, query, cb) -> {
// 支持嵌套属性路径,如 company.id
String[] fieldPath = field.split("\\.");
Path<?> path = root;
for (String segment : fieldPath) {
path = path.get(segment);
}
Class<X> clz = (Class<X>) path.getJavaType();
ObjectMapper objectMapper = SpringApp.getBean(ObjectMapper.class);
X lowVal = objectMapper.convertValue(lowNode, clz);
X highVal = objectMapper.convertValue(highNode, clz);
return cb.between(path.as(clz), lowVal, highVal);
};
}
private boolean isAndOrOperator(String str) {
return ParamConstant.Operator.AND.name().equalsIgnoreCase(str)
|| ParamConstant.Operator.OR.name().equalsIgnoreCase(str);
}
}

View File

@@ -1,17 +1,11 @@
package com.ecep.contract.cloud.rk;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import com.ecep.contract.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,17 +20,20 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ecep.contract.BlackReasonType;
import com.ecep.contract.EntityService;
import com.ecep.contract.IEntityService;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.CloudInfo;
import com.ecep.contract.ds.MyRepository;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.company.model.CompanyBlackReason;
import com.ecep.contract.ds.company.repository.CompanyBlackReasonRepository;
import com.ecep.contract.ds.company.repository.CompanyOldNameRepository;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.other.model.CloudRk;
import com.ecep.contract.ds.other.service.SysConfService;
import com.ecep.contract.service.VoableService;
import com.ecep.contract.util.FileUtils;
import com.ecep.contract.util.HttpJsonUtils;
import com.ecep.contract.util.MyStringUtils;
import com.ecep.contract.util.SpecificationUtils;
import com.ecep.contract.vo.CloudRkVo;
@@ -53,7 +50,8 @@ import lombok.Data;
@Lazy
@Service
@CacheConfig(cacheNames = "cloud-rk")
public class CloudRkService implements IEntityService<CloudRk>, QueryService<CloudRkVo>, VoableService<CloudRk, CloudRkVo> {
public class CloudRkService extends EntityService<CloudRk, CloudRkVo, Integer>
implements IEntityService<CloudRk>, QueryService<CloudRkVo>, VoableService<CloudRk, CloudRkVo> {
private static final Logger logger = LoggerFactory.getLogger(CloudRkService.class);
public static final String KEY_PROXY = "cloud.rk.proxy";
@@ -93,14 +91,12 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
private boolean nowName;
}
@Lazy
@Autowired
private SysConfService confService;
@Lazy
@Autowired
private CloudRkRepository cloudRKRepository;
@Autowired
private CompanyOldNameRepository companyOldNameRepository;
@Autowired
private CompanyBlackReasonRepository companyBlackReasonRepository;
@Override
public CloudRk getById(Integer id) {
@@ -113,26 +109,16 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
}
@Override
public Page<CloudRkVo> findAll(JsonNode paramsNode, Pageable pageable) {
protected Specification<CloudRk> buildParameterSpecification(JsonNode paramsNode) {
Specification<CloudRk> spec = null;
if (paramsNode.has("searchText")) {
spec = getSpecification(paramsNode.get("searchText").asText());
}
spec = SpecificationUtils.andParam(spec, paramsNode, "company");
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "cloudId", "customerGrade", "customerScore", "vendorGrade", "vendorScore", "active", "version", "rank", "description");
// 可以根据需要添加更多参数处理
return findAll(spec, pageable).map(CloudRk::toVo);
}
public Page<CloudRk> findAll(Specification<CloudRk> spec, Pageable pageable) {
return cloudRKRepository.findAll(spec, pageable);
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "cloudId", "customerGrade", "customerScore",
"vendorGrade", "vendorScore", "active", "version", "rank", "description");
return spec;
}
@Override
public Specification<CloudRk> getSpecification(String searchText) {
if (!StringUtils.hasText(searchText)) {
return null;
}
protected Specification<CloudRk> buildSearchSpecification(String searchText) {
return (root, query, builder) -> {
Path<Object> company = root.get("company");
return builder.or(
@@ -143,6 +129,10 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
};
}
public Page<CloudRk> findAll(Specification<CloudRk> spec, Pageable pageable) {
return cloudRKRepository.findAll(spec, pageable);
}
private void toCompanyBlackReasonList(
Company company, BlackReasonType type,
JsonNode reason, List<CompanyBlackReason> dbReasons,
@@ -386,4 +376,16 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
cloudRk.setVersion(vo.getVersion());
}
@Override
protected MyRepository<CloudRk, Integer> getRepository() {
return cloudRKRepository;
}
@Override
public CloudRk createNewEntity() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'createNewEntity'");
}
}

View File

@@ -6,6 +6,7 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.ParamConstant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -42,7 +43,7 @@ public class SpecificationUtils {
* @return 搜索条件为空时返回null否则返回一个Specification对象
*/
public static <T> Specification<T> andWith(String searchText,
Function<String, Specification<T>> buildSearchSpecification) {
Function<String, Specification<T>> buildSearchSpecification) {
Specification<T> spec = null;
String[] split = null;
do {
@@ -85,7 +86,8 @@ public class SpecificationUtils {
return spec;
}
public static <T> Specification<T> andFieldEqualParam(Specification<T> spec, JsonNode paramsNode, String... fields) {
public static <T> Specification<T> andFieldEqualParam(Specification<T> spec, JsonNode paramsNode,
String... fields) {
for (String field : fields) {
if (!paramsNode.has(field)) {
continue;
@@ -112,8 +114,8 @@ public class SpecificationUtils {
return spec;
}
public static <T, Y extends Comparable<? super Y>> Specification<T> andFieldBetweenParam(Specification<T> spec, JsonNode paramsNode, String field, Class<Y> valueType) {
public static <T, Y extends Comparable<? super Y>> Specification<T> andFieldBetweenParam(Specification<T> spec,
JsonNode paramsNode, String field, Class<Y> valueType) {
if (paramsNode.has(field)) {
JsonNode param = paramsNode.get(field);
if (param.isObject()) {
@@ -131,4 +133,12 @@ public class SpecificationUtils {
}
return spec;
}
public static <T> Specification<T> applySearchText(JsonNode paramsNode,
Function<String, Specification<T>> applySearchTextSpecification) {
if (paramsNode.has(ParamConstant.KEY_SEARCH_TEXT)) {
return applySearchTextSpecification.apply(paramsNode.get(ParamConstant.KEY_SEARCH_TEXT).asText());
}
return null;
}
}