PAYATHON 2026

Spring Boot 确认发票后如何更新支付方式实体?

支付老李

结论

问题通常不在 @Transactional 或 saveAll(),而是查询参数传错了。

findByTempInvoiceEntity_TempInvoiceId(...) 接收的是临时发票 ID,原代码传入的却是:

payment.getPaymentId()

支付记录 ID 和临时发票 ID 通常不是同一个值,所以查询结果很可能为空。查不到 CardEntity,也就无法更新 confirmInvoiceId。

另外还有三个问题需要处理:

  • 删除 TempInvoiceEntity 时,CardEntity 可能仍在引用它,从而触发外键约束或级联操作。
  • 第二种写法在 filter() 中更新数据,而且始终返回 false。这种副作用用法不合适。
  • 方法捕获了所有异常,却没有重新抛出。这可能导致事务不按预期回滚,也会掩盖真正的数据库错误。

先确认查询维度

当前 Repository 方法是:

List<CardEntity> findByTempInvoiceEntity_TempInvoiceId(Long tempInvoiceId);

它会按照下面的实体路径查询:

CardEntity.tempInvoiceEntity.tempInvoiceId

因此,这里应该传入 invoiceId:

List<CardEntity> cardEntities =
        cardRepo.findByTempInvoiceEntity_TempInvoiceId(invoiceId);

而不是:

payment.getPaymentId()

如果实际业务关系是“一条支付记录对应一条或多条卡支付记录”,Repository 就应该按 paymentId 查询:

List<CardEntity> findByPaymentId(Long paymentId);

调用方式也要相应改为:

List<CardEntity> cardEntities =
        cardRepo.findByPaymentId(payment.getPaymentId());

这两种查询对应不同的业务关系,不能混用。

推荐实现

如果所有卡支付记录都直接关联临时发票,就不必针对每个 PaymentsEntity 重复查询。按发票一次查出并更新即可:

@Repository
public interface CardRepo extends JpaRepository<CardEntity, Long> {

    List<CardEntity> findByTempInvoiceEntity_TempInvoiceId(Long tempInvoiceId);
}

服务方法:

private void alterPaymentMethodConfirmInvoice(
        Long tempInvoiceId,
        List<PaymentsEntity> payments,
        ConfirmInvoiceEntity confirmedInvoice) {

    boolean containsCardPayment = payments.stream()
            .anyMatch(payment ->
                    "card".equalsIgnoreCase(payment.getPaymentType()));

    if (!containsCardPayment) {
        return;
    }

    List<CardEntity> cardEntities =
            cardRepo.findByTempInvoiceEntity_TempInvoiceId(tempInvoiceId);

    for (CardEntity cardEntity : cardEntities) {
        cardEntity.setConfirmInvoiceEntity(confirmedInvoice);

        // 如果确认后要删除临时发票,必须解除这个外键关系。
        cardEntity.setTempInvoiceEntity(null);
    }

    cardRepo.saveAll(cardEntities);
}

主流程需要明确传入原始的 invoiceId:

@Override
@Transactional
public NonPaginatedResponse insertIntoConfirmInvoice(Long invoiceId) {
    TempInvoiceEntity selectedTempInvoice = tempInvoiceRepo.findById(invoiceId)
            .orElseThrow(() ->
                    new IllegalArgumentException("Temp invoice not found: " + invoiceId));

    ConfirmInvoiceEntity confirmedInvoice =
            createNewConfirmInvoiceData(selectedTempInvoice);

    boolean cartItemsConfirmed =
            confirmProductCartService.confirmTheCartItems(invoiceId, confirmedInvoice);

    if (!cartItemsConfirmed) {
        throw new IllegalStateException("Cart items could not be confirmed");
    }

    List<PaymentsEntity> payments =
            paymentsRepo.findBySalesInvoice_TempInvoiceId(invoiceId);

    alterPaymentMethodConfirmInvoice(
            invoiceId,
            payments,
            confirmedInvoice
    );

    List<PaymentsEntity> confirmedPayments = payments.stream()
            .map(payment -> {
                PaymentsEntity confirmedPayment = new PaymentsEntity();
                confirmedPayment.setPaymentType(payment.getPaymentType());
                confirmedPayment.setPaidDate(payment.getPaidDate());
                confirmedPayment.setPaidAmount(payment.getPaidAmount());
                confirmedPayment.setConfirmInvoice(confirmedInvoice);
                confirmedPayment.setSalesInvoice(null);
                return confirmedPayment;
            })
            .toList();

    paymentsRepo.saveAll(confirmedPayments);

    /*
     * 在删除临时发票之前提前 flush,可以让外键、字段映射等错误
     * 在事务方法内部暴露出来。
     */
    cardRepo.flush();
    paymentsRepo.flush();

    tempInvoiceRepo.delete(selectedTempInvoice);

    NonPaginatedResponse response = new NonPaginatedResponse();
    response.setStatus(HttpStatus.ACCEPTED);
    response.setSuccessMessage("Invoice has been confirmed.");
    response.setResult(null);
    return response;
}

不要用 filter() 执行更新

下面的代码确实会在调用 .toList() 时执行,但它的语义不对:

selectAllPayments.stream().filter(allPay -> {
    // 修改数据库
    return false;
}).toList();

filter() 应该只用来筛选元素。这里始终返回 false,得到的列表一定为空,而且把数据库更新藏在筛选操作中,也不便于阅读和排查。

如果只是遍历,使用普通循环即可:

for (PaymentsEntity payment : selectAllPayments) {
    if ("card".equalsIgnoreCase(payment.getPaymentType())) {
        // 更新卡支付记录
    }
}

也可以先判断是否存在卡支付:

boolean hasCardPayment = selectAllPayments.stream()
        .anyMatch(payment ->
                "card".equalsIgnoreCase(payment.getPaymentType()));

检查关联字段是否由 CardEntity 维护

按照当前映射,@JoinColumn 写在 CardEntity 一侧,所以它是关联的拥有方:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "confirmInvoiceId")
private ConfirmInvoiceEntity confirmInvoiceEntity;

原则上,下面的赋值就可以更新 card_tbl.confirmInvoiceId:

cardEntity.setConfirmInvoiceEntity(confirmedInvoice);

同一事务中查询出来的 CardEntity 通常是托管实体。即使不调用 saveAll(),JPA 也会在事务提交时通过脏检查执行更新。当然,保留 saveAll() 也没有问题。

还要核对数据库列名和相关约束:

@JoinColumn(name = "confirmInvoiceId")

这里的 confirmInvoiceId 必须确实是 card_tbl 中的外键列。如果数据库实际使用 confirm_invoice_id,映射应改成:

@JoinColumn(name = "confirm_invoice_id")
private ConfirmInvoiceEntity confirmInvoiceEntity;

同时检查这个列是否被其他属性重复映射成只读字段,例如:

@Column(name = "confirmInvoiceId", insertable = false, updatable = false)

如果存在重复映射,或者配置了 updatable = false,Hibernate 就不会按预期更新该列。

删除临时发票时注意外键

原流程最后执行:

tempInvoiceRepo.deleteById(invoiceId);

但 CardEntity 中可能仍然保留着这个关联:

private TempInvoiceEntity tempInvoiceEntity;

如果 card_tbl.tempInvoiceId 还指向即将删除的临时发票,数据库可能拒绝删除。某些级联配置也可能导致卡支付记录被删除。

确认发票时,通常需要同时迁移关联:

cardEntity.setConfirmInvoiceEntity(confirmedInvoice);
cardEntity.setTempInvoiceEntity(null);

对应的数据库列必须允许 NULL:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tempInvoiceId", nullable = true)
private TempInvoiceEntity tempInvoiceEntity;

如果业务需要保留临时发票的历史记录,就不应删除 TempInvoiceEntity,而应增加状态字段,例如 CONFIRMED。

不要吞掉事务异常

原代码捕获所有异常后直接返回:

catch (Exception e) {
    e.printStackTrace();
    // 返回错误响应
}

Spring 默认会在异常从 @Transactional 方法抛出时自动回滚。异常一旦被捕获,事务可能继续提交之前已经执行的部分操作。

更合适的做法是让异常继续向外传播,再由统一异常处理器生成响应:

@Override
@Transactional
public NonPaginatedResponse insertIntoConfirmInvoice(Long invoiceId) {
    // 业务逻辑
}

如果必须在当前方法中捕获异常,就应该重新抛出:

catch (Exception e) {
    throw new IllegalStateException(
            "Couldn't confirm invoice: " + invoiceId, e);
}

也可以显式标记事务回滚,但通常没有重新抛出清晰:

TransactionAspectSupport.currentTransactionStatus()
        .setRollbackOnly();

排查时最先检查的内容

先确认查询是否真的返回了数据:

List<CardEntity> cards =
        cardRepo.findByTempInvoiceEntity_TempInvoiceId(invoiceId);

if (cards.isEmpty()) {
    throw new IllegalStateException(
            "No card records found for temp invoice: " + invoiceId);
}

如果列表为空,问题出在查询参数或已有数据,而不是 setter。

如果列表不为空,但数据库仍未更新,可以按以下顺序检查:

  1. @JoinColumn 是否指向 card_tbl 中正确的列。
  2. setConfirmInvoiceEntity() 是否确实给字段赋了值。
  3. 该关联是否配置了 updatable = false。
  4. 删除临时发票时是否出现外键或级联异常。
  5. 异常是否被 catch (Exception) 隐藏。
  6. 是否在事务提交前查看数据库,因而误以为更新没有发生。

需要修正的重点是明确区分 paymentId 和 tempInvoiceId,根据真实的关联关系查询 CardEntity,更新关联的拥有方,并在删除临时发票前解除旧关联。

备注:内容仅供参考。