perf: 优化正式结算单分页查询N+1导致接口超时

This commit is contained in:
刘泉佳
2026-11-05 14:59:27 +08:00
parent 72553b68c5
commit 1b811181a8
@@ -11,6 +11,7 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
@@ -221,19 +222,108 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids); if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids);
} }
IPage<FormalSettlement> result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); IPage<FormalSettlement> result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime));
// 本页应付结算单的项目预付核销合计一次查询,避免逐行 toVO 重算汇总时的 N+1 Page<FormalSettlementVO> voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
Map<Long, BigDecimal> writeOffAmounts = this.projectAdvanceWriteOffService.sumBySettlementIds( // 收款汇总与预结算单号按本页批量查询后内存分发,避免逐行 toVO 的 N+1(约 500 次 SQL → 常数次)
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, voPage.setRecords(this.toVOList(result.getRecords()));
result.getRecords().stream()
.filter(item -> "payable".equals(item.getSettlementType()))
.map(FormalSettlement::getId).toList());
IPage<FormalSettlementVO> voPage = result.convert(item -> this.toVO(item, writeOffAmounts));
fillAttachmentSyncStatus(voPage.getRecords()); fillAttachmentSyncStatus(voPage.getRecords());
fillKingdeeLockStatus(voPage.getRecords()); fillKingdeeLockStatus(voPage.getRecords());
fillKingdeeAssignStatus(voPage.getRecords()); fillKingdeeAssignStatus(voPage.getRecords());
return voPage; return voPage;
} }
/**
* 列表页批量 Entity → VO:收款汇总各数据源与预结算单号按本页一次批量查询后内存分发,
* 计算口径与单笔版 toVO 完全一致(复用 calculatePaymentSummary 纯计算重载),
* 固定约 6 次查询替代逐行 toVO 的 4~5 次/行(100 行约 500 次 SQL)。
*
* @param entities 本页结算单
* @return 本页 VO 列表
*/
private List<FormalSettlementVO> toVOList(List<FormalSettlement> entities) {
if (Func.isEmpty(entities)) {
return new ArrayList<>();
}
List<Long> ids = entities.stream().map(FormalSettlement::getId).toList();
boolean hasPayable = entities.stream().anyMatch(item -> "payable".equals(item.getSettlementType()));
boolean hasReceivable = entities.stream().anyMatch(item -> "receivable".equals(item.getSettlementType()));
// 结算-付款申请关系(全量)
Map<Long, List<PaymentApplicationSettlement>> relationsBySettlement = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.in(PaymentApplicationSettlement::getFormalSettlementId, ids)
.eq(PaymentApplicationSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(PaymentApplicationSettlement::getFormalSettlementId));
// 付款申请(按结算单直挂,payment_type=结算)
Map<Long, List<PaymentApplication>> applicationsBySettlement = paymentApplicationMapper.selectList(
Wrappers.<PaymentApplication>lambdaQuery()
.in(PaymentApplication::getSettlementId, ids)
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(PaymentApplication::getSettlementId));
// 关系表引用的付款申请状态(全量一次)
Set<Long> relationApplicationIds = relationsBySettlement.values().stream()
.flatMap(List::stream).map(PaymentApplicationSettlement::getPaymentApplicationId)
.filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, String> relationStatuses = relationApplicationIds.isEmpty() ? Map.of()
: paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getId, PaymentApplication::getApprovalStatus)
.in(PaymentApplication::getId, relationApplicationIds)
.eq(PaymentApplication::getIsDeleted, 0)).stream()
.collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus,
(first, second) -> first));
// 来源行(预结算单号展示不过滤删除;收款汇总计算时内存过滤 isDeleted=0,与单笔版口径一致)
Map<Long, List<FormalSettlementSource>> sourcesBySettlement = sourceMapper.selectList(
Wrappers.<FormalSettlementSource>lambdaQuery()
.in(FormalSettlementSource::getFormalSettlementId, ids)).stream()
.collect(Collectors.groupingBy(FormalSettlementSource::getFormalSettlementId));
// 收款认领分摊聚合(仅应收结算单需要)
Map<Long, BigDecimal> receiptClaimPaidBySettlement = hasReceivable
? receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, ids)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0)).stream()
.collect(Collectors.groupingBy(ReceiptClaimSettlement::getFormalSettlementId,
Collectors.reducing(BigDecimal.ZERO,
item -> this.money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
: Map.of();
// 项目预付核销合计(仅应付结算单需要,一次 group-by 避免逐单查询)
Map<Long, BigDecimal> writeOffAmountsBySettlement = hasPayable
? this.projectAdvanceWriteOffService.sumBySettlementIds(
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
entities.stream().filter(item -> "payable".equals(item.getSettlementType()))
.map(FormalSettlement::getId).toList())
: Map.of();
List<FormalSettlementVO> voList = new ArrayList<>(entities.size());
for (FormalSettlement entity : entities) {
FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity);
vo.setInvoiceStatus(this.invoiceStatus(entity.getInvoiceAmount(), entity.getSettlementAmount()));
List<PaymentApplicationSettlement> relations = relationsBySettlement
.getOrDefault(entity.getId(), List.of());
// 直挂付款申请需排除该结算单关系表已引用的申请(与单笔版 notIn 语义一致)
Set<Long> ownRelationApplicationIds = relations.stream()
.map(PaymentApplicationSettlement::getPaymentApplicationId)
.filter(Objects::nonNull).collect(Collectors.toSet());
List<PaymentApplication> directApplications = applicationsBySettlement
.getOrDefault(entity.getId(), List.of()).stream()
.filter(item -> !ownRelationApplicationIds.contains(item.getId()))
.toList();
PaymentSummary summary = this.calculatePaymentSummary(entity, new PaymentSummaryData(
relations, directApplications, relationStatuses,
sourcesBySettlement.getOrDefault(entity.getId(), List.of()).stream()
.filter(item -> Objects.equals(item.getIsDeleted(), 0)).toList(),
receiptClaimPaidBySettlement.getOrDefault(entity.getId(), BigDecimal.ZERO),
writeOffAmountsBySettlement));
vo.setAppliedPaymentAmount(summary.appliedAmount());
vo.setPaidAmount(summary.paidAmount());
vo.setRemainingPayableAmount(this.money(entity.getSettlementAmount()).subtract(summary.paidAmount())
.max(BigDecimal.ZERO));
vo.setPreSettlementNos(sourcesBySettlement.getOrDefault(entity.getId(), List.of()).stream()
.map(FormalSettlementSource::getPreSettlementNo).collect(Collectors.joining(",")));
voList.add(vo);
}
return voList;
}
/** /**
* 填充已指定应付聚合态(仅付款结算单):按本页结算单一次批量查认领关系与收票登记,避免 N+1。 * 填充已指定应付聚合态(仅付款结算单):按本页结算单一次批量查认领关系与收票登记,避免 N+1。
* 聚合规则:任一指定失败 → failed;全部已指定 → assigned;其余(有认领但未指定/未推送)→ unspecified; * 聚合规则:任一指定失败 → failed;全部已指定 → assigned;其余(有认领但未指定/未推送)→ unspecified;
@@ -2517,7 +2607,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
} }
/** /**
* @param writeOffAmounts 结算单id -&gt; 项目预付核销合计(列表页批量传入避免 N+1;null 表示按需单笔查询) * @param writeOffAmounts 结算单id -&gt; 项目预付核销合计(null 表示按需单笔查询;列表页批量装配请走 toVOList)
*/ */
private FormalSettlementVO toVO(FormalSettlement entity, Map<Long, BigDecimal> writeOffAmounts) { private FormalSettlementVO toVO(FormalSettlement entity, Map<Long, BigDecimal> writeOffAmounts) {
FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity); FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity);