前端传来的Json数据:
{capitalNo: "", capitalName: "AAA", capitalType: "", capitalIndexType: "", capitalAccount: "",…}
capitalAccount
: 
""
capitalIndexSource
: 
""
capitalIndexType
: 
""
capitalName
: 
"AAA"
capitalNo
: 
""
capitalSource
: 
""
capitalStates
: 
[]
capitalType
: 
""
createDateRange
: 
{beginDate: "", endDate: ""}
current
: 
1
remark
: 
""
size
: 
20

后端spring boot 处理:
    /**
     * 获取资金信息分页
     *
     * @param queryDTO 查询对象
     * @return {@link Result}<{@link PageResult}<{@link CapitalInfoVO}>>
     */
    @PostMapping("/page")
    public Result<PageResult<CapitalInfoVO>> queryPage(@RequestBody @Valid CapitalInfoQueryDTO queryDTO) {

        String endpoint = "/capital/info/page";
        String method = "queryPage";

        log.info("【资金信息】查询资金信息分页,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO));

        // 1、基本校验已由 @Valid 完成,Spring会自动执行以下验证:
        //    a. 检查@Pattern注解(格式验证)
        //    b. 调用@AssertTrue注解的方法(业务逻辑验证)

        // 处理校验结果,由全局异常 【处理方法参数验证异常(MethodArgumentNotValidException)】 处理校验结果

        // 2、处理业务逻辑校验,检查空条件
        if (isAllQueryParamsEmpty(queryDTO)) {
            log.warn("【资金信息】 至少需要一个查询条件,{},{}", endpoint, method);
            return Result.error("请至少输入一个查询条件");
        }

        // 3、处理业务逻辑校验 - 日期范围
        String dateRangeError = validateDateRange(queryDTO.getCreateDateRange());
        if (dateRangeError != null) {
            return Result.error(dateRangeError);
        }

        try {
            PageResult<CapitalInfoVO> capitalInfoPage = capitalInfoService.queryPage(queryDTO);

            return Result.success(capitalInfoPage);

        } catch (Exception e) {
            log.error("【资金信息】查询资金信息分页失败,{},{}", endpoint, method, e);
            return Result.error("查询资金信息分页失败");
        }
    }

    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
               && queryDTO.getCapitalYear() == null
               && queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty()
               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
               && queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()
               && queryDTO.getCreateDateRange().getEndDateTime().isEmpty();
    }
为什么前端接收到的响应结果为:{"code":1,"message":"请至少输入一个查询条件","data":null}

前端接收到的响应 {"code":1,"message":"请至少输入一个查询条件","data":null} 是因为后端在 isAllQueryParamsEmpty 方法中错误地判断所有查询条件都为空,尽管前端传入了 capitalName 为 "AAA"。问题出在该方法的逻辑中,由于运算符优先级处理不当,导致实际判断结果与预期不符。

具体原因如下:

  • 方法中使用了一系列 && 和 || 组合条件,但 Java 中 && 优先级高于 ||,因此表达式被解析为:

    text

    (前面所有 && 条件) || (capitalStates.isEmpty() && 剩余条件)
  • 前面的 && 条件中包含了 capitalName 的非空判断((capitalName == null || capitalName.trim().isEmpty())),由于 capitalName 为 "AAA",该条件为 false,导致前半部分整体为 false

  • 后半部分中,capitalStates 为空数组,capitalStates.isEmpty() 为 trueremark 为空字符串,日期范围也为空,因此后半部分整体为 true

  • 最终整个表达式 false || true 结果为 true,导致方法返回 true,触发“请至少输入一个查询条件”的错误。

解决方案:调整 isAllQueryParamsEmpty 方法中的逻辑,为 capitalStates 的判断添加明确的括号,确保所有条件正确组合。例如:

java

&& (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())

这段代码还需优化吗?
    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
               && queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()
               && queryDTO.getCreateDateRange().getEndDateTime().isEmpty();
    }

你的代码已经解决了之前运算符优先级的问题,整体逻辑清晰。不过,从健壮性、可读性和维护性角度,仍有几个可以优化的地方:

1. 潜在的空指针风险

  • 问题queryDTO.getCreateDateRange() 可能返回 null,直接调用 getBeginDateTime().isEmpty() 会抛出 NullPointerException。同样,beginDateTime 和 endDateTime 也可能为 null

  • 改进:先判空,再调用方法。

2. 代码冗长,可读性一般

  • 大量重复的 (field == null || field.trim().isEmpty()) 模式,使方法难以阅读和维护。

  • 改进:提取工具方法,如 isBlank(String str),或直接使用 Apache Commons Lang 的 StringUtils.isBlank()

3. 字段变更时的维护成本

  • 如果以后增加或删除查询字段,需要手动修改此方法,容易遗漏。

  • 改进:考虑使用反射或注解标记查询字段,但需权衡复杂度,通常简单项目直接维护即可。

4. 日期范围的空判断可能不够准确

  • 如果 createDateRange 对象不为空,但两个日期字符串都是空,业务上视为无日期条件,当前逻辑正确。

  • 但需确保 beginDateTime 和 endDateTime 的 isEmpty() 是安全的(已处理 null)。


优化建议示例

java

import org.apache.commons.lang3.StringUtils; // 或自己写工具类

private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
    if (queryDTO == null) {
        return true;
    }

    return isBlank(queryDTO.getCapitalNo())
            && isBlank(queryDTO.getCapitalName())
            && isBlank(queryDTO.getCapitalType())
            && isBlank(queryDTO.getCapitalIndexType())
            && isBlank(queryDTO.getCapitalAccount())
            && isBlank(queryDTO.getCapitalSource())
            && isBlank(queryDTO.getCapitalIndexSource())
            && queryDTO.getCapitalYear() == null
            && isCollectionEmpty(queryDTO.getCapitalStates())
            && isBlank(queryDTO.getRemark())
            && isDateRangeEmpty(queryDTO.getCreateDateRange());
}

private boolean isBlank(String str) {
    return str == null || str.trim().isEmpty();
}

private boolean isCollectionEmpty(Collection<?> collection) {
    return collection == null || collection.isEmpty();
}

private boolean isDateRangeEmpty(DateRange range) {
    return range == null 
        || (isBlank(range.getBeginDateTime()) && isBlank(range.getEndDateTime()));
}

如果不想引入外部库,自己写静态工具类:

java

public final class QueryUtils {
    public static boolean isBlank(String str) { ... }
    public static boolean isCollectionEmpty(Collection<?> c) { ... }
}

这样改造后:

  • 健壮性提升:避免 NPE

  • 可读性增强:条件语句一目了然

  • 复用性高:工具方法可用于其他校验


是否需要更复杂的优化?

如果你的项目中有大量类似的 DTO 和全空判断,可以考虑基于注解(如 @QueryCondition)配合反射自动检查,但会增加复杂度。对于当前场景,上述小优化已足够。


这样改进后,是否可以了?
    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());
    }

你的改进已经很好地解决了空指针问题,逻辑也是正确的。现在当 createDateRange 为 null 时,两个日期条件会自动视为空,符合预期;当 createDateRange 不为 null 时,会检查两个日期字符串是否为空,也与原意图一致。

不过,如果 getBeginDateTime() 或 getEndDateTime() 可能返回 null(而不是空字符串),那么调用 isEmpty() 仍然会触发空指针异常。虽然大多数场景下日期字段会初始化为空字符串,但为了绝对健壮,可以进一步补充判空:

java

&& (queryDTO.getCreateDateRange() == null 
    || (queryDTO.getCreateDateRange().getBeginDateTime() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()))
&& (queryDTO.getCreateDateRange() == null 
    || (queryDTO.getCreateDateRange().getEndDateTime() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty()))

这样就能全面防护 null 值。当然,如果确定日期字段永远不会返回 null,当前的代码已经足够。

另外,如果你希望代码更简洁、可维护,可以考虑抽取工具方法(如 isBlank)来避免重复的判空逻辑,但这属于锦上添花的优化,不影响正确性。

总的来说,改进后的代码是可以的,已经满足当前需求。如果你对空指针有更高要求,再补充一层判空即可。


这样改后:
    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
               && (queryDTO.getCreateDateRange() == null
                   || (queryDTO.getCreateDateRange().getBeginDateTime() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty()))
               && (queryDTO.getCreateDateRange() == null
                   || (queryDTO.getCreateDateRange().getEndDateTime() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty()));
    }
为什么idea会提示queryDTO.getCreateDateRange().getBeginDateTime() == null和queryDTO.getCreateDateRange().getEndDateTime() == null替换为isEmpty

IDEA 提示 queryDTO.getCreateDateRange().getBeginDateTime() == null 可替换为 isEmpty(),通常是因为 IDEA 认为 getBeginDateTime() 返回的对象(假设是 String)不可能为 null,或者它检测到你在同一个表达式中已经确保了调用对象非空,但字段本身仍可能为 null。实际上,这种提示可能来自不同的代码检查规则:

  1. “可以替换为 isEmpty()

    • 如果 getBeginDateTime() 返回的是 String,且 IDEA 通过某些分析(例如方法注解 @NotNull 或上下文)认为该方法不会返回 null,那么 == null 的判断就是多余的,可以简化为 isEmpty()

    • 但在你的代码中,你同时判断了 == null 和 isEmpty(),这是为了应对字段可能为 null 的情况。如果字段确实可能为 null,那么 == null 的判断是必要的,不能直接替换为 isEmpty()(因为 null.isEmpty() 会抛异常)。

  2. 建议使用 StringUtils.isEmpty()

    • IDEA 可能检测到你在手动实现判空+判空字符串的组合,并建议使用 Apache Commons Lang 的 StringUtils.isEmpty() 或 StringUtils.isBlank(),这些工具方法内部已经处理了 null 和空字符串。

    • 如果你已经引入了相关工具类,采纳提示可以让代码更简洁。

  3. 检查配置过于激进

    • 某些 IDEA 的检查规则(如“Constant conditions & exceptions”)可能误判,认为 getBeginDateTime() 不可能为 null(比如从上下文推断它总是由框架注入,不会为 null),从而建议简化。

建议处理方式

  • 如果确定 getBeginDateTime() 永远不会返回 null(例如,DTO 中的字段在构造时已初始化为空字符串),则可以直接使用 isEmpty(),不需要 == null 判断。

  • 如果不确定或可能返回 null,保留双重判断是安全的,但代码略显冗长。可以:

    • 忽略 IDEA 提示(按 Alt+Enter 选择“忽略”或调整检查级别)。

    • 使用工具方法简化,例如:

      java

      import org.apache.commons.lang3.StringUtils;
      
      && (queryDTO.getCreateDateRange() == null 
          || StringUtils.isEmpty(queryDTO.getCreateDateRange().getBeginDateTime()))
      && (queryDTO.getCreateDateRange() == null 
          || StringUtils.isEmpty(queryDTO.getCreateDateRange().getEndDateTime()))

      这样既简洁又能处理 null

关于你最后的代码

你的代码逻辑正确,已覆盖 createDateRange 为 null 以及日期字段为 null 或空字符串的情况,不会有运行时风险。IDEA 的提示仅属于代码风格建议,不影响功能。如果你希望消除提示,可以按上述方法调整;如果觉得当前写法清晰,也可以保留。

总结:提示的出现是因为 IDEA 认为有更简洁的写法(可能是误判或推荐工具类),你可以根据实际需求决定是否采纳。


结合原有的:
package com.weiyu.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.ToString;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

/**
 * 日期范围查询 DTO
 */
@Data
@ToString(exclude = {"cachedBeginDate", "cachedEndDate", "cachedBeginDateTime", "cachedEndDateTime"}) // toString时排除缓存字段
@SuppressWarnings("unused")
public class DateRange {
    /**
     * 开始日期
     */
    //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用分支(|)
    //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用分支(|)
    //@Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用可选组(?),现代正则最佳实践
    @Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})?$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用可选组(?),现代正则最佳实践
    //@DateTimeFormat(pattern = "yyyy-MM-dd") // 格式化入参,前端→后端(接收参数),处理表单提交、URL参数、请求参数中的日期时间字符串转换,对JSON请求无效,可以移除
    //@JsonFormat(pattern = "yyyy-MM-dd") // 格式化出参,后端→前端(返回数据),处理 JSON 数据与 Java 对象之间的转换,但用于String类型时作用有限,可以移除
    private String beginDate;

    /**
     * 结束日期
     */
    //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用分支(|)
    //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$|^$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用分支(|)
    //@Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})$", message = "日期格式必须为yyyy-MM-dd") // 不允许空字符串,使用可选组(?),现代正则最佳实践
    @Pattern(regexp = "^(\\d{4}-\\d{2}-\\d{2})?$", message = "日期格式必须为yyyy-MM-dd") // 允许空字符串,使用可选组(?),现代正则最佳实践
    //@DateTimeFormat(pattern = "yyyy-MM-dd") // 格式化入参,前端→后端(接收参数),处理表单提交、URL参数、请求参数中的日期时间字符串转换,对JSON请求无效,可以移除
    //@JsonFormat(pattern = "yyyy-MM-dd") // 格式化出参,后端→前端(返回数据),处理 JSON 数据与 Java 对象之间的转换,但用于String类型时作用有限,可以移除
    private String endDate;

    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    // 缓存字段,避免重复解析
    @JsonIgnore
    private transient LocalDate cachedBeginDate;

    @JsonIgnore
    private transient LocalDate cachedEndDate;

    @JsonIgnore
    private transient LocalDateTime cachedBeginDateTime;

    @JsonIgnore
    private transient LocalDateTime cachedEndDateTime;

    // ========== 业务方法 ==========

    /**
     * 获取解析后的开始日期(计算属性,使用缓存)
     * 注意:此方法提供更直接的LocalDate访问,减少重复解析
     */
    @JsonIgnore
    public Optional<LocalDate> getParsedBeginDate() {
        // 优先返回缓存
        if (cachedBeginDate != null) {
            return Optional.of(cachedBeginDate);
        }

        if (!StringUtils.hasText(beginDate)) {
            return Optional.empty();
        }

        try {
            // 计算并缓存
            cachedBeginDate = LocalDate.parse(beginDate, DATE_FORMATTER);
            return Optional.of(cachedBeginDate);
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    /**
     * 获取解析后的结束日期(计算属性,使用缓存)
     * 注意:此方法提供更直接的LocalDate访问,减少重复解析
     */
    @JsonIgnore
    public Optional<LocalDate> getParsedEndDate() {
        // 优先返回缓存
        if (cachedEndDate != null) {
            return Optional.of(cachedEndDate);
        }

        if (!StringUtils.hasText(endDate)) {
            return Optional.empty();
        }

        try {
            // 计算并缓存
            cachedEndDate = LocalDate.parse(endDate, DATE_FORMATTER);
            return Optional.of(cachedEndDate);
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    /**
     * 获取开始日期时间(计算属性,使用缓存)
     * 注意:此方法提供更直接的LocalDateTime访问,减少重复解析
     */
    @JsonIgnore
    public Optional<LocalDateTime> getBeginDateTime() {
        // 优先返回缓存
        if (cachedBeginDateTime != null) {
            return Optional.of(cachedBeginDateTime);
        }

        Optional<LocalDate> beginDateOpt = getParsedBeginDate();
        if (beginDateOpt.isEmpty()) {
            return Optional.empty();
        }

        // 计算并缓存
        cachedBeginDateTime = beginDateOpt.get().atStartOfDay();
        return Optional.of(cachedBeginDateTime);
    }

    /**
     * 获取结束日期时间(计算属性,使用缓存)
     * 注意:此方法提供更直接的LocalDateTime访问,减少重复解析
     */
    @JsonIgnore
    public Optional<LocalDateTime> getEndDateTime() {
        // 优先返回缓存
        if (cachedEndDateTime != null) {
            return Optional.of(cachedEndDateTime);
        }

        Optional<LocalDate> endDateOpt = getParsedEndDate();
        if (endDateOpt.isEmpty()) {
            return Optional.empty();
        }

        // 计算并缓存
        cachedEndDateTime = endDateOpt.get().atTime(LocalTime.MAX);
        return Optional.of(cachedEndDateTime);
    }

    /**
     * 验证日期范围是否有效(开始日期 ≤ 结束日期)
     * 注意:当日期为空字符串时,返回 true
     */
    @JsonIgnore
    @AssertTrue(message = "开始日期不能晚于结束日期") // @AssertTrue 专门用于跨字段验证(涉及多个字段的业务逻辑验证)
    public boolean isValid() {
        Optional<LocalDate> beginOpt = getParsedBeginDate();  // 使用缓存方法
        Optional<LocalDate> endOpt = getParsedEndDate();      // 使用缓存方法

        if (beginOpt.isPresent() && endOpt.isPresent()) {
            return !beginOpt.get().isAfter(endOpt.get());
        }
        return true; // 允许部分为空
    }

    /**
     * 是否有有效的日期范围
     * 返回:true=有有效日期范围,false=无日期范围或无效
     */
    @JsonIgnore
    @SuppressWarnings("BooleanMethodIsAlwaysInverted") // 抑制反转方法警告
    public boolean hasValidDateRange() {
        // 两个都有值且有效
        return StringUtils.hasText(beginDate) &&
               StringUtils.hasText(endDate) &&
               isValid();
    }

    /**
     * 检查是否选择了日期范围(无论是否有效)
     */
    @JsonIgnore
    public boolean hasDateRange() {
        return StringUtils.hasText(beginDate) ||
               StringUtils.hasText(endDate);
    }

    /**
     * 检查日期范围是否完整(两个都有值或两个都没有值)
     */
    @JsonIgnore
    public boolean isComplete() {
        boolean hasBegin = StringUtils.hasText(beginDate);
        boolean hasEnd = StringUtils.hasText(endDate);
        return (hasBegin && hasEnd) || (!hasBegin && !hasEnd);
    }

    /**
     * 检查日期范围是否不完整(只有一个有值)
     */
    @JsonIgnore
    public boolean isIncomplete() {
        boolean hasBegin = StringUtils.hasText(beginDate);
        boolean hasEnd = StringUtils.hasText(endDate);
        return hasBegin != hasEnd; // 异或:只有一个有值
    }

    /**
     * 创建新的DateRange
     */
    public static DateRange of(String beginDate, String endDate) {
        DateRange range = new DateRange();
        range.setBeginDate(beginDate);
        range.setEndDate(endDate);
        return range;
    }

    // ========== 自定义setter以支持缓存清理 ==========

    /**
     * 自定义setter,设置beginDate时清除缓存
     * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter
     */
    public void setBeginDate(String beginDate) {
        this.beginDate = beginDate;
        this.cachedBeginDate = null;      // 清除日期缓存
        this.cachedBeginDateTime = null;  // 清除日期时间缓存
    }

    /**
     * 自定义setter,设置endDate时清除缓存
     * 注意:使用@Setter注解时无法直接添加缓存清理逻辑,需要显式定义setter
     */
    public void setEndDate(String endDate) {
        this.endDate = endDate;
        this.cachedEndDate = null;        // 清除日期缓存
        this.cachedEndDateTime = null;    // 清除日期时间缓存
    }

    /**
     * 清除所有缓存
     */
    @JsonIgnore
    public void clearCache() {
        this.cachedBeginDate = null;
        this.cachedEndDate = null;
        this.cachedBeginDateTime = null;
        this.cachedEndDateTime = null;
    }

    /**
     * 预加载所有缓存(适用于频繁访问的场景)
     */
    @JsonIgnore
    public void preloadCache() {
        getBeginDateTime();  // 这会触发缓存加载
        getEndDateTime();    // 这会触发缓存加载
    }

    /**
     * 检查缓存是否已加载
     */
    @JsonIgnore
    public boolean isCacheLoaded() {
        return cachedBeginDateTime != null && cachedEndDateTime != null;
    }

    // ========== Builder模式支持 ==========

    @JsonIgnore
    public DateRangeBuilder toBuilder() {
        return new DateRangeBuilder()
                .beginDate(beginDate)
                .endDate(endDate);
    }

    public static DateRangeBuilder builder() {
        return new DateRangeBuilder();
    }

    public static class DateRangeBuilder {
        private String beginDate;
        private String endDate;

        public DateRangeBuilder beginDate(String beginDate) {
            this.beginDate = beginDate;
            return this;
        }

        public DateRangeBuilder endDate(String endDate) {
            this.endDate = endDate;
            return this;
        }

        public DateRange build() {
            return DateRange.of(beginDate, endDate);
        }
    }
}
最终优化为:
    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());
    }
这样是否合理?

你的最终代码已经解决了之前的问题,并且引入了 Optional.isEmpty() 来判断日期时间是否存在,这在 Java 11+ 环境下是可行的。但结合你提供的 DateRange 类设计,这个写法仍有几个值得商榷的地方,可能影响代码的健壮性和可读性。

当前代码分析

java

&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
&& (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty())

优点

  • 正确处理了 createDateRange 为 null 的情况,避免 NPE。

  • 利用 DateRange 已有的 getBeginDateTime() 和 getEndDateTime() 方法,复用了解析逻辑。

潜在问题

  1. Optional.isEmpty() 的 Java 版本依赖
    Optional.isEmpty() 是 Java 11 引入的方法。如果项目仍使用 Java 8,代码将无法编译。你需要确认项目的 JDK 版本。

  2. 将无效日期视为“空条件”可能引发歧义
    getBeginDateTime() 在日期字符串为空或解析失败(如 "2025-02-30")时返回 Optional.empty()
    如果用户输入了格式正确但实际不存在的日期(如 2 月 30 日),@Pattern 注解无法捕获这种逻辑错误,导致该无效日期被当成空条件处理,从而绕过“至少一个查询条件”的检查,最终查询时忽略了这个日期条件。用户期望得到错误提示,但系统却静默执行,可能造成困惑。

  3. 语义不够直观
    getBeginDateTime().isEmpty() 检查的是“是否存在有效的开始日期时间”,而当前方法的本意是“日期范围的字符串是否为空”。两者在概念上略有差异,阅读代码时需要理解 DateRange 的内部行为,增加了认知负担。

优化建议

方案一:直接检查日期字符串(推荐)

直接使用 DateRange 的 getBeginDate() 和 getEndDate() 进行判空,与其它字段的检查方式保持一致,逻辑更清晰,且不受解析失败的影响(日期有效性应由校验层处理)。

java

&& (queryDTO.getCreateDateRange() == null
    || ( (queryDTO.getCreateDateRange().getBeginDate() == null || queryDTO.getCreateDateRange().getBeginDate().trim().isEmpty())
         && (queryDTO.getCreateDateRange().getEndDate() == null || queryDTO.getCreateDateRange().getEndDate().trim().isEmpty()) ))

如果项目已引入 Spring,可以复用 StringUtils.hasText 使代码更简洁:

java

import org.springframework.util.StringUtils;

&& (queryDTO.getCreateDateRange() == null
    || (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
        && !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())))

方案二:保持使用 Optional.isEmpty(),但增强日期有效性校验

如果你希望沿用 getBeginDateTime().isEmpty(),建议在 DateRange 中增加对单个日期有效性的校验(例如通过 @AssertTrue 或自定义注解),确保无效日期能被提前拦截,不会混入业务逻辑。但这样会增加校验复杂度,且与当前 DateRange 的设计(解析失败返回 Optional.empty())不完全一致。

总结

当前代码是否合理?

  • 在 Java 11+ 且你接受将无效日期视为空条件的前提下,可以工作

  • 但从代码健壮性和可维护性角度,建议改为直接检查日期字符串,理由如上。

最终优化后的代码(使用 StringUtils.hasText)示例:

java

private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
    if (queryDTO == null) {
        return true;
    }

    return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
           && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
           && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
           && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
           && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
           && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
           && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
           && queryDTO.getCapitalYear() == null
           && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
           && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
           && (queryDTO.getCreateDateRange() == null
               || (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
                   && !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())));
}

这样既避免了 NPE,又消除了对 Optional.isEmpty() 的依赖和无效日期被静默忽略的风险,同时与其他字段的判空风格保持一致。


最终优化:

    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryDTO queryDTO) {
        if (queryDTO == null) {
            return true;
        }

//        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
//               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
//               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
//               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
//               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
//               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
//               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
//               && queryDTO.getCapitalYear() == null
//               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
//               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
//               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
//               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());

        return StringUtils.hasText(queryDTO.getCapitalNo())
               && StringUtils.hasText(queryDTO.getCapitalName())
               && StringUtils.hasText(queryDTO.getCapitalType())
               && StringUtils.hasText(queryDTO.getCapitalIndexType())
               && StringUtils.hasText(queryDTO.getCapitalAccount())
               && StringUtils.hasText(queryDTO.getCapitalSource())
               && StringUtils.hasText(queryDTO.getCapitalIndexSource())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && StringUtils.hasText(queryDTO.getRemark())
               && (queryDTO.getCreateDateRange() == null
                   || (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
                       && !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())));
    }

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