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

批量添加对象的POST请求:捕获并返回单个对象的验证错误

批量添加对象时的验证错误处理方案

我在API中实现了一个接收对象列表作为参数的批量添加方法,需要处理可能存在的验证错误,要求响应中包含每个出错对象的具体错误信息——比如3个对象缺少手机号、1个对象缺少SSN这类情况,该如何触发这些错误并在响应中展示?以下是我的代码实现:

InvoiceDTO 代码

public class InvoiceDTO {

  @NotNull(message = "PersonId can't be null")
  @PersonId
  private String personId;

  @NotNull(message = "invoiceDate can't be null")
  @PastOrPresent
  @JsonFormat(pattern = "yyyy-MM-dd")
  private LocalDate invoiceDate;

  @NotNull(message = "invoiceNumber can't be null")
  private String invoiceNumber;

  @NotNull(message = "phone number can't be null")
  private Long phoneNumber;

  @NotNull(message = "invoiceAmount can't be null")
  private Double invoiceAmount; //how much does the client ows the org

  @NotNull(message = "invoiceNumber can't be null")
  private String accountNumber;

  private int msdin; //Type of subscription
}

Controller 代码

@PostMapping("/invoices")
private ResponseEntity<Response> addNewInvoices(@RequestBody @Valid List<InvoiceDTO> invoiceDTO) {

  //Todo find a way to through exceptions for every object if validations errors occurs
  ResponseEntity<Response> response = null;
  for (InvoiceDTO invoices : invoiceDTO) {
    Optional<Organization> organization = this.organizationService.getOrganizationFromRequest();
    Optional<Madeen> madeen = madeenRepository.findByPersonId(invoices.getPersonId());


    if (!madeen.isPresent()) {
      Madeen newMadeen = new Madeen();
      newMadeen.setCreatedDate(LocalDate.now());
      newMadeen.setPersonId(invoices.getPersonId());
      newMadeen.setStatus(Status.NOT_MADEEN);
      newMadeen.setMadeenTimeTracker(null);
      newMadeen.setOrganization(organization.get());
      madeenRepository.save(newMadeen);
    }

    log.info("Add new Invoice : {}", invoiceDTO);
    String tcn = UUID.randomUUID().toString();
    AuditLog auditLog = new AuditLog();
    auditLog.setTcn(tcn);
    auditLog.setRequestBody(invoiceDTO.toString());
    response = null;


    //Add the new amount of the invoice to an existing debts
    Optional<Organization> organization1 = this.organizationService.getOrganizationFromRequest();
    Optional<Debts> debts = debtsRepository.findDebtsByPersonIdAndOrganization_id(invoices.getPersonId(), organization1.get().getId());
    Optional<Madeen> madeenOptional = madeenRepository.findByPersonId(invoices.getPersonId());


    if (!debts.isPresent()) {
      Debts newDebt = new Debts();
      newDebt.setPersonId(invoices.getPersonId());
      newDebt.setCreatedDate(LocalDate.now());
      newDebt.setUpdatedDate(invoices.getInvoiceDate());
      newDebt.setDebtAmount(invoices.getInvoiceAmount());
      newDebt.setOrganization(organization1.get());
      newDebt.setMadeenId(madeenOptional.get());
      debtsRepository.save(newDebt);
    }

    Optional<Debts> updatedDebts = debtsRepository.findDebtsByPersonIdAndOrganization_id(invoices.getPersonId(), organization1.get().getId());
    Madeen madeenCheck = madeenOptional.get();
    Debts existingDebts = updatedDebts.get();

    if (debts.isPresent()) {
      existingDebts.setDebtAmount(existingDebts.getDebtAmount() + invoices.getInvoiceAmount());
      existingDebts.setUpdatedDate(invoices.getInvoiceDate());
      debtsRepository.save(existingDebts);
    }

    if (existingDebts.getDebtAmount() < -1) {
      madeenCheck.setStatus(Status.STUMBLED);
      madeenRepository.save(madeenCheck);
    }
    //Calculate the amount of debts
    List<Debts> findAllDebts = debtsRepository.findDebtsByPersonId(invoices.getPersonId());
    double totalDebts = 0; //find how much is it the totalDebts among all organization;
    for (Debts item : findAllDebts) {
      totalDebts = totalDebts + item.getDebtAmount();
    }

    if (totalDebts <= -750 && madeenCheck.getMadeenTimeTracker() == null) {
      madeenCheck.setMadeenTimeTracker(LocalDate.now());
      madeenRepository.save(madeenCheck);
    }

    if (totalDebts > -750) {
      madeenCheck.setMadeenTimeTracker(null);
      madeenRepository.save(madeenCheck);
    }

    if (madeenCheck.getMadeenTimeTracker() != null) {
      LocalDate sixtyDaysFromLastInvoice = madeenCheck.getMadeenTimeTracker().plus(Period.ofDays(60));
      if (totalDebts <= -750 && LocalDate.now().isAfter(sixtyDaysFromLastInvoice)) {
        madeenCheck.setStatus(Status.MADEEN);
        madeenRepository.save(madeenCheck);
      }
    }

    if (totalDebts >= 0) {
      madeenCheck.setStatus(Status.NOT_MADEEN);
      madeenRepository.save(madeenCheck);

    }
  }
  return response;
}

解决方案

1. 问题核心

Spring默认的@Valid注解对List类型参数的验证只会抛出第一个遇到的错误,无法收集所有对象的验证信息,因此需要手动遍历每个对象进行校验,收集全部错误后再处理合法对象。

2. 步骤实现

(1)注入Validator

在Controller中引入Spring的Validator,用于手动校验每个DTO对象:

@Autowired
private Validator validator;

(2)定义批量错误响应结构

创建自定义响应类,用于返回所有错误详情和成功处理的对象:

public class BatchValidationResponse {
    // 错误对象列表
    private List<ObjectErrorDetails> errors;
    // 成功处理的对象列表
    private List<InvoiceDTO> successfulInvoices;

    // 单个对象的错误详情
    public static class ObjectErrorDetails {
        // 对象在列表中的索引位置
        private int index;
        // 对象的唯一标识(方便定位)
        private String personId;
        // 该对象的所有错误信息
        private List<String> errorMessages;

        // Getter、Setter、构造方法
        public ObjectErrorDetails(int index, String personId, List<String> errorMessages) {
            this.index = index;
            this.personId = personId;
            this.errorMessages = errorMessages;
        }

        // 省略Getter、Setter
    }

    // Getter、Setter、构造方法
    public BatchValidationResponse() {
        this.errors = new ArrayList<>();
        this.successfulInvoices = new ArrayList<>();
    }

    // 省略Getter、Setter
}

(3)改造Controller逻辑

修改addNewInvoices方法,先批量校验所有对象,收集错误,再处理合法对象:

