From f91fc19745fe3991e1afd75829b5ff321f235ada Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 23 Sep 2026 00:17:13 +0800 Subject: [PATCH] =?UTF-8?q?:zap:=20=E8=AE=A1=E8=B4=B9=E6=96=B9=E6=A1=88?= =?UTF-8?q?=E8=BF=90=E8=BE=93=E6=96=B9=E5=BC=8F=E7=B2=BE=E7=A1=AE=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E5=B9=B6=E5=AE=8C=E5=96=84=E4=BB=98=E6=AC=BE?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E5=8D=95=E8=B4=B9=E7=94=A8=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复公路/铁路子串误匹配导致错生成应收应付;尾款付款支持关联预结算单;运单新建/复制为已完成时按合同计费生成应收应付。 Co-authored-by: Cursor --- .../impl/PaymentApplicationServiceImpl.java | 78 ++++++++++++++----- .../ReceivablePayableDetailServiceImpl.java | 72 +++++++++++++---- .../service/impl/WaybillServiceImpl.java | 24 +++++- .../wrapper/PaymentApplicationWrapper.java | 2 +- 4 files changed, 140 insertions(+), 36 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index c06b24c..05cc4a3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -150,15 +150,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId()) @@ -474,13 +492,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0)) throw new ServiceException("付款比例必须在0-100之间"); - if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("申请付款金额不能小于0"); + if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("申请付款金额必须大于0"); if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null && request.getPreSettlementId() == null && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("非项目预付必须关联结算单"); if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目"); if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单"); if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null - && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("结算付款必须关联正式结算单"); + && Func.isEmpty(request.getSettlementIds()) && request.getPreSettlementId() == null) { + throw new ServiceException("尾款付款必须关联正式结算单或预结算单"); + } } private void validateInvoice(PaymentApplicationInvoiceRequest invoice) { @@ -546,12 +566,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds(); if (ids.isEmpty() && request.getSettlementId() != null) ids = List.of(request.getSettlementId()); ids = ids.stream().filter(Objects::nonNull).distinct().toList(); + if (ids.isEmpty()) return List.of(); List settlements = ids.stream().map(id -> formalSettlementMapper.selectById(id)).toList(); if (settlements.stream().anyMatch(item -> item == null || Objects.equals(item.getIsDeleted(), 1) || !APPROVED.equals(item.getApprovalStatus()) || !"payable".equals(item.getSettlementType()))) { @@ -586,7 +607,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl money(item.getSettlementAmount()).subtract(cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO)) + .map(item -> money(item.getSettlementAmount()).subtract(cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO)) .reduce(BigDecimal.ZERO, BigDecimal::add); if (money(entity.getAppliedAmount()).compareTo(payableAmount) > 0) throw new ServiceException("申请付款金额不能超过所选结算单可付款金额合计"); FormalSettlement first = settlements.get(0); @@ -601,14 +622,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); BigDecimal totalBase = settlements.stream().map(item -> money(item.getSettlementAmount()).subtract( - cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO)) + cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO)) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingPaid = money(entity.getPaidAmount()); for (int index = 0; index < settlements.size(); index++) { FormalSettlement settlement = settlements.get(index); BigDecimal base = money(settlement.getSettlementAmount()).subtract( - cumulativeAppliedAmount("settlement_payment", settlement.getId(), entity.getId())).max(BigDecimal.ZERO); + cumulativeFormalSettlementApplied(settlement.getId(), entity.getId())).max(BigDecimal.ZERO); BigDecimal applied = index == settlements.size() - 1 ? remainingApplied : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN); BigDecimal paid = index == settlements.size() - 1 ? remainingPaid @@ -663,11 +684,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl wrapper = Wrappers.lambdaQuery() - .eq("settlement_payment".equals(paymentType), PaymentApplication::getSettlementId, referenceId) - .eq("progress_advance".equals(paymentType), PaymentApplication::getPreSettlementId, referenceId) - .ne(excludeId != null, PaymentApplication::getId, excludeId); - return sumApplied(wrapper); + if ("progress_advance".equals(paymentType)) { + return cumulativePreSettlementApplied(referenceId, excludeId); + } + if ("settlement_payment".equals(paymentType)) { + return cumulativeFormalSettlementApplied(referenceId, excludeId); + } + return BigDecimal.ZERO; + } + + private BigDecimal cumulativeFormalSettlementApplied(Long settlementId, Long excludeId) { + return sumApplied(Wrappers.lambdaQuery() + .eq(PaymentApplication::getSettlementId, settlementId) + .ne(excludeId != null, PaymentApplication::getId, excludeId)); + } + + private BigDecimal cumulativePreSettlementApplied(Long preSettlementId, Long excludeId) { + // 进度预付、尾款付款关联同一预结算单时累计占用可付款额度 + return sumApplied(Wrappers.lambdaQuery() + .eq(PaymentApplication::getPreSettlementId, preSettlementId) + .ne(excludeId != null, PaymentApplication::getId, excludeId)); } private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index a761ddb..c3d4771 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -1817,22 +1817,68 @@ public class ReceivablePayableDetailServiceImpl /** * 解析合同生效计费方案。 - * 合同存在勾选默认的计费方案时仅在默认方案内匹配计算,不再回退其他方案; - * 全部方案均未勾选默认时仅使用最新添加的计费方案。 + * 仅使用与运单运输方式一致的计费方案;无匹配方案时返回 null,不生成应收应付。 + * 匹配方案中存在默认方案时取最新默认方案;否则取最新匹配方案。 */ private Map resolveDefaultBillingPlan(List> plans, String transportType) { if (plans.isEmpty()) return null; - List> defaultPlans = plans.stream().filter(this::isDefaultPlan).toList(); - if (defaultPlans.isEmpty()) return plans.get(plans.size() - 1); - Optional> matchedDefaultPlan = defaultPlans.stream() - .filter(plan -> !isBlank(plan.get("transportMode"))) - .filter(plan -> matchesCondition(plan.get("transportMode"), transportType)) - .findFirst(); - if (matchedDefaultPlan.isPresent()) return matchedDefaultPlan.get(); - return defaultPlans.stream() - .filter(plan -> isBlank(plan.get("transportMode"))) - .findFirst() - .orElseGet(() -> defaultPlans.get(defaultPlans.size() - 1)); + List> matchedPlans = plans.stream() + .filter(plan -> matchesTransportMode(plan.get("transportMode"), transportType)) + .toList(); + if (matchedPlans.isEmpty()) { + return null; + } + List> defaultPlans = matchedPlans.stream().filter(this::isDefaultPlan).toList(); + if (!defaultPlans.isEmpty()) { + return defaultPlans.get(defaultPlans.size() - 1); + } + return matchedPlans.get(matchedPlans.size() - 1); + } + + /** + * 计费方案运输方式与运单运输方式比对。 + * 统一归一化为字典键后精确比较,避免 railway 被子串 road 误匹配。 + */ + private boolean matchesTransportMode(Object expected, String actual) { + if (isBlank(expected) || Func.isBlank(actual)) { + return false; + } + if (expected instanceof List values) { + return values.stream().anyMatch(value -> matchesTransportMode(value, actual)); + } + String expectedKey = normalizeTransportModeKey(String.valueOf(expected)); + String actualKey = normalizeTransportModeKey(actual); + if (expectedKey.isEmpty() || actualKey.isEmpty()) { + return false; + } + return expectedKey.equals(actualKey); + } + + /** + * 将运输方式字典键/中文名归一化为统一键:road / railway / river / air。 + * 须先判断铁路再判断公路,避免 railway 被识别为 road。 + */ + private String normalizeTransportModeKey(String value) { + if (value == null) { + return ""; + } + String text = value.trim().toLowerCase(); + if (text.isEmpty()) { + return ""; + } + if ("railway".equals(text) || "rail".equals(text) || text.contains("铁路")) { + return "railway"; + } + if ("road".equals(text) || "gl".equals(text) || text.contains("公路") || text.contains("道路")) { + return "road"; + } + if ("river".equals(text) || "water".equals(text) || text.contains("水路") || text.contains("水运")) { + return "river"; + } + if ("air".equals(text) || "aviation".equals(text) || text.contains("航空") || text.contains("空运")) { + return "air"; + } + return text; } private BigDecimal calculateRule(Map rule, Waybill waybill) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index acff318..9594475 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -684,9 +684,15 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override @Transactional(rollbackFor = Exception.class) public boolean submit(Waybill waybill) { + boolean created = Func.isEmpty(waybill.getId()); prepareForSave(waybill); validate(waybill); - return saveOrUpdate(waybill); + boolean result = saveOrUpdate(waybill); + // 复制后确认提交为已完成时,按合同计费规则生成应收应付 + if (result && created) { + generateReceivablePayableIfCompleted(waybill); + } + return result; } @Override @@ -1000,9 +1006,25 @@ public class WaybillServiceImpl extends BaseServiceImpl } validate(target); save(target); + // 原运单已完成时,复制后按合同系统计费规则生成应收应付明细 + generateReceivablePayableIfCompleted(target); return detail(target.getId()); } + /** + * 新建/复制运单若业务状态为已完成,按合同系统计费模式生成应收、应付明细。 + *

使用同事务入口,避免 REQUIRES_NEW 读不到尚未提交的新运单。

+ */ + private void generateReceivablePayableIfCompleted(Waybill waybill) { + if (waybill == null || Func.isEmpty(waybill.getId())) { + return; + } + if (!"completed".equals(waybill.getBusinessStatus())) { + return; + } + receivablePayableDetailService.generateForImportedWaybills(List.of(waybill)); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean changeRoute(Waybill waybill) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java index 7d4f482..2e0aa1f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java @@ -38,7 +38,7 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper "项目预付"; case "progress_advance" -> "进度预付"; - case "settlement_payment" -> "结算付款"; + case "settlement_payment" -> "尾款付款"; default -> entity.getPaymentType(); }); vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {