You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring Boot+Thymeleaf+JPA中实现可确认的告警与错误提示

实现Thymeleaf表单的可确认告警功能

需求场景

  • 预订日期:日期在过去→错误(必须修正);日期在一年以后→可确认告警(用户可选择忽略/确认)
  • 预订报价:金额≤0→错误;金额大于1000美元→可确认告警

当前问题

  1. 用Jakarta Validation约束实现告警后,Thymeleaf模板无法区分「错误」和「告警」,无法给告警添加可确认交互(比如勾选框)
  2. 不知道如何将「告警已确认」的状态传递回Validator,确保用户修正所有错误、确认所有告警后才能保存数据

现有代码

实体类代码

@Entity
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode
public class BookingHeader {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;

        @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
        @FutureOrPresent
        @OpenBookingPeriodConstraint(message = "{BookingHeader.BookingDate.OpenBookingPeriodConstraint}")
        private Date bookingDate;
        
        @Positive
        @OfferPriceTooHighConstraint
        private int bookingOffer;
}

自定义约束注解

@Documented
@Constraint(validatedBy = OpenBookingPeriodValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenBookingPeriodConstraint {
       String message() default "Booking is too far in the future";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
}

约束验证器

public class OpenBookingPeriodValidator implements ConstraintValidator<OpenBookingPeriodConstraint, Date> {

        @Override
        public void initialize(OpenBookingPeriodConstraint constraint) {
        }

        @Override
        public boolean isValid(Date dateField, ConstraintValidatorContext cxt) {

                Calendar cal = Calendar.getInstance();
                cal.setTime(new Date());
                cal.add(Calendar.DATE, 365);
                return dateField != null && dateField.before(cal.getTime());
        }

}

Thymeleaf模板代码

<div class = "container">
    <div class = "row">
        <div class = "col-lg-6 col-md-6 col-sm-6 container justify-content-center card">
            <h1 class ="text-center"> Create Booking</h1>
            <div class = "card-body">
                <form th:action="@{/booking/add}" th:object="${bookingHeader}" method="post" id="contactForm1" data-bs-target="#main_menu_users">
                    
                    <div class ="form-group">
                        <label for="bookingDate">bookingDate</label>
                        <input type="date" th:field="*{bookingDate}" id="bookingDate" placeholder="bookingDate" class="form-control">
                        <span th:if="${#fields.hasErrors('bookingDate')}" th:errors="*{bookingDate}"></span>
                    </div>
                    <br>
                    <div class ="form-group">
                        <label for="bookingOffer">bookingOffer</label>
                        <input type="number" th:field="*{bookingOffer}" id="bookingOffer" placeholder="bookingOffer" class="form-control">
                        <span th:if="${#fields.hasErrors('bookingOffer')}" th:errors="*{bookingOffer}"></span>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

当前效果截图

  • 错误显示正常:错误显示正常
  • 需改为可确认告警的消息:这些消息应为可跳过/确认的告警

解决方案

1. 用Payload标记区分错误和告警

利用Jakarta Validation的Payload接口,给不同类型的验证结果打标记:

定义Payload标记类

// 标记告警类型的验证消息
public interface AlertPayload extends Payload {}
// 标记错误类型的验证消息(默认约束如@Positive可归为这类)
public interface ErrorPayload extends Payload {}

修改自定义约束的Payload配置

将告警类的约束指定AlertPayload:

@Documented
@Constraint(validatedBy = OpenBookingPeriodValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenBookingPeriodConstraint {
       String message() default "Booking is too far in the future";
        Class<?>[] groups() default {};
        // 指定告警类型Payload
        Class<? extends Payload>[] payload() default {AlertPayload.class};
}

OfferPriceTooHighConstraint做同样修改,指定AlertPayload。

2. Thymeleaf模板区分显示错误和告警

通过Thymeleaf的#fields对象获取错误的Payload信息,分别渲染错误和带确认框的告警:

修改模板代码

<div class ="form-group">
    <label for="bookingDate">bookingDate</label>
    <input type="date" th:field="*{bookingDate}" id="bookingDate" placeholder="bookingDate" class="form-control">
    <!-- 渲染错误消息(无AlertPayload标记) -->
    <span th:if="${#fields.hasErrors('bookingDate')}" 
          th:each="err : ${#fields.errors('bookingDate')}"
          th:if="${!err.payload.?[T(com.yourpackage.AlertPayload).isInstance(it)]}"
          class="text-danger">
        [[${err.message}]]
    </span>
    <!-- 渲染可确认告警 -->
    <div th:if="${#fields.hasErrors('bookingDate')}" 
         th:each="err : ${#fields.errors('bookingDate')}"
         th:if="${err.payload.?[T(com.yourpackage.AlertPayload).isInstance(it)]}"
         class="text-warning">
        [[${err.message}]]
        <input type="checkbox" th:field="*{confirmedAlerts['bookingDate']}" /> 确认忽略此告警
    </div>
</div>

<div class ="form-group">
    <label for="bookingOffer">bookingOffer</label>
    <input type="number" th:field="*{bookingOffer}" id="bookingOffer" placeholder="bookingOffer" class="form-control">
    <!-- 渲染错误消息 -->
    <span th:if="${#fields.hasErrors('bookingOffer')}" 
          th:each="err : ${#fields.errors('bookingOffer')}"
          th:if="${!err.payload.?[T(com.yourpackage.AlertPayload).isInstance(it)]}"
          class="text-danger">
        [[${err.message}]]
    </span>
    <!-- 渲染可确认告警 -->
    <div th:if="${#fields.hasErrors('bookingOffer')}" 
         th:each="err : ${#fields.errors('bookingOffer')}"
         th:if="${err.payload.?[T(com.yourpackage.AlertPayload).isInstance(it)]}"
         class="text-warning">
        [[${err.message}]]
        <input type="checkbox" th:field="*{confirmedAlerts['bookingOffer']}" /> 确认忽略此告警
    </div>
</div>

3. 实体类添加告警确认状态字段

在BookingHeader中添加临时字段存储用户确认的告警(不持久化到数据库):

@Entity
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode
public class BookingHeader {
    // 原有字段...

    // 存储已确认的告警字段名,@Transient避免持久化
    @Transient
    private Map<String, Boolean> confirmedAlerts = new HashMap<>();
}

4. 修改验证器逻辑:跳过已确认的告警

在验证器中获取实体对象,判断用户是否已确认该告警,确认后直接通过验证:

public class OpenBookingPeriodValidator implements ConstraintValidator<OpenBookingPeriodConstraint, Date> {

    @Override
    public boolean isValid(Date dateField, ConstraintValidatorContext context) {
        // 获取根实体对象
        BookingHeader booking = (BookingHeader) context.getRootBean();
        
        // 用户已确认该告警,直接返回有效
        if (Boolean.TRUE.equals(booking.getConfirmedAlerts().get("bookingDate"))) {
            return true;
        }

        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        cal.add(Calendar.DATE, 365);
        return dateField != null && dateField.before(cal.getTime());
    }
}

OfferPriceTooHighValidator做同样修改,判断confirmedAlerts.get("bookingOffer")。

5. 控制器处理提交逻辑

先校验必须修正的错误,再检查所有告警是否已确认,全部通过后再保存数据:

@PostMapping("/booking/add")
public String addBooking(@Valid @ModelAttribute BookingHeader booking, BindingResult result, Model model) {
    // 检查是否有必须修正的错误(非告警类验证失败)
    boolean hasMandatoryErrors = result.getAllErrors().stream()
            .anyMatch(err -> !err.getPayload().stream().anyMatch(AlertPayload.class::isInstance));
    
    if (hasMandatoryErrors) {
        model.addAttribute("bookingHeader", booking);
        return "booking-form";
    }

    // 检查所有告警是否已确认
    boolean allAlertsConfirmed = result.getAllErrors().stream()
            .filter(err -> err.getPayload().stream().anyMatch(AlertPayload.class::isInstance))
            .allMatch(err -> {
                String fieldName = ((FieldError) err).getField();
                return Boolean.TRUE.equals(booking.getConfirmedAlerts().get(fieldName));
            });

    if (!allAlertsConfirmed) {
        model.addAttribute("bookingHeader", booking);
        model.addAttribute("alertError", "请确认所有告警后提交");
        return "booking-form";
    }

    // 所有校验通过,保存数据
    bookingRepository.save(booking);
    return "redirect:/booking/list";
}

内容的提问来源于stack exchange,提问作者Borbarad13

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 19:13:09