@PostMapping("/invoices")
public ResponseEntity<BatchValidationResponse> addNewInvoices(@RequestBody List<InvoiceDTO> invoiceDTOList) {
    BatchValidationResponse response = new BatchValidationResponse();

    // 遍历所有对象,逐个校验
    for (int i = 0; i < invoiceDTOList.size(); i++) {
        InvoiceDTO invoice = invoiceDTOList.get(i);
        // 执行DTO字段校验
        Set<ConstraintViolation<InvoiceDTO>> violations = validator.validate(invoice);

        if (!violations.isEmpty()) {
            // 收集该对象的错误信息
            List<String> errorMessages = violations.stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.toList());
            response.getErrors().add(new BatchValidationResponse.ObjectErrorDetails(
                    i, invoice.getPersonId(), errorMessages
            ));
        } else {
            // 校验通过,加入待处理列表
            response.getSuccessfulInvoices().add(invoice);
        }
    }

    // 处理所有校验通过的对象
    for (InvoiceDTO invoice : response.getSuccessfulInvoices()) {
        Optional<Organization> organization = this.organizationService.getOrganizationFromRequest();
        Optional<Madeen> madeen = madeenRepository.findByPersonId(invoice.getPersonId());

        // 创建Madeen(如果不存在)
        if (!madeen.isPresent()) {
            Madeen newMadeen = new Madeen();
            newMadeen.setCreatedDate(LocalDate.now());
            newMadeen.setPersonId(invoice.getPersonId());
            newMadeen.setStatus(Status.NOT_MADEEN);
            newMadeen.setMadeenTimeTracker(null);
            newMadeen.setOrganization(organization.get());
            madeenRepository.save(newMadeen);
        }

        // 记录审计日志
        log.info("Add new Invoice : {}", invoice);
        String tcn = UUID.randomUUID().toString();
        AuditLog auditLog = new AuditLog();
        auditLog.setTcn(tcn);
        auditLog.setRequestBody(invoice.toString());
        // 这里需要补充auditLog的保存逻辑,原代码中缺失

        // 处理债务逻辑
        Optional<Organization> org = this.organizationService.getOrganizationFromRequest();
        Optional<Debts> debts = debtsRepository.findDebtsByPersonIdAndOrganization_id(invoice.getPersonId(), org.get().getId());
        Optional<Madeen> madeenOptional = madeenRepository.findByPersonId(invoice.getPersonId());

        if (!debts.isPresent()) {
            // 创建新债务
            Debts newDebt = new Debts();
            newDebt.setPersonId(invoice.getPersonId());
            newDebt.setCreatedDate(LocalDate.now());
            newDebt.setUpdatedDate(invoice.getInvoiceDate());
            newDebt.setDebtAmount(invoice.getInvoiceAmount());
            newDebt.setOrganization(org.get());
            newDebt.setMadeenId(madeenOptional.get());
            debtsRepository.save(newDebt);
        } else {
            // 更新现有债务
            Debts existingDebts = debts.get();
            existingDebts.setDebtAmount(existingDebts.getDebtAmount() + invoice.getInvoiceAmount());
            existingDebts.setUpdatedDate(invoice.getInvoiceDate());
            debtsRepository.save(existingDebts);
        }

        // 更新Madeen状态
        Madeen madeenCheck = madeenOptional.orElseThrow(() -> new RuntimeException("Madeen not found"));
        List<Debts> allDebts = debtsRepository.findDebtsByPersonId(invoice.getPersonId());
        double totalDebts = allDebts.stream().mapToDouble(Debts::getDebtAmount).sum();

        // 更新状态逻辑
        if (totalDebts < -1) {
            madeenCheck.setStatus(Status.STUMBLED);
        } else if (totalDebts >= 0) {
            madeenCheck.setStatus(Status.NOT_MADEEN);
        } else if (madeenCheck.getMadeenTimeTracker() != null) {
            LocalDate sixtyDaysLater = madeenCheck.getMadeenTimeTracker().plus(Period.ofDays(60));
            if (totalDebts <= -750 && LocalDate.now().isAfter(sixtyDaysLater)) {
                madeenCheck.setStatus(Status.MADEEN);
            }
        }

        // 更新时间跟踪器
        if (totalDebts <= -750 && madeenCheck.getMadeenTimeTracker() == null) {
            madeenCheck.setMadeenTimeTracker(LocalDate.now());
        } else if (totalDebts > -750) {
            madeenCheck.setMadeenTimeTracker(null);
        }

        madeenRepository.save(madeenCheck);
    }

    return ResponseEntity.ok(response);
}

(4)补充业务验证(可选)

如果有字段注解无法覆盖的业务规则(比如personId必须存在于系统中),可以在DTO校验后添加额外校验,并将错误加入响应:

// 在DTO校验通过后添加
Optional<Madeen> madeenCheck = madeenRepository.findByPersonId(invoice.getPersonId());
if (!madeenCheck.isPresent()) {
    response.getErrors().add(new BatchValidationResponse.ObjectErrorDetails(
            i, invoice.getPersonId(), List.of("PersonId不存在于系统中")
    ));
    continue; // 跳过后续业务处理
}

3. 响应示例

当存在错误时,返回的JSON结构如下:

{
  "errors": [
    {
      "index": 0,
      "personId": "P123",
      "errorMessages": ["phone number can't be null", "invoiceAmount can't be null"]
    },
    {
      "index": 2,
      "personId": "P456",
      "errorMessages": ["PersonId can't be null"]
    }
  ],
  "successfulInvoices": [
    {
      "personId": "P789",
      "invoiceDate": "2024-05-20",
      "invoiceNumber": "INV-001",
      "phoneNumber": 13800138000,
      "invoiceAmount": 100.0,
      "accountNumber": "ACC-001",
      "msdin": 1
    }
  ]
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 00:10:51