批量添加对象的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
相关产品推荐
相关产品推荐

