perf: 分录拆分补齐上下文缓存消除N+1,联调详细日志
N+1 治理(复用 KingdeeSyncContext 全批档案缓存,批量同步不因拆分逻辑增加重复查询): - 合同计费方案解析按合同id缓存(BillingPlanInfo:计费单位+费用归类一次解析全批复用), 消除批量同步逐单重复解析同一合同计费方案JSON(此前计费单位/归类各解析一次) - 货物费用行按结算明细id上下文缓存(含空行占位,仅补查缺失明细),多结算单单据 (收票登记分摊多结算单)跨结算单一次预查,逐结算单解析命中缓存 - 结算合计费用项按结算单id上下文缓存,多笔收票登记分摊同一结算单不再重复查询 - KingdeeSyncContext 新增 billingPlanCache/detailFeeCache/summaryFeeCache 组件 联调详细日志: - 逐费用项归类明细(同单去重):费用项->费用大类/税率+归类来源(计费方案规则/费用项档案/ 名称特征兜底/交叉补齐);来源为名称兜底或税率未识别时升级 warn 提示补配置 - 分录生成日志增加费用项构成(运输费:100.00 + 装卸费:50.00)、货物、车号 - 货物费用行批量查询打印缓存命中/补查行数
This commit is contained in:
+17
-4
@@ -29,15 +29,24 @@ import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.CustomerReceiptAccount;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 金蝶同步上下文:批量同步时全批共享的档案缓存,消除逐单重复的远程/数据库查询(N+1)。
|
||||
* <p>单笔同步使用 {@link #empty()},各缓存为空时自动回退为逐单查询,行为与原逻辑一致。</p>
|
||||
* 金蝶同步上下文:批量同步时全批共享的档案缓存,消除逐单重复的远程/数据库查询与重复解析(N+1)。
|
||||
* <p>缓存清单与加载来源:
|
||||
* 计量单位档案(ISysClient.getMeasurementUnits)、费用项档案(ISysClient.getFeeItems)、
|
||||
* 客商编码(客商档案按乙方/付款方查询)、合同/项目/正式结算单(selectById)、
|
||||
* 结算明细(按结算单批量)、财务应付单已生成预检(A102-4 批量查询)、
|
||||
* 收款账户、合同计费方案解析({@link KingdeeApBillSyncSupport.BillingPlanInfo},按合同id解析一次)、
|
||||
* 货物费用行(按结算明细id查询,键全量占位避免空行重复查询)、结算合计费用项(按结算单id查询)。
|
||||
* 单笔同步使用 {@link #empty()},各缓存为空时首单加载后即复用。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -47,11 +56,15 @@ public record KingdeeSyncContext(Map<String, String> measureUnitCodeByName, Map<
|
||||
Map<Long, List<FormalSettlementDetail>> settlementDetailCache,
|
||||
Map<Long, FormalSettlement> settlementCache,
|
||||
Map<Long, CustomerReceiptAccount> receiptAccountCache,
|
||||
Map<String, FeeItem> feeItemCacheByName) {
|
||||
Map<String, FeeItem> feeItemCacheByName,
|
||||
Map<Long, KingdeeApBillSyncSupport.BillingPlanInfo> billingPlanCache,
|
||||
Map<Long, List<FormalSettlementDetailFee>> detailFeeCache,
|
||||
Map<Long, List<FormalSettlementSummaryFee>> summaryFeeCache) {
|
||||
|
||||
/** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
|
||||
public static KingdeeSyncContext empty() {
|
||||
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(),
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(),
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
}
|
||||
|
||||
+126
-27
@@ -69,10 +69,12 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -89,6 +91,10 @@ import java.util.stream.Collectors;
|
||||
* 物料编码按项目业务类型/业务模式/装卸大类映射(见 {@link #resolveMaterialNumber});
|
||||
* 计量单位编码按费用类型匹配合同计费方案规则计费单位,再匹配计量单位档案编码;
|
||||
* 数量运输费大类取货物运输量、其余默认 1;单价按结算金额/数量反算。</p>
|
||||
* <p>缓存与 N+1:单笔/批量同步共用 {@link KingdeeSyncContext} 全批档案缓存——合同/项目/客商/正式结算单/
|
||||
* 应付单预检/计量单位档案/费用项档案走既有缓存,计费方案解析({@link BillingPlanInfo})按合同id解析一次、
|
||||
* 货物费用行按结算明细id、结算合计费用项按结算单id缓存复用,
|
||||
* 批量同步(如收票单批量推送分摊多结算单)不因拆分逻辑增加重复查询与重复解析。</p>
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@@ -111,6 +117,14 @@ public class KingdeeApBillSyncSupport {
|
||||
private static final String FREIGHT_FEE_CATEGORY = "运输费";
|
||||
/** 分录金额勾稽允许误差(页面金额与费用项合计的分摊尾差容忍度) */
|
||||
private static final BigDecimal AMOUNT_TOLERANCE = new BigDecimal("0.01");
|
||||
/** 费用归类来源:合同计费方案规则 */
|
||||
private static final String PLAN_FEE_SOURCE = "合同计费方案规则";
|
||||
/** 费用归类来源:费用项档案 */
|
||||
private static final String ARCHIVE_FEE_SOURCE = "费用项档案";
|
||||
/** 费用归类来源:名称特征兜底(计费方案与费用项档案均未维护,告警提示补配置) */
|
||||
private static final String FALLBACK_FEE_SOURCE = "名称特征兜底";
|
||||
/** 费用归类来源:合同计费方案税率 + 费用项档案大类(交叉补齐) */
|
||||
private static final String MIXED_FEE_SOURCE = "合同计费方案税率+费用项档案大类";
|
||||
|
||||
private final IBfiService bfiService;
|
||||
private final ContractManageMapper contractManageMapper;
|
||||
@@ -436,6 +450,9 @@ public class KingdeeApBillSyncSupport {
|
||||
detailsBySettlementId.computeIfAbsent(settlementDetail.getFormalSettlementId(), key -> new ArrayList<>()).add(settlementDetail);
|
||||
}
|
||||
List<BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
// 跨结算单一次批量预查全部货物费用行入上下文缓存:逐结算单解析时命中缓存,
|
||||
// 消除多结算单单据(收票登记分摊多结算单)逐结算单重复查询的 N+1
|
||||
detailFeesByDetailId(settlementDetails, context);
|
||||
detailsBySettlementId.forEach((settlementId, details) ->
|
||||
entryGroups.addAll(this.resolveEntryGroups(requireSettlement(settlementId, context), details, context)));
|
||||
return entryGroups;
|
||||
@@ -467,16 +484,16 @@ public class KingdeeApBillSyncSupport {
|
||||
}
|
||||
ProjectApply project = requireProject(settlement.getProjectId(), context);
|
||||
ContractManage contract = requireContract(settlement.getContractId(), context);
|
||||
// 合同计费方案规则:费用类型/费用项 -> 计费单位
|
||||
Map<String, String> billingUnitByRule = billingUnitByRule(contract);
|
||||
// 合同计费方案规则:费用项 -> (费用大类, 税率),结算明细费用项拆分归类主来源
|
||||
Map<String, FeeClassInfo> feeClassByRule = feeClassByRule(contract);
|
||||
// 费用项档案:费用项名称 -> 档案(费用大类/税率兜底来源)
|
||||
// 合同计费方案解析(上下文缓存:按合同id解析一次全批复用,消除批量同步逐单重复解析 JSON 的 N+1)
|
||||
BillingPlanInfo billingPlan = billingPlanInfo(contract, context);
|
||||
Map<String, String> billingUnitByRule = billingPlan.billingUnitByRule();
|
||||
Map<String, FeeClassInfo> feeClassByRule = billingPlan.feeClassByRule();
|
||||
// 费用项档案:费用项名称 -> 档案(费用大类/税率兜底来源,上下文缓存全批复用)
|
||||
Map<String, FeeItem> feeItemArchive = feeItemArchiveByName(context);
|
||||
// 计量单位档案:单位名称 -> 计量单位编码
|
||||
// 计量单位档案:单位名称 -> 计量单位编码(上下文缓存全批复用)
|
||||
Map<String, String> unitCodeByName = measureUnitCodeByName(context);
|
||||
// 结算明细货物费用行:一车多货按货物拆分与费用项归类取数来源
|
||||
Map<Long, List<FormalSettlementDetailFee>> feesByDetailId = detailFeesByDetailId(settlementDetails);
|
||||
// 结算明细货物费用行:一车多货按货物拆分与费用项归类取数来源(上下文缓存:跨结算单/跨单据复用,仅补查缺失明细)
|
||||
Map<Long, List<FormalSettlementDetailFee>> feesByDetailId = detailFeesByDetailId(settlementDetails, context);
|
||||
List<BfiEntryGroup> entryGroups = new ArrayList<>();
|
||||
int detailEntryCount = 0;
|
||||
for (FormalSettlementDetail settlementDetail : settlementDetails) {
|
||||
@@ -484,8 +501,8 @@ public class KingdeeApBillSyncSupport {
|
||||
feesByDetailId.getOrDefault(settlementDetail.getId(), List.of()),
|
||||
billingUnitByRule, feeClassByRule, unitCodeByName, feeItemArchive);
|
||||
}
|
||||
// 结算合计只取手工添加的费用项(manualFlag=1);系统自动汇总行是结算明细金额的再汇总,再送会与明细重复
|
||||
List<FormalSettlementSummaryFee> manualSummaryFees = listSummaryFees(settlement.getId()).stream()
|
||||
// 结算合计只取手工添加的费用项(manualFlag=1);系统自动汇总行是结算明细金额的再汇总,再送会与明细重复(上下文缓存按结算单id复用)
|
||||
List<FormalSettlementSummaryFee> manualSummaryFees = listSummaryFees(settlement.getId(), context).stream()
|
||||
.filter(summaryFee -> Integer.valueOf(1).equals(summaryFee.getManualFlag())).toList();
|
||||
for (FormalSettlementSummaryFee summaryFee : manualSummaryFees) {
|
||||
appendSummaryEntry(entryGroups, settlement, project, summaryFee,
|
||||
@@ -520,6 +537,8 @@ public class KingdeeApBillSyncSupport {
|
||||
String sourceDesc = "结算明细[" + Func.toStr(detail.getDocumentNo()) + "]";
|
||||
Map<DetailSplitKey, DetailSplitGroup> groups = new LinkedHashMap<>();
|
||||
boolean multiCargo = detailFees.size() > 1;
|
||||
// 逐费用项归类明细日志去重:同一费用项同单只记一次(联调排查归类来源与缺配置项)
|
||||
Set<String> classifiedFeeItems = new HashSet<>();
|
||||
for (FormalSettlementDetailFee detailFee : detailFees) {
|
||||
Map<String, BigDecimal> feeItems = parseFeeItems(detailFee.getFeeItemsJson());
|
||||
// 页面合计口径:费用项JSON无运费项时,运费(freightAmount)并入运输费大类参与拆分
|
||||
@@ -548,6 +567,10 @@ public class KingdeeApBillSyncSupport {
|
||||
BigDecimal amount = money(item.getValue());
|
||||
FeeClassInfo classInfo = classifyFeeItem(feeItem, feeClassByRule, feeItemArchive);
|
||||
String feeType = feeCategoryOf(feeItem, classInfo);
|
||||
// 逐费用项归类明细(每个费用项同单只记一次):来源与税率缺失告警,提示补维护计费方案/费用项档案
|
||||
if (classifiedFeeItems.add(feeItem)) {
|
||||
logClassify(sourceDesc, feeItem, classInfo, feeType);
|
||||
}
|
||||
DetailSplitKey splitKey = new DetailSplitKey(cargoKey, feeType, taxRateText(classInfo.taxRate()));
|
||||
DetailSplitGroup group = groups.computeIfAbsent(splitKey, key -> new DetailSplitGroup());
|
||||
if (group.firstFeeItem == null) {
|
||||
@@ -555,6 +578,7 @@ public class KingdeeApBillSyncSupport {
|
||||
}
|
||||
group.transportGoods = firstNonBlank(detailFee.getCargoName(), group.transportGoods);
|
||||
group.amountTax = group.amountTax.add(amount);
|
||||
group.feeItemAmounts.merge(feeItem, amount, BigDecimal::add);
|
||||
if (isFreightFeeItem(feeItem)) {
|
||||
group.freightQuantity = group.freightQuantity.add(
|
||||
detailFee.getTransportQuantity() == null ? BigDecimal.ZERO : money(detailFee.getTransportQuantity()));
|
||||
@@ -604,13 +628,41 @@ public class KingdeeApBillSyncSupport {
|
||||
group.amountNoTax == null ? null : money(group.amountNoTax),
|
||||
group.transportGoods == null ? detail.getCargoName() : group.transportGoods,
|
||||
detail.getVehicleNo(), settlement.getId()));
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],费用大类[{}],税率[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
log.info("金蝶应付单分录生成:来源[{}],分录标识[{}],费用大类[{}],税率[{}],费用项构成[{}],货物[{}],车号[{}],物料编码[{}],计量单位[{}],数量[{}],单价[{}],含税金额[{}],不含税金额[{}]",
|
||||
sourceDesc, entryCode, splitKey.feeType(), Func.toStr(splitKey.taxRateText(), "未识别"),
|
||||
feeItemDetailText(group.feeItemAmounts),
|
||||
Func.toStr(group.transportGoods, detail.getCargoName()), detail.getVehicleNo(),
|
||||
materialNumber, measureunitNumber, quantity, price, amount, group.amountNoTax == null ? null : money(group.amountNoTax));
|
||||
}
|
||||
return groups.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用项归类明细日志:记录费用项 -> 费用大类/税率及归类来源;归类来源为名称特征兜底
|
||||
* (计费方案与费用项档案均未维护该费用项)或税率未识别时升级告警,提示补维护配置,
|
||||
* 避免拆分行税率缺失被并入"未识别税率"行造成对账困难
|
||||
*/
|
||||
private void logClassify(String sourceDesc, String feeItem, FeeClassInfo classInfo, String feeType) {
|
||||
String rateText = taxRateText(classInfo.taxRate());
|
||||
if (FALLBACK_FEE_SOURCE.equals(classInfo.source())) {
|
||||
log.warn("金蝶分录费用归类:{}费用项[{}]未在合同计费方案规则与费用项档案中维护,仅按名称特征归类为费用大类[{}]且税率未识别,相关费用将并入未识别税率行,请补维护计费方案或费用项档案",
|
||||
sourceDesc, feeItem, feeType);
|
||||
} else if (classInfo.taxRate() == null) {
|
||||
log.warn("金蝶分录费用归类:{}费用项[{}]归类费用大类[{}]({})但税率未识别,相关费用将并入未识别税率行,请补维护计费方案税率",
|
||||
sourceDesc, feeItem, feeType, classInfo.source());
|
||||
} else {
|
||||
log.info("金蝶分录费用归类:{}费用项[{}] -> 费用大类[{}],税率[{}%]({})",
|
||||
sourceDesc, feeItem, feeType, rateText, classInfo.source());
|
||||
}
|
||||
}
|
||||
|
||||
/** 费用项构成文本(联调排查对账用):费用项:金额 拼接,如"运输费:100.00 + 装卸费:50.00" */
|
||||
private String feeItemDetailText(Map<String, BigDecimal> feeItemAmounts) {
|
||||
return feeItemAmounts.entrySet().stream()
|
||||
.map(item -> item.getKey() + ":" + money(item.getValue()))
|
||||
.collect(Collectors.joining(" + "));
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算明细整行兜底分录(历史数据:货物费用行缺失或费用项JSON为空时使用):
|
||||
* 物料按项目业务类型/业务模式/装卸大类映射,数量取运输量(兜底 1),
|
||||
@@ -756,6 +808,19 @@ public class KingdeeApBillSyncSupport {
|
||||
return unitCodeByName.get(billingUnit.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同计费方案解析(上下文缓存):按合同id解析一次计费方案JSON,得到 计费单位映射 + 费用归类映射,
|
||||
* 批量同步全批复用,消除逐单重复解析同一合同计费方案JSON的 N+1
|
||||
*
|
||||
* @param contract 结算合同
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 计费方案解析结果(计费单位 + 费用归类)
|
||||
*/
|
||||
public BillingPlanInfo billingPlanInfo(ContractManage contract, KingdeeSyncContext context) {
|
||||
return context.billingPlanCache().computeIfAbsent(contract.getId(), contractId ->
|
||||
new BillingPlanInfo(billingUnitByRule(contract), feeClassByRule(contract)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同计费方案规则解析:费用类型/费用项 -> 计费单位(取首个配置了计费单位的规则,
|
||||
* 计费方案 JSON 解析失败按未配置处理,由计量单位解析处统一拦截报错)
|
||||
@@ -799,7 +864,7 @@ public class KingdeeApBillSyncSupport {
|
||||
if (Func.isBlank(feeItem) && Func.isBlank(feeType)) {
|
||||
continue;
|
||||
}
|
||||
FeeClassInfo classInfo = new FeeClassInfo(feeType, feeAmount(rule.get("taxRate")));
|
||||
FeeClassInfo classInfo = new FeeClassInfo(feeType, feeAmount(rule.get("taxRate")), PLAN_FEE_SOURCE);
|
||||
if (Func.isNotBlank(feeItem)) {
|
||||
mapping.putIfAbsent(feeItem, classInfo);
|
||||
}
|
||||
@@ -876,21 +941,21 @@ public class KingdeeApBillSyncSupport {
|
||||
* @param feeItem 费用项名称
|
||||
* @param feeClassByRule 合同计费方案:费用项/费用类型 -> (费用大类, 税率)
|
||||
* @param feeItemArchive 费用项档案:费用项名称 -> 档案
|
||||
* @return 费用归类(费用大类 + 税率,均可为空由调用处兜底)
|
||||
* @return 费用归类(费用大类 + 税率 + 归类来源,大类/税率可空由调用处兜底)
|
||||
*/
|
||||
private FeeClassInfo classifyFeeItem(String feeItem, Map<String, FeeClassInfo> feeClassByRule, Map<String, FeeItem> feeItemArchive) {
|
||||
FeeClassInfo classInfo = feeClassByRule.get(feeItem);
|
||||
FeeItem archive = feeItemArchive.get(feeItem);
|
||||
if (classInfo != null) {
|
||||
if (Func.isBlank(classInfo.feeType()) && archive != null) {
|
||||
return new FeeClassInfo(textValue(archive.getFeeCategory()), classInfo.taxRate());
|
||||
return new FeeClassInfo(textValue(archive.getFeeCategory()), classInfo.taxRate(), MIXED_FEE_SOURCE);
|
||||
}
|
||||
return classInfo;
|
||||
}
|
||||
if (archive != null) {
|
||||
return new FeeClassInfo(textValue(archive.getFeeCategory()), archive.getTaxRate());
|
||||
return new FeeClassInfo(textValue(archive.getFeeCategory()), archive.getTaxRate(), ARCHIVE_FEE_SOURCE);
|
||||
}
|
||||
return new FeeClassInfo(feeItem.contains("装卸") ? LOADING_FEE_TYPE : FREIGHT_FEE_CATEGORY, null);
|
||||
return new FeeClassInfo(feeItem.contains("装卸") ? LOADING_FEE_TYPE : FREIGHT_FEE_CATEGORY, null, FALLBACK_FEE_SOURCE);
|
||||
}
|
||||
|
||||
/** 费用大类归一:归类无大类时按费用项名称特征兜底(含“装卸”为装卸大类,其余归运输费大类) */
|
||||
@@ -984,21 +1049,34 @@ public class KingdeeApBillSyncSupport {
|
||||
return value == null ? null : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
/** 结算明细id -> 货物费用行(一车多货按货物拆分与费用项归类取数来源) */
|
||||
private Map<Long, List<FormalSettlementDetailFee>> detailFeesByDetailId(List<FormalSettlementDetail> settlementDetails) {
|
||||
/**
|
||||
* 结算明细id -> 货物费用行(一车多货按货物拆分与费用项归类取数来源)。
|
||||
* <p>上下文缓存按明细id全量占位:仅补查缓存缺失的明细id,已查过的明细(含查无费用行的)不再重复查询,
|
||||
* 消除批量同步、多结算单单据(如收票登记分摊多结算单)下同一明细重复查询的 N+1;
|
||||
* 返回映射保持入参明细顺序。</p>
|
||||
*/
|
||||
private Map<Long, List<FormalSettlementDetailFee>> detailFeesByDetailId(List<FormalSettlementDetail> settlementDetails, KingdeeSyncContext context) {
|
||||
List<Long> detailIds = settlementDetails.stream().map(FormalSettlementDetail::getId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (Func.isEmpty(detailIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
return formalSettlementDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)
|
||||
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId));
|
||||
Map<Long, List<FormalSettlementDetailFee>> cache = context.detailFeeCache();
|
||||
List<Long> missingIds = detailIds.stream().filter(id -> !cache.containsKey(id)).toList();
|
||||
if (Func.isNotEmpty(missingIds)) {
|
||||
Map<Long, List<FormalSettlementDetailFee>> loaded = formalSettlementDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, missingIds)
|
||||
.eq(FormalSettlementDetailFee::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo))
|
||||
.stream().collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId));
|
||||
// 查无费用行的明细也空列表占位,避免后续重复查询
|
||||
missingIds.forEach(id -> cache.put(id, loaded.getOrDefault(id, List.of())));
|
||||
log.info("货物费用行批量查询:明细{}行,缓存命中{}行,补查{}行", detailIds.size(), detailIds.size() - missingIds.size(), missingIds.size());
|
||||
}
|
||||
return detailIds.stream().collect(Collectors.toMap(id -> id, cache::get, (first, second) -> first, LinkedHashMap::new));
|
||||
}
|
||||
|
||||
/** 正式结算合计费用项(页面结算合计,保持行序) */
|
||||
/** 正式结算合计费用项(页面结算合计,保持行序);无上下文兼容入口(分录解析请用 {@link #listSummaryFees(Long, KingdeeSyncContext)}) */
|
||||
public List<FormalSettlementSummaryFee> listSummaryFees(Long settlementId) {
|
||||
return formalSettlementSummaryFeeMapper.selectList(Wrappers.<FormalSettlementSummaryFee>lambdaQuery()
|
||||
.eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId)
|
||||
@@ -1006,6 +1084,14 @@ public class KingdeeApBillSyncSupport {
|
||||
.orderByAsc(FormalSettlementSummaryFee::getLineNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 正式结算合计费用项(页面结算合计,保持行序),上下文缓存按结算单id复用:
|
||||
* 批量同步中多张单据(如多笔收票登记)分摊同一结算单时不再重复查询(N+1)
|
||||
*/
|
||||
public List<FormalSettlementSummaryFee> listSummaryFees(Long settlementId, KingdeeSyncContext context) {
|
||||
return context.summaryFeeCache().computeIfAbsent(settlementId, this::listSummaryFees);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计量单位档案:单位名称 -> 计量单位编码(批量同步时全批共享一次加载结果;
|
||||
* 应收单同步组件复用)
|
||||
@@ -1222,13 +1308,24 @@ public class KingdeeApBillSyncSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算明细费用归类:费用大类(费用类型,如 物流配送/装卸搬运)+ 税率(百分比数值)。
|
||||
* 合同计费方案规则解析为主来源,费用项档案兜底
|
||||
* 结算明细费用归类:费用大类(费用类型,如 物流配送/装卸搬运)+ 税率(百分比数值)+ 归类来源。
|
||||
* 合同计费方案规则解析为主来源,费用项档案兜底,名称特征再兜底
|
||||
*
|
||||
* @param feeType 费用大类,未识别为空由调用处按名称特征兜底
|
||||
* @param taxRate 税率(百分比数值),未识别为空
|
||||
* @param source 归类来源(合同计费方案规则/费用项档案/名称特征兜底等,联调排查用)
|
||||
*/
|
||||
private record FeeClassInfo(String feeType, BigDecimal taxRate) {
|
||||
public record FeeClassInfo(String feeType, BigDecimal taxRate, String source) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同计费方案解析结果(上下文缓存载体,按合同id解析一次全批复用):
|
||||
* 计费单位映射 + 费用归类映射,供结算明细拆分与计量单位匹配取数
|
||||
*
|
||||
* @param billingUnitByRule 费用类型/费用项 -> 计费单位
|
||||
* @param feeClassByRule 费用项/费用类型 -> (费用大类, 税率, 归类来源)
|
||||
*/
|
||||
public record BillingPlanInfo(Map<String, String> billingUnitByRule, Map<String, FeeClassInfo> feeClassByRule) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1249,6 +1346,8 @@ public class KingdeeApBillSyncSupport {
|
||||
private String transportGoods;
|
||||
/** 含税金额合计 */
|
||||
private BigDecimal amountTax = BigDecimal.ZERO;
|
||||
/** 组内费用项 -> 金额(联调对账明细日志用) */
|
||||
private final Map<String, BigDecimal> feeItemAmounts = new LinkedHashMap<>();
|
||||
/** 不含税金额分摊合计(行不含税缺失为空不送) */
|
||||
private BigDecimal amountNoTax;
|
||||
/** 运输量累计(运输费大类数量来源) */
|
||||
|
||||
Reference in New Issue
Block a user