From 6f73e3fbf990017e82eb78b0c3d278922b762cdf Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 17 Oct 2026 08:31:33 +0800 Subject: [PATCH 1/7] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=BA=94?= =?UTF-8?q?=E4=BB=98=E6=89=BF=E8=BF=90=E5=95=86=E3=80=81=E6=B1=87=E7=A5=A8?= =?UTF-8?q?=E5=8D=A0=E7=94=A8=E3=80=81=E4=B8=80=E5=8D=95=E5=A4=9A=E8=B4=A7?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E4=B8=8E=E8=BF=90=E8=BE=93=E5=AF=B9=E8=B4=A6?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../TransportReconciliationExternal.java | 3 + .../transport/mk/MkProcessBizHandlers.java | 17 +- .../impl/FormalSettlementServiceImpl.java | 112 +++++++++++ .../impl/PaymentApplicationServiceImpl.java | 188 ++++++++++++++++-- .../ReceivablePayableDetailServiceImpl.java | 129 ++++++++++-- .../TransportReconciliationServiceImpl.java | 59 +++++- 6 files changed, 465 insertions(+), 43 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java index 353febe3..7cc632d4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java @@ -6,6 +6,8 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity { private BigDecimal freightAmount; private String feeItemsJson; private BigDecimal settlementAmount; + @TableField(updateStrategy = FieldStrategy.ALWAYS) private Boolean suspectedDuplicate; private String matchStatus; private Long matchedInternalId; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java index 918749f6..68aabb25 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java @@ -403,10 +403,19 @@ public final class MkProcessBizHandlers { if (entity == null || "returned".equals(entity.getApprovalStatus())) { return entity != null; } - entity.setApprovalStatus("returned"); - entity.setCurrentNode("审核不通过"); - entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); - return service.updateById(entity); + if ("reviewing".equals(entity.getApprovalStatus())) { + PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest(); + request.setId(id); + service.returnBill(request); + } + PaymentApplication updated = service.getById(id); + if (updated == null) { + return false; + } + updated.setApprovalStatus("returned"); + updated.setCurrentNode("审核不通过"); + updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); + return service.updateById(updated); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index c68b021d..aedcfb5c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -314,6 +314,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details = detailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo)); fillDetailWaybillAddresses(details); + fillDetailCargoNames(details); vo.setDetails(details); vo.setSummaryFees(listSummaryFees(id)); vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() @@ -1663,6 +1664,117 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details) { + if (details == null || details.isEmpty()) { + return; + } + List waybillIds = details.stream().map(FormalSettlementDetail::getWaybillId) + .filter(Objects::nonNull).distinct().toList(); + Map waybillMap = waybillIds.isEmpty() ? Map.of() : waybillService.listByIds(waybillIds).stream() + .collect(Collectors.toMap(Waybill::getId, Function.identity(), (left, right) -> left)); + List detailIds = details.stream().map(FormalSettlementDetail::getId).filter(Objects::nonNull).toList(); + Map> feesByDetail = detailIds.isEmpty() ? Map.of() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds) + .orderByAsc(FormalSettlementDetailFee::getLineNo)).stream() + .collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId, LinkedHashMap::new, Collectors.toList())); + for (FormalSettlementDetail detail : details) { + Waybill waybill = waybillMap.get(detail.getWaybillId()); + LinkedHashSet goodsNames = new LinkedHashSet<>(); + LinkedHashSet goodsTypes = new LinkedHashSet<>(); + if (waybill != null) { + collectGoodsCargo(waybill.getGoodsJson(), goodsNames, goodsTypes); + } + LinkedHashSet feeNames = new LinkedHashSet<>(); + LinkedHashSet feeTypes = new LinkedHashSet<>(); + for (FormalSettlementDetailFee fee : feesByDetail.getOrDefault(detail.getId(), List.of())) { + addCargoToken(feeNames, fee.getCargoName(), true); + addCargoToken(feeTypes, fee.getCargoType(), true); + } + LinkedHashSet names = goodsNames.size() > 1 ? goodsNames : feeNames.size() > 1 ? feeNames : goodsNames; + LinkedHashSet types = goodsTypes.size() > 1 ? goodsTypes : feeTypes.size() > 1 ? feeTypes : goodsTypes; + if (names.isEmpty()) { + addCargoToken(names, detail.getCargoName(), true); + } + if (types.isEmpty()) { + addCargoToken(types, detail.getCargoType(), true); + } + if (names.size() > 1) { + detail.setCargoName(joinCargo(names)); + } + if (types.size() > 1) { + detail.setCargoType(joinCargo(types)); + } + } + } + + private void persistDetailCargoNames(Long settlementId) { + List details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId) + .orderByAsc(FormalSettlementDetail::getLineNo)); + Map cargoNames = new LinkedHashMap<>(); + Map cargoTypes = new LinkedHashMap<>(); + for (FormalSettlementDetail detail : details) { + cargoNames.put(detail.getId(), detail.getCargoName()); + cargoTypes.put(detail.getId(), detail.getCargoType()); + } + fillDetailCargoNames(details); + for (FormalSettlementDetail detail : details) { + if (Objects.equals(cargoNames.get(detail.getId()), detail.getCargoName()) + && Objects.equals(cargoTypes.get(detail.getId()), detail.getCargoType())) { + continue; + } + detailMapper.update(null, Wrappers.lambdaUpdate() + .eq(FormalSettlementDetail::getId, detail.getId()) + .set(FormalSettlementDetail::getCargoName, detail.getCargoName()) + .set(FormalSettlementDetail::getCargoType, detail.getCargoType())); + } + } + + private void collectGoodsCargo(String goodsJson, Set names, Set types) { + if (Func.isEmpty(goodsJson)) { + return; + } + try { + Object parsed = JsonUtil.parse(goodsJson, List.class); + if (!(parsed instanceof List rows)) { + return; + } + for (Object row : rows) { + if (!(row instanceof Map goods)) { + continue; + } + addCargoToken(names, goods.get("cargoName") == null ? null : String.valueOf(goods.get("cargoName")), false); + addCargoToken(types, goods.get("cargoType") == null ? null : String.valueOf(goods.get("cargoType")), false); + } + } catch (RuntimeException ignored) { + // 货物清单异常时保留明细上已有的货物名称。 + } + } + + private void addCargoToken(Set target, String raw, boolean splitJoined) { + if (Func.isEmpty(raw)) { + return; + } + String[] parts = splitJoined ? raw.split("[,,]") : new String[]{raw}; + for (String part : parts) { + String token = part.trim(); + if (token.isEmpty() || "手工调差".equals(token)) { + continue; + } + target.add(token); + } + } + + private String joinCargo(Set values) { + String joined = String.join(",", values); + return joined.length() > 500 ? joined.substring(0, 500) : joined; } private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) { 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 a0d5b94c..b3643674 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 @@ -383,6 +383,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId())); int lineNo = 1; @@ -419,7 +421,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); settlementRelationMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); removeById(entity); } + public void removeDraft(Long id) { + PaymentApplication entity = editable(id); + if (isBillPayment(entity.getPaymentMethod())) { + releaseBillBalanceIfOccupied(entity); + } + invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); + recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); + settlementRelationMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); + removeById(entity); + } @Override @Transactional(rollbackFor = Exception.class) public void submit(PaymentApplicationStatusRequest request) { @@ -434,13 +445,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl wrapper) { return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream() + .filter(item -> !releasedBillPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + /** 汇票付款被驳回后不再占用结算单可付款金额,删除草稿由逻辑删除自然释放。 */ + private boolean releasedBillPayment(PaymentApplication payment) { + return payment != null + && isBillPayment(payment.getPaymentMethod()) + && TransportBusiConstant.RETURNED.equals(payment.getApprovalStatus()); + } + private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) { if ("progress_advance".equals(paymentType)) { return cumulativePreSettlementApplied(referenceId, excludeId); @@ -2053,7 +2095,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl !releasedBillPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); return relationAmount.add(legacyAmount); } @@ -2093,7 +2137,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0) throw new ServiceException("申请付款金额超过汇票可用余额"); + BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId())); + if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额"); if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内"); } private void useBillBalance(PaymentApplication entity) { BillLedger ledger = lockedBill(entity.getBillLedgerId()); - validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true); - BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() - .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE")); - if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录"); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null); + BillLedgerUsage exists = lockedBillUsage(entity.getId()); + if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) { + throw new ServiceException("该付款申请已生成汇票使用记录"); + } BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); - ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount)); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount).setScale(2, RoundingMode.HALF_UP)); + billLedgerMapper.updateById(ledger); + if (exists == null) { + BillLedgerUsage usage = new BillLedgerUsage(); + usage.setBillLedgerId(ledger.getId()); + usage.setPaymentApplicationId(entity.getId()); + usage.setApplicationNo(entity.getPaymentNo()); + usage.setUsedAmount(amount); + usage.setUseDeptId(entity.getDeptId()); + usage.setUseDeptName(entity.getDeptName()); + usage.setUsageStatus(TransportBusiConstant.APPROVED); + usage.setStatus(1); + billLedgerUsageMapper.insert(usage); + return; + } + exists.setBillLedgerId(ledger.getId()); + exists.setApplicationNo(entity.getPaymentNo()); + exists.setUsedAmount(amount); + exists.setUseDeptId(entity.getDeptId()); + exists.setUseDeptName(entity.getDeptName()); + exists.setUsageStatus(TransportBusiConstant.APPROVED); + billLedgerUsageMapper.updateById(exists); + } + + /** + * 汇票付款申请生成后占用汇票可用余额。同一申请重复保存时只调整差额; + * 更换汇票或改为非汇票付款时,先退回原占用。 + */ + private void syncBillOccupation(PaymentApplication entity) { + BillLedgerUsage usage = lockedBillUsage(entity.getId()); + boolean occupy = isBillPayment(entity.getPaymentMethod()) && entity.getBillLedgerId() != null; + if (!occupy) { + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) { + releaseUsage(usage); + } + return; + } + BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus()) + && Objects.equals(usage.getBillLedgerId(), entity.getBillLedgerId())) { + adjustOccupiedAmount(usage, entity, amount); + return; + } + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) { + releaseUsage(usage); + } + useBillBalance(entity); + } + + private void adjustOccupiedAmount(BillLedgerUsage usage, PaymentApplication entity, BigDecimal amount) { + BigDecimal delta = amount.subtract(money(usage.getUsedAmount())).setScale(2, RoundingMode.HALF_UP); + BillLedger ledger = lockedBill(usage.getBillLedgerId()); + if (delta.compareTo(BigDecimal.ZERO) > 0 && delta.compareTo(money(ledger.getAvailableBalance())) > 0) { + throw new ServiceException("申请付款金额超过汇票可用余额"); + } + BigDecimal available = money(ledger.getAvailableBalance()).subtract(delta).setScale(2, RoundingMode.HALF_UP); + if (available.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("申请付款金额超过汇票可用余额"); + } + BigDecimal faceAmount = money(ledger.getFaceAmount()); + if (available.compareTo(faceAmount) > 0) { + available = faceAmount; + } + ledger.setAvailableBalance(available); billLedgerMapper.updateById(ledger); - BillLedgerUsage usage = new BillLedgerUsage(); - usage.setBillLedgerId(ledger.getId()); - usage.setPaymentApplicationId(entity.getId()); usage.setApplicationNo(entity.getPaymentNo()); usage.setUsedAmount(amount); usage.setUseDeptId(entity.getDeptId()); usage.setUseDeptName(entity.getDeptName()); - usage.setUsageStatus(TransportBusiConstant.APPROVED); - usage.setStatus(1); - billLedgerUsageMapper.insert(usage); + billLedgerUsageMapper.updateById(usage); + } + + private BigDecimal occupiedAmount(Long paymentApplicationId, Long billLedgerId) { + if (paymentApplicationId == null || billLedgerId == null) { + return BigDecimal.ZERO; + } + BillLedgerUsage usage = activeBillUsage(paymentApplicationId); + if (usage == null || !Objects.equals(usage.getBillLedgerId(), billLedgerId)) { + return BigDecimal.ZERO; + } + return money(usage.getUsedAmount()); + } + + private BillLedgerUsage activeBillUsage(Long paymentApplicationId) { + if (paymentApplicationId == null) { + return null; + } + return billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId) + .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED) + .last("limit 1")); + } + + private BillLedgerUsage lockedBillUsage(Long paymentApplicationId) { + if (paymentApplicationId == null) { + return null; + } + return billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId) + .last("limit 1 FOR UPDATE")); } private void releaseBillBalance(PaymentApplication entity) { @@ -2139,9 +2273,23 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()) + .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE")); + if (usage == null) { + return; + } + releaseUsage(usage); + } + + private void releaseUsage(BillLedgerUsage usage) { BillLedger ledger = lockedBill(usage.getBillLedgerId()); ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount())) - .min(money(ledger.getFaceAmount()))); + .min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP)); billLedgerMapper.updateById(ledger); usage.setUsageStatus("released"); billLedgerUsageMapper.updateById(usage); 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 e5cbabb4..d1172ab4 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 @@ -836,6 +836,12 @@ public class ReceivablePayableDetailServiceImpl if (existsByWaybill(waybill.getId(), targetSettlementType)) { continue; } + if ("payable".equals(targetSettlementType) + && trimToNull(waybill.getCarrierName()) != null + && !matchesWaybillCarrier(contract, waybill)) { + throw new ServiceException("运单【" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId())) + + "】的承运商不是所选承运商合同的乙方,不能使用项目内其他承运商合同生成应付"); + } ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), targetSettlementType); detail.setSourceType(SOURCE_MANUAL_GENERATION); save(detail); @@ -1064,11 +1070,20 @@ public class ReceivablePayableDetailServiceImpl List payableWaybills = waybills.stream() .filter(waybill -> !isSelfTransport(waybill)).toList(); if (payableWaybills.isEmpty()) return; + List carrierMatchedWaybills = payableWaybills.stream() + .filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)).toList(); + List otherCarrierWaybills = payableWaybills.stream() + .filter(waybill -> !matchesWaybillCarrier(carrierContract, waybill)).toList(); + if (!otherCarrierWaybills.isEmpty()) { + generateAutomaticWaybillDetails(otherCarrierWaybills, false, + this::resolveCarrierContract, currentUserId, generateTime); + } + if (carrierMatchedWaybills.isEmpty()) return; generateAutomaticDetail(carrierContract, "payable", - () -> existsByLoading(loadingNo, "payable"), payableWaybills, + () -> existsByLoading(loadingNo, "payable"), carrierMatchedWaybills, (fees, unitPrice) -> { normalizeWaybillFeeLines(fees); - return buildLoadingDetail(loadingNo, carrierContract, payableWaybills, fees, unitPrice); + return buildLoadingDetail(loadingNo, carrierContract, carrierMatchedWaybills, fees, unitPrice); }, false, currentUserId, generateTime); } @@ -1118,22 +1133,54 @@ public class ReceivablePayableDetailServiceImpl private ContractManage resolveWaybillCarrierContract(Waybill waybill) { if (Func.isNotEmpty(waybill.getLoadingNo())) return null; + return resolveCarrierContract(waybill); + } + + /** + * 应付只认运单选择的承运商。 + * 运单已记录承运商合同时,乙方必须就是该承运商;否则按承运商名称回查同一项目下的承运商合同。 + * 不允许落到项目里其他承运商合同的客商。 + */ + private ContractManage resolveCarrierContract(Waybill waybill) { + if (waybill == null || isSelfTransport(waybill)) { + return null; + } + String carrierName = trimToNull(waybill.getCarrierName()); if (Func.isNotEmpty(waybill.getCarrierContractId())) { - ContractManage contract = contractManageService.getById(waybill.getCarrierContractId()); - if (contract == null || !"承运商合同".equals(contract.getContractCategory())) { + ContractManage recorded = contractManageService.getById(waybill.getCarrierContractId()); + if (recorded == null || !"承运商合同".equals(recorded.getContractCategory())) { throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确"); } - return contract; + if (carrierName == null || matchesWaybillCarrier(recorded, waybill)) { + return recorded; + } } - return findCarrierContract(waybill); + ContractManage matched = findCarrierContract(waybill); + if (matched != null && matchesWaybillCarrier(matched, waybill)) { + return matched; + } + return null; + } + + private boolean matchesWaybillCarrier(ContractManage contract, Waybill waybill) { + if (contract == null || waybill == null || !"承运商合同".equals(contract.getContractCategory())) { + return false; + } + if (waybill.getProjectId() != null && contract.getProjectId() != null + && !Objects.equals(waybill.getProjectId(), contract.getProjectId())) { + return false; + } + String carrierName = trimToNull(waybill.getCarrierName()); + String partyB = trimToNull(contract.getPartyB()); + return carrierName != null && carrierName.equals(partyB); } private ContractManage findCarrierContract(Waybill waybill) { - if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) - || Func.isEmpty(waybill.getCarrierName())) { + String carrierName = trimToNull(waybill.getCarrierName()); + if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) { return null; } - // 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。 + // 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。 ContractManage customerContract = contractManageService.getById(waybill.getContractId()); if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) { return null; @@ -1142,13 +1189,38 @@ public class ReceivablePayableDetailServiceImpl .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getProjectId, waybill.getProjectId()) .eq(ContractManage::getPartyA, customerContract.getPartyA()) - .eq(ContractManage::getPartyB, waybill.getCarrierName()) + .eq(ContractManage::getPartyB, carrierName) .eq(ContractManage::getContractCategory, "承运商合同") .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) .orderByDesc(ContractManage::getCreateTime), false); } + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + /** + * 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。 + */ + private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) { + if ("payable".equals(settlementType)) { + String carrierName = trimToNull(waybill == null ? null : waybill.getCarrierName()); + if (carrierName != null) { + return carrierName; + } + return contract == null ? null : contract.getPartyB(); + } + if (contract != null && Func.isNotEmpty(contract.getPartyA())) { + return contract.getPartyA(); + } + return waybill == null ? null : waybill.getCustomerName(); + } + @Override @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @@ -1201,12 +1273,24 @@ public class ReceivablePayableDetailServiceImpl .collect(Collectors.toMap(ContractManage::getId, contract -> contract)); Map> carrierWaybills = waybills.stream().collect(Collectors.groupingBy( Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList())); + List mismatchedCarrierWaybills = waybills.stream() + .filter(waybill -> !matchesWaybillCarrier(carrierContracts.get(waybill.getCarrierContractId()), waybill)) + .toList(); + if (!mismatchedCarrierWaybills.isEmpty()) { + generateAutomaticWaybillDetails(mismatchedCarrierWaybills, false, + this::resolveCarrierContract, currentUserId, generateTime); + } for (Map.Entry> entry : carrierWaybills.entrySet()) { ContractManage carrierContract = carrierContracts.get(entry.getKey()); if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) { throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确"); } - List currentCarrierWaybills = entry.getValue(); + List currentCarrierWaybills = entry.getValue().stream() + .filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)) + .toList(); + if (currentCarrierWaybills.isEmpty()) { + continue; + } generateAutomaticDetail(carrierContract, "payable", () -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()), currentCarrierWaybills, @@ -1635,9 +1719,7 @@ public class ReceivablePayableDetailServiceImpl detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); - detail.setCustomerName(contract == null - ? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName()) - : ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA())); + detail.setCustomerName(resolveCounterpartyName(waybill, contract, settlementType)); detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractNo(contract == null ? null : contract.getContractNo()); detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); @@ -1652,8 +1734,8 @@ public class ReceivablePayableDetailServiceImpl detail.setArrivalContact(waybill.getArrivalContact()); detail.setArrivalPhone(waybill.getArrivalPhone()); detail.setTransportType(waybill.getTransportType()); - detail.setCargoName(waybill.getCargoName()); - detail.setCargoType(waybill.getCargoType()); + detail.setCargoName(joinDistinctGoods(waybill, "cargoName", waybill.getCargoName())); + detail.setCargoType(joinDistinctGoods(waybill, "cargoType", waybill.getCargoType())); detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill)); detail.setQuantityUnit(waybill.getQuantityUnit()); detail.setMileage(normalizeGeneratedMileage(waybill.getMileage())); @@ -1668,6 +1750,21 @@ public class ReceivablePayableDetailServiceImpl return detail; } + /** 一单多货时主表只保留首行货物,应付明细改记全部货物名称、货物类型。 */ + private String joinDistinctGoods(Waybill waybill, String field, String fallback) { + LinkedHashSet values = new LinkedHashSet<>(); + for (Map goods : parseList(waybill.getGoodsJson())) { + String value = stringValue(goods, field, ""); + if (Func.isNotEmpty(value)) { + values.add(value.trim()); + } + } + if (values.isEmpty()) { + return fallback; + } + return String.join(",", values); + } + /** * 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java index c22902cc..dff1f39b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -1233,14 +1233,67 @@ public class TransportReconciliationServiceImpl private List assertAllMatched(TransportReconciliation bill) { List internals = internalRows(bill.getId()); List externals = externalRows(bill.getId()); - if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size() - || internals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchResult())) - || externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) { + // 调整账单后旧的疑似重复标记可能已失效,按当前内容重算,避免已一一匹配仍被拦截 + syncSuspectedDuplicates(externals); + if (!oneToOneMatched(internals, externals)) { throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复"); } return internals; } + private void syncSuspectedDuplicates(List externals) { + Map> groups = externals.stream() + .collect(Collectors.groupingBy(this::duplicateKey)); + Set duplicateIds = new HashSet<>(); + for (List group : groups.values()) { + if (group.size() > 1) { + group.stream().map(TransportReconciliationExternal::getId).forEach(duplicateIds::add); + } + } + for (TransportReconciliationExternal external : externals) { + boolean duplicate = duplicateIds.contains(external.getId()); + String matchStatus = external.getMatchStatus(); + if (!duplicate && DUPLICATE.equals(matchStatus) && external.getMatchedInternalId() != null) { + matchStatus = MATCHED; + } + if (Boolean.TRUE.equals(external.getSuspectedDuplicate()) == duplicate + && Objects.equals(matchStatus, external.getMatchStatus())) { + continue; + } + external.setSuspectedDuplicate(duplicate); + external.setMatchStatus(matchStatus); + externalMapper.update(null, Wrappers.lambdaUpdate() + .eq(TransportReconciliationExternal::getId, external.getId()) + .set(TransportReconciliationExternal::getSuspectedDuplicate, duplicate) + .set(TransportReconciliationExternal::getMatchStatus, matchStatus)); + } + } + + private boolean oneToOneMatched(List internals, + List externals) { + if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()) { + return false; + } + if (externals.stream().anyMatch(item -> Boolean.TRUE.equals(item.getSuspectedDuplicate()))) { + return false; + } + Map externalMap = externals.stream() + .collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity(), (left, right) -> left)); + Set usedExternalIds = new HashSet<>(); + for (TransportReconciliationInternal internal : internals) { + if (!MATCHED.equals(internal.getMatchResult()) || internal.getMatchedExternalId() == null + || !usedExternalIds.add(internal.getMatchedExternalId())) { + return false; + } + TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId()); + if (external == null || !MATCHED.equals(external.getMatchStatus()) + || !Objects.equals(external.getMatchedInternalId(), internal.getId())) { + return false; + } + } + return usedExternalIds.size() == externals.size(); + } + private void refreshStats(Long id) { TransportReconciliation bill = existing(id); List internals = internalRows(id); From be0491d86a39ea57fad799a51affa8f78ef055ab Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 17 Oct 2026 11:29:01 +0800 Subject: [PATCH 2/7] =?UTF-8?q?:bug:=20=E6=9A=82=E6=97=B6=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E8=BF=90=E5=8D=95=E5=AE=8C=E6=88=90=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 历史运单 deptId 为 -1 或与当前组织不一致时,批量完成会被跳过。 Co-authored-by: Cursor --- .../springblade/transport/service/impl/WaybillServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 fe4a5160..c65d7a5a 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 @@ -1495,7 +1495,8 @@ public class WaybillServiceImpl extends BaseServiceImpl @Override @Transactional(rollbackFor = Exception.class) public boolean complete(Long id) { - return doComplete(loadEditable(id, true)); + // 暂时取消组织校验:历史运单 deptId 可能为 -1 / 与当前组织不一致,完成时不再拦截 + return doComplete(loadEditable(id, false)); } @Override From 0bcb3550792be4008271c6e7c0beeac2488db72b Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 17 Oct 2026 15:35:20 +0800 Subject: [PATCH 3/7] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8DMK=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E9=A1=B5=E5=AE=A1=E6=89=B9=E5=9B=9E=E5=86=99=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=94=AF=E6=8C=81OA=E9=83=A8=E9=97=A8=E6=8C=89?= =?UTF-8?q?=E5=85=AC=E5=8F=B8oaId=E8=BD=AE=E8=AF=A2=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../system/pojo/vo/OaOrgSyncPageVO.java | 15 ++ .../system/controller/DeptController.java | 7 +- .../system/service/IOASyncService.java | 7 +- .../service/impl/OASyncServiceImpl.java | 138 +++++++++++------- .../transport/mk/MkProcessBizHandlers.java | 24 ++- .../transport/mk/MkProcessMessageService.java | 103 +++++++++---- 6 files changed, 205 insertions(+), 89 deletions(-) diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java index bd1bd7a4..b58f8e73 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/vo/OaOrgSyncPageVO.java @@ -72,4 +72,19 @@ public class OaOrgSyncPageVO implements Serializable { @Schema(description = "全量同步结束后原始数据层级 JSON 文件路径") private String dumpFilePath; + + @Schema(description = "当前同步的公司 OA 主键(部门阶段按公司轮询时使用)") + private String companyOaId; + + @Schema(description = "当前公司序号,从 1 开始") + private Integer companyIndex; + + @Schema(description = "待同步公司总数") + private Integer companyTotal; + + @Schema(description = "当前公司的部门是否已同步完") + private Boolean companyFinished; + + @Schema(description = "下一公司 OA 主键;当前公司完成后回传,前端据此切换并重置页码") + private String nextCompanyOaId; } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java index 39d60f72..47ef9a82 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/DeptController.java @@ -204,7 +204,7 @@ public class DeptController extends BladeController { } /** - * 从OA按页同步部门(需先完成公司同步) + * 从OA按页同步部门(需先完成公司同步;按公司 oaId 轮询查询) */ @IsAdmin @PostMapping("/sync-oa-department") @@ -214,8 +214,9 @@ public class DeptController extends BladeController { @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "20") Integer size, @RequestParam(defaultValue = "false") Boolean incremental, - @RequestParam(required = false) String modifiedSince) { - return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince)); + @RequestParam(required = false) String modifiedSince, + @RequestParam(required = false) String companyOaId) { + return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince, companyOaId)); } /** diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java index 3b003897..266b9e57 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IOASyncService.java @@ -52,13 +52,14 @@ public interface IOASyncService { OaOrgSyncPageVO syncCompanyPage(int current, int size, boolean incremental, String modifiedSince); /** - * 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表 + * 按页从 OA 部门接口同步部门;须先完成公司同步,再按公司 oaId 轮询查询部门 * - * @param current 当前页,从 1 开始 + * @param current 当前页,从 1 开始(相对当前公司) * @param size 每页条数 * @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA) * @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值 + * @param companyOaId 当前公司 OA 主键;首屏可空(自动取第一家),后续回传 * @return 本页同步结果 */ - OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince); + OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 4cb3487f..72930c52 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -153,10 +153,10 @@ public class OASyncServiceImpl implements IOASyncService { @Transactional(rollbackFor = Exception.class) @Override - public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince) { + public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId) { try { ComposeLogUtil.addLog(log); - return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince); + return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince, companyOaId); } finally { ComposeLogUtil.removeLastLog(); } @@ -224,29 +224,28 @@ public class OASyncServiceImpl implements IOASyncService { } /** - * 同步部门 + * 同步部门:先整理已同步公司,再按公司 oaId 逐个轮询查询部门 * @param startTime 查询开始时间 */ private void syncDept(Date startTime) { - String subCompanyIds = getOaSubCompanyIds(false); - if (StringUtils.isEmpty(subCompanyIds)) { + List companyOaIds = listAllowedCompanyOaIds(false); + if (CollectionUtil.isEmpty(companyOaIds)) { ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数"); return; } // 未处理的数据 List notHandleList = new ArrayList<>(); - // 1. 设置查询参数 - OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds); - // 2. 分页查询并处理数据 - OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> { - ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response)); - return new ServiceException("调用OA接口查询部门信息失败"); - }, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> { - // 处理数据 - OrgSyncCount syncCount = handleDept(list, false); - notHandleList.addAll(syncCount.getNotHandledList()); - }); - // 3. 未处理的数据 + for (String companyOaId : companyOaIds) { + ComposeLogUtil.getLastLog().info("按公司轮询同步部门 companyOaId={}", companyOaId); + OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, companyOaId); + OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> { + ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}", companyOaId, JSON.toJSONString(response)); + return new ServiceException("调用OA接口查询部门信息失败"); + }, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> { + OrgSyncCount syncCount = handleDept(list, false); + notHandleList.addAll(syncCount.getNotHandledList()); + }); + } if (CollectionUtil.isNotEmpty(notHandleList)) { ComposeLogUtil.getLastLog().warn("同步部门,还有未处理的数据:{}", JSON.toJSONString(notHandleList)); } @@ -398,34 +397,55 @@ public class OASyncServiceImpl implements IOASyncService { } /** - * 按页从 OA 部门接口同步部门 + * 按页从 OA 部门接口同步部门:公司数据整理完成后,按公司 oaId 逐个轮询查询 * - * @param current 当前页 + * @param current 当前页(相对当前公司) * @param size 每页条数 * @param incremental 是否增量同步 * @param modifiedSince 增量起始时间(后续分页回传) + * @param companyOaId 当前公司 OA 主键;空则从第一家开始 * @return 本页同步结果 */ - private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince) { + private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince, + String companyOaId) { int pageNo = current < 1 ? 1 : current; int pageSize = size < 1 ? 20 : Math.min(size, 200); - String subCompanyIds = getOaSubCompanyIds(true); - if (StringUtils.isEmpty(subCompanyIds)) { + // 先整理已同步企业列表,再按企业轮询 + List companyOaIds = listAllowedCompanyOaIds(true); + if (CollectionUtil.isEmpty(companyOaIds)) { ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步"); OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty()); emptyPageVO.setFinished(true); + emptyPageVO.setCompanyFinished(true); + emptyPageVO.setCompanyIndex(0); + emptyPageVO.setCompanyTotal(0); if (!incremental) { emptyPageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump()); } return emptyPageVO; } + int companyIndex = 0; + String currentCompanyOaId = companyOaIds.get(0); + if (StringUtils.isNotBlank(companyOaId)) { + int foundIndex = companyOaIds.indexOf(companyOaId.trim()); + if (foundIndex >= 0) { + companyIndex = foundIndex; + currentCompanyOaId = companyOaIds.get(foundIndex); + } else { + ComposeLogUtil.getLastLog().warn("回传公司oaId不在待同步列表中,回退第一家 companyOaId={}", companyOaId); + } + } IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT); - OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), subCompanyIds); + // 查询部门时只传当前公司的 oa_id + OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), currentCompanyOaId); departmentSearch.setCurPage(pageNo); departmentSearch.setPageSize(pageSize); + ComposeLogUtil.getLastLog().info("按公司查询OA部门 companyOaId={} companyIndex={}/{} page={}/{}", + currentCompanyOaId, companyIndex + 1, companyOaIds.size(), pageNo, pageSize); OAResponse oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch)); if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) { - ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse)); + ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}", + currentCompanyOaId, JSON.toJSONString(oaResponse)); throw new ServiceException("调用OA接口查询部门信息失败"); } OAResponseData responseData = oaResponse.getData(); @@ -437,25 +457,44 @@ public class OASyncServiceImpl implements IOASyncService { } OrgSyncCount syncCount = handleDept(oaDepartments, true); if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) { - ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList())); + ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据 companyOaId={}:{}", + currentCompanyOaId, JSON.toJSONString(syncCount.getNotHandledList())); } OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount); pageVO.setModifiedSince(incrementalStart.getModifiedSince()); - if (Boolean.TRUE.equals(pageVO.getFinished())) { - // 部门分批同步完成后,清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表 - this.removeOutsideWhitelistOrgs(); - deptService.rebuildOaHierarchy(); - deptService.removeOaOrphansWithoutParent(); - deptService.updateAncestors(null); - if (!incremental) { - pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump()); + pageVO.setCompanyOaId(currentCompanyOaId); + pageVO.setCompanyIndex(companyIndex + 1); + pageVO.setCompanyTotal(companyOaIds.size()); + boolean companyPageFinished = Boolean.TRUE.equals(pageVO.getFinished()); + if (companyPageFinished) { + if (companyIndex + 1 < companyOaIds.size()) { + // 当前公司部门已拉完,进入下一公司(整体未结束,不重建层级) + pageVO.setCompanyFinished(true); + pageVO.setNextCompanyOaId(companyOaIds.get(companyIndex + 1)); + pageVO.setFinished(false); + } else { + // 全部公司轮询完成:清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表 + pageVO.setCompanyFinished(true); + pageVO.setNextCompanyOaId(null); + pageVO.setFinished(true); + this.removeOutsideWhitelistOrgs(); + deptService.rebuildOaHierarchy(); + deptService.removeOaOrphansWithoutParent(); + deptService.updateAncestors(null); + if (!incremental) { + pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump()); + } } + } else { + pageVO.setCompanyFinished(false); + pageVO.setNextCompanyOaId(currentCompanyOaId); + pageVO.setFinished(false); } CacheUtil.clear(SYS_CACHE); CacheUtil.clear(SYS_CACHE, Boolean.FALSE); - ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} {}/{},成功{},跳过{}", - incremental ? "incremental" : "full", pageNo, totalSize, - syncCount.getSyncedCount(), syncCount.getSkippedCount()); + ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} companyOaId={} company={}/{} page={}/{},成功{},跳过{}", + incremental ? "incremental" : "full", currentCompanyOaId, companyIndex + 1, companyOaIds.size(), + pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount()); return pageVO; } @@ -604,15 +643,15 @@ public class OASyncServiceImpl implements IOASyncService { /** * 组装 OA 部门分页查询参数 * - * @param startTime 增量查询开始时间 - * @param subCompanyIds 子公司 id 列表 + * @param startTime 增量查询开始时间 + * @param companyOaId 当前公司 OA 主键(单次只传一家公司) * @return 查询参数 */ - private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) { + private OADepartmentSearch buildDepartmentSearch(Date startTime, String companyOaId) { OADepartmentSearch departmentSearch = new OADepartmentSearch(); departmentSearch.setCurPage(1); departmentSearch.setPageSize(20); - departmentSearch.setSubcompanyid1(subCompanyIds); + departmentSearch.setSubcompanyid1(companyOaId); if (startTime != null) { departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME)); } @@ -1145,12 +1184,12 @@ public class OASyncServiceImpl implements IOASyncService { } /** - * 获取 OA 部门查询使用的子公司 id + * 整理部门同步用的公司 OA 主键列表(公司阶段完成后调用) * * @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司 - * @return 逗号分隔的 OA 公司 id + * @return 白名单内公司 oaId 列表(去重、保序) */ - private String getOaSubCompanyIds(boolean syncAll) { + private List listAllowedCompanyOaIds(boolean syncAll) { List companyList; if (syncAll) { companyList = deptService.list(Wrappers.lambdaQuery() @@ -1161,7 +1200,7 @@ public class OASyncServiceImpl implements IOASyncService { } else { Set companyNames = this.getCompanyNames(); if (companyNames.isEmpty()) { - return null; + return Collections.emptyList(); } companyList = deptService.list(Wrappers.lambdaQuery() .in(Dept::getDeptName, companyNames) @@ -1170,20 +1209,17 @@ public class OASyncServiceImpl implements IOASyncService { .ne(Dept::getOaId, "") ); } - Map companyByOaId = new HashMap<>(); + Map companyByOaId = new LinkedHashMap<>(); for (Dept company : companyList) { String oaId = this.resolveOaCompanyId(company); if (StringUtils.isNotBlank(oaId)) { - companyByOaId.put(oaId, company); + companyByOaId.putIfAbsent(oaId, company); } } Set allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId); - return companyList.stream() - .map(this::resolveOaCompanyId) - .filter(StringUtils::isNotBlank) + return companyByOaId.keySet().stream() .filter(allowedCompanyOaIds::contains) - .distinct() - .collect(Collectors.joining(",")); + .collect(Collectors.toList()); } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java index 68aabb25..b96c8469 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java @@ -1,6 +1,7 @@ package org.springblade.transport.mk; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springblade.core.tenant.annotation.TenantIgnore; import org.springblade.core.tool.utils.Func; import org.springblade.transport.pojo.dto.BillPaymentStatusRequest; @@ -270,6 +271,7 @@ public final class MkProcessBizHandlers { } } + @Slf4j @Component @TenantIgnore @RequiredArgsConstructor @@ -290,6 +292,12 @@ public final class MkProcessBizHandlers { public boolean syncFromProcessInfo(Long id, Object processInfo) { FormalSettlement entity = service.getById(id); if (entity == null) { + log.warn("MK同步当前节点失败,正式结算单不存在 id={}", id); + return false; + } + // 已终态单据禁止再写回审批中,避免流程结束后的节点刷新把状态打回 reviewing + if ("approved".equals(entity.getApprovalStatus()) || "returned".equals(entity.getApprovalStatus()) + || "voided".equals(entity.getApprovalStatus())) { return false; } String node = MkProcessNodeHelper.currentNode(processInfo); @@ -310,8 +318,12 @@ public final class MkProcessBizHandlers { @Override public boolean approveFromProcess(Long id, String processorName) { FormalSettlement entity = service.getById(id); - if (entity == null || "approved".equals(entity.getApprovalStatus())) { - return entity != null; + if (entity == null) { + log.error("MK审批通过同步失败,正式结算单不存在 id={}", id); + return false; + } + if ("approved".equals(entity.getApprovalStatus())) { + return true; } // 统一走 service.approve:与手动审批同一入口(补变更记录),审批事务提交后自动触发金蝶推送事件; // 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送 @@ -330,8 +342,12 @@ public final class MkProcessBizHandlers { @Override public boolean rejectFromProcess(Long id, String processorName) { FormalSettlement entity = service.getById(id); - if (entity == null || "returned".equals(entity.getApprovalStatus())) { - return entity != null; + if (entity == null) { + log.error("MK审批驳回同步失败,正式结算单不存在 id={}", id); + return false; + } + if ("returned".equals(entity.getApprovalStatus())) { + return true; } entity.setApprovalStatus("returned"); entity.setCurrentNode("审核不通过"); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessMessageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessMessageService.java index 2e23f078..debb8852 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessMessageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessMessageService.java @@ -4,10 +4,12 @@ import com.alibaba.fastjson2.JSON; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tenant.TenantUtil; import org.springblade.core.tenant.annotation.TenantIgnore; import org.springblade.core.tool.api.FR; import org.springblade.core.tool.utils.StringUtil; import org.springblade.process.feign.IBusinessProcessClient; +import org.springblade.process.pojo.vo.MkFormBindVO; import org.springframework.stereotype.Service; import java.util.Collections; @@ -69,9 +71,9 @@ public class MkProcessMessageService { log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType); return; } - String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId"); + Long bizId = resolveBizId(asMap(body == null ? null : body.get("formData"))); queryCurrentNodes(bizType, processId, loginName); - scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues); + scheduleProcessInfo(handler, bizType, processId, loginName, bizId, formValues); } private void queryCurrentNodes(String bizType, String processId, String loginName) { @@ -86,36 +88,35 @@ public class MkProcessMessageService { } private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId, - String loginName, String bizIdText, Map formValues) { + String loginName, Long bizId, Map formValues) { Map formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues); - log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId); + log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={} bizId={}", + PROCESS_INFO_DELAY_SECONDS, bizType, processId, bizId); + // 延迟任务跑在独立线程,请求态 @TenantIgnore 已丢失,必须显式 ignore,否则 getById 可能因租户条件查空 scheduler.schedule( - () -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy), + () -> TenantUtil.ignore(() -> queryProcessInfo(handler, bizType, processId, loginName, bizId, formValuesCopy)), PROCESS_INFO_DELAY_SECONDS, TimeUnit.SECONDS ); } private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId, - String loginName, String bizIdText, Map formValues) { + String loginName, Long bizId, Map formValues) { try { FR processInfoResult = businessProcessClient.getProcessInfo(processId, loginName); Object processInfo = processInfoResult == null ? null : processInfoResult.getData(); - log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}", - bizType, processId, loginName, JSON.toJSONString(processInfo)); + log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} bizId={} result={}", + bizType, processId, loginName, bizId, JSON.toJSONString(processInfo)); if (isProcessFinished(processInfo)) { - applyProcessResult(handler, bizType, bizIdText, processId, formValues, true); + applyProcessResult(handler, bizType, bizId, processId, formValues, true); } else if (MkProcessNodeHelper.isProcessRejected(processInfo)) { - applyProcessResult(handler, bizType, bizIdText, processId, formValues, false); - } else if (StringUtil.isNotBlank(bizIdText)) { - try { - handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo); - } catch (NumberFormatException e) { - log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText); - } + applyProcessResult(handler, bizType, bizId, processId, formValues, false); + } else if (bizId != null) { + handler.syncFromProcessInfo(bizId, processInfo); } } catch (Exception e) { - log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e); + log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={} bizId={}", + bizType, processId, loginName, bizId, e); } } @@ -123,21 +124,24 @@ public class MkProcessMessageService { return "30".equals(firstText(asMap(processInfo), "fdProcessStatus")); } - private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText, + private void applyProcessResult(IMkProcessBizHandler handler, String bizType, Long bizId, String processId, Map formValues, boolean approved) { String action = approved ? "审核通过" : "审核驳回"; - if (StringUtil.isBlank(bizIdText)) { - log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId); + if (bizId == null) { + log.warn("流程{}但未解析到业务主键,跳过同步 bizType={} processId={}", action, bizType, processId); return; } try { String processorName = firstText(formValues, "mkUserName", "mkLoginName"); - Long bizId = Long.valueOf(bizIdText); - if (approved) { - handler.approveFromProcess(bizId, processorName); - } else { - handler.rejectFromProcess(bizId, processorName); + boolean synced = approved + ? handler.approveFromProcess(bizId, processorName) + : handler.rejectFromProcess(bizId, processorName); + if (!synced) { + log.error("流程{}同步业务状态失败(业务单据不存在或未更新) bizType={} bizId={} processId={}", + action, bizType, bizId, processId); + return; } + log.info("流程{}同步业务状态成功 bizType={} bizId={} processId={}", action, bizType, bizId, processId); try { FR markResult = businessProcessClient.markProcessFinished(processId, approved); log.info("公开页标记业务流程完结 bizType={} processId={} approved={} result={}", @@ -146,13 +150,56 @@ public class MkProcessMessageService { log.error("公开页标记业务流程完结失败 bizType={} processId={} approved={}", bizType, processId, approved, markException); } - } catch (NumberFormatException e) { - log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText); } catch (Exception e) { - log.error("流程{}同步业务状态失败 bizType={} id={} processId={}", action, bizType, bizIdText, processId, e); + log.error("流程{}同步业务状态失败 bizType={} bizId={} processId={}", action, bizType, bizId, processId, e); } } + /** + * 公开页 formData 里的 id / formInstanceId 可能是表单实例 id(与业务主键不同), + * 须先按 MkFormBind 反查业务主键,与 detail 接口保持一致。 + */ + private Long resolveBizId(Map formData) { + String formInstanceIdText = firstText(formData, "formInstanceId"); + String idText = firstText(formData, "id"); + Long resolved = resolveByFormInstanceId(formInstanceIdText); + if (resolved != null) { + return resolved; + } + resolved = resolveByFormInstanceId(idText); + if (resolved != null) { + return resolved; + } + if (StringUtil.isNotBlank(idText)) { + try { + return Long.valueOf(idText); + } catch (NumberFormatException e) { + log.warn("公开页流程消息业务id格式不正确 id={}", idText); + } + } + return null; + } + + private Long resolveByFormInstanceId(String formInstanceIdText) { + if (StringUtil.isBlank(formInstanceIdText)) { + return null; + } + try { + Long formInstanceId = Long.valueOf(formInstanceIdText); + FR result = businessProcessClient.resolveByFormInstanceId(formInstanceId); + if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) { + Long bizId = result.getData().getBizId(); + log.info("公开页按表单实例id解析业务主键 formInstanceId={} bizId={}", formInstanceId, bizId); + return bizId; + } + } catch (NumberFormatException e) { + log.warn("公开页表单实例id格式不正确 formInstanceId={}", formInstanceIdText); + } catch (Exception e) { + log.warn("公开页按表单实例id解析业务主键失败 formInstanceId={}", formInstanceIdText, e); + } + return null; + } + private static Map asMap(Object value) { if (!(value instanceof Map map)) { return Collections.emptyMap(); From 3ff90eb19d471ec2dd13d8efb80ddc963b46db54 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 17 Oct 2026 16:22:37 +0800 Subject: [PATCH 4/7] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8DOA=E5=85=AC?= =?UTF-8?q?=E5=8F=B8=E4=B8=8E=E9=83=A8=E9=97=A8=E4=B8=BB=E9=94=AE=E6=92=9E?= =?UTF-8?q?=E5=8F=B7=E5=AF=BC=E8=87=B4=E4=B8=8B=E7=BA=A7=E9=83=A8=E9=97=A8?= =?UTF-8?q?=E6=8C=82=E9=94=99=E7=88=B6=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../system/service/impl/DeptServiceImpl.java | 81 ++++++++++++++----- .../service/impl/OASyncServiceImpl.java | 28 ++++++- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java index d55f05e6..036e161b 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/DeptServiceImpl.java @@ -1039,7 +1039,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID boolean isCompany = this.isOaCompany(dept); boolean parentExists = isCompany ? companyByOaId.containsKey(parentOaId) - : departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId); + : this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId) != null; // 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留 if (!parentExists) { deleteIds.add(dept.getId()); @@ -1063,7 +1063,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID } Dept parent = this.isOaCompany(dept) ? companyByOaId.get(parentOaId) - : Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId)); + : this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId); if (parent != null && deleteIds.contains(parent.getId())) { deleteIds.add(dept.getId()); oaCascadeChanged = true; @@ -1175,7 +1175,10 @@ public class DeptServiceImpl extends ServiceImpl implements ID } /** - * 根据 OA 上级主键解析本系统父级 + * 根据 OA 上级主键解析本系统父级。 + *

+ * OA 公司与部门主键同属数字空间(如公司 id=19「运输事业部」与部门 id=19「投资发展部」), + * 解析部门上级时必须按「同属公司」消歧,禁止跨公司误挂。 */ private OaParentLink resolveOaParentLink(Dept dept, Map companyByOaId, Map departmentByOaId) { String parentOaId = dept.getOaSupSubComId(); @@ -1202,21 +1205,7 @@ public class DeptServiceImpl extends ServiceImpl implements ID log.warn("部门缺少上级OA主键,跳过层级重建 oaId={} deptId={}", dept.getOaId(), dept.getId()); return null; } - boolean parentIsDepartment = StringUtil.isNotBlank(dept.getParentCode()) - && dept.getParentCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX); - boolean parentIsCompany = StringUtil.isNotBlank(dept.getParentCode()) - && dept.getParentCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX); - Dept parent; - if (parentIsDepartment) { - parent = departmentByOaId.get(parentOaId); - } else if (parentIsCompany) { - parent = companyByOaId.get(parentOaId); - } else { - parent = departmentByOaId.get(parentOaId); - if (parent == null) { - parent = companyByOaId.get(parentOaId); - } - } + Dept parent = this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId); if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) { log.warn("未找到上级组织,跳过层级重建 oaId={} oaSupSubComId={} deptId={}", dept.getOaId(), parentOaId, dept.getId()); @@ -1227,6 +1216,62 @@ public class DeptServiceImpl extends ServiceImpl implements ID return new OaParentLink(parent.getId(), this.resolveParentCode(parent, prefix, parentOaId)); } + /** + * 解析部门的 OA 上级:优先同公司下的上级部门,其次所属/同号公司;避免公司与部门 oaId 撞号误挂。 + */ + private Dept resolveOaDepartmentParent(Dept dept, String parentOaId, + Map companyByOaId, Map departmentByOaId) { + Dept belongCompany = this.findBelongCompany(dept, companyByOaId); + // 根部门:oaSupSubComId 存的是所属公司 OA 主键 + if (belongCompany != null && StringUtil.equals(parentOaId, belongCompany.getOaId())) { + return belongCompany; + } + Dept parentDept = departmentByOaId.get(parentOaId); + if (parentDept != null && this.isSameOaCompanyTree(dept, parentDept, companyByOaId)) { + return parentDept; + } + return companyByOaId.get(parentOaId); + } + + /** + * 按所属公司编码或 OA 主键定位本地公司 + */ + private Dept findBelongCompany(Dept dept, Map companyByOaId) { + if (dept == null || companyByOaId == null || companyByOaId.isEmpty()) { + return null; + } + if (StringUtil.isNotBlank(dept.getBelongCompanyCode())) { + for (Dept company : companyByOaId.values()) { + if (company != null && StringUtil.equals(company.getDeptCode(), dept.getBelongCompanyCode())) { + return company; + } + } + Dept byOaId = companyByOaId.get(dept.getBelongCompanyCode()); + if (byOaId != null) { + return byOaId; + } + } + return null; + } + + /** + * 判断两个组织是否同属一家 OA 公司(用于部门 oaId 与公司 oaId 撞号消歧) + */ + private boolean isSameOaCompanyTree(Dept left, Dept right, Map companyByOaId) { + if (left == null || right == null) { + return false; + } + if (StringUtil.isNotBlank(left.getBelongCompanyCode()) && StringUtil.isNotBlank(right.getBelongCompanyCode())) { + return StringUtil.equals(left.getBelongCompanyCode(), right.getBelongCompanyCode()); + } + Dept leftCompany = this.findBelongCompany(left, companyByOaId); + Dept rightCompany = this.isOaCompany(right) ? right : this.findBelongCompany(right, companyByOaId); + if (leftCompany == null || rightCompany == null || leftCompany.getId() == null || rightCompany.getId() == null) { + return false; + } + return Objects.equals(leftCompany.getId(), rightCompany.getId()); + } + private String resolveParentCode(Dept parent, String prefix, String parentOaId) { if (StringUtil.isNotBlank(parent.getDeptCode())) { return parent.getDeptCode(); diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java index 72930c52..6f3143a4 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/OASyncServiceImpl.java @@ -468,10 +468,11 @@ public class OASyncServiceImpl implements IOASyncService { boolean companyPageFinished = Boolean.TRUE.equals(pageVO.getFinished()); if (companyPageFinished) { if (companyIndex + 1 < companyOaIds.size()) { - // 当前公司部门已拉完,进入下一公司(整体未结束,不重建层级) + // 当前公司部门已拉完:先按 OA 主键校正父子挂载,再进入下一公司 pageVO.setCompanyFinished(true); pageVO.setNextCompanyOaId(companyOaIds.get(companyIndex + 1)); pageVO.setFinished(false); + deptService.rebuildOaHierarchy(); } else { // 全部公司轮询完成:清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表 pageVO.setCompanyFinished(true); @@ -805,9 +806,11 @@ public class OASyncServiceImpl implements IOASyncService { } String parentOaId = dept.getOaSupSubComId(); Dept belongCompany = companyByOaId.values().stream() - .filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())) + .filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()) + || StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode())) .findFirst() .orElse(null); + // 根部门:oaSupSubComId 即所属公司 OA 主键,必须挂公司(OA 公司/部门主键会撞号) if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId) || StringUtils.equals(parentOaId, belongCompany.getOaId()))) { dept.setParentCode(belongCompany.getDeptCode()); @@ -818,9 +821,19 @@ public class OASyncServiceImpl implements IOASyncService { if (parentDept == null) { parentDept = existingByOaId.get(parentOaId); } + // 跨公司同号部门不可作为上级(例如总部部门 id=19 与公司 id=19) + if (parentDept != null && StringUtils.isNotBlank(dept.getBelongCompanyCode()) + && StringUtils.isNotBlank(parentDept.getBelongCompanyCode()) + && !StringUtils.equals(dept.getBelongCompanyCode(), parentDept.getBelongCompanyCode())) { + parentDept = null; + } + Dept parentCompany = StringUtils.isNotBlank(parentOaId) ? companyByOaId.get(parentOaId) : null; if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) { dept.setParentCode(parentDept.getDeptCode()); dept.setParentId(parentDept.getId()); + } else if (parentCompany != null && StringUtils.isNotBlank(parentCompany.getDeptCode())) { + dept.setParentCode(parentCompany.getDeptCode()); + dept.setParentId(parentCompany.getId()); } else if (belongCompany != null) { // 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建 dept.setParentCode(belongCompany.getDeptCode()); @@ -1034,7 +1047,7 @@ public class OASyncServiceImpl implements IOASyncService { /** * oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。 - * 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。 + * 组织编码已按新规则重算、或父子挂载与库中不一致时,即使 modified 相同也要更新。 */ private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) { if (existing == null) { @@ -1044,6 +1057,15 @@ public class OASyncServiceImpl implements IOASyncService { && !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) { return false; } + if (incoming != null && incoming.getParentId() != null + && !Objects.equals(existing.getParentId(), incoming.getParentId())) { + return false; + } + if (incoming != null && StringUtils.isNotBlank(incoming.getParentCode()) + && !StringUtils.equals(StringUtils.trimToEmpty(existing.getParentCode()), + StringUtils.trimToEmpty(incoming.getParentCode()))) { + return false; + } String localModified = StringUtils.trimToEmpty(existing.getOaModified()); String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified()); if (localModified.isEmpty() || remoteModified.isEmpty()) { From e98feab2d9f285e415221e913b25a56a3cab983e Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 17 Oct 2026 17:01:26 +0800 Subject: [PATCH 5/7] =?UTF-8?q?:sparkles:=20=E9=A1=B9=E7=9B=AE=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E6=94=AF=E6=8C=81=E4=BF=9D=E5=AD=98=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=83=A8=E9=97=A8=E4=B8=8E=E5=B9=B3=E5=8F=B0=E5=85=AC=E5=8F=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../transport/service/impl/ProjectApplyServiceImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 64526a01..66e154f2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -260,6 +260,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl beforeData = buildProjectChangeSnapshot(oldRecord); prepare(projectApply); + normalizeUndertakeDept(projectApply); copyChangeFields(oldRecord, projectApply); validateSubmit(oldRecord); validateChange(oldRecord, projectApply.getChangeType()); @@ -559,6 +561,11 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl Date: Sat, 17 Oct 2026 17:03:25 +0800 Subject: [PATCH 6/7] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E5=AE=A1=E6=89=B9=E9=80=9A=E8=BF=87=E5=90=8E=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E5=86=8D=E6=AC=A1=E5=8F=91=E8=B5=B7=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../service/impl/ProjectApplyServiceImpl.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 66e154f2..40ddbb6b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -82,6 +82,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl wrapper .nested(formal -> formal.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL) - .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved")) + .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, STATUS_CHANGE_APPROVED)) .or(temporary -> temporary.eq(ProjectApply::getEffectiveType, EFFECTIVE_TEMPORARY) .ne(ProjectApply::getApprovalStatus, STATUS_VOIDED))); } @@ -882,7 +883,9 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl Date: Sun, 18 Oct 2026 05:12:27 +0800 Subject: [PATCH 7/7] =?UTF-8?q?:sparkles:=20=E7=BB=93=E7=AE=97=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=E4=B8=8E=E6=B1=87=E7=A5=A8=E6=A0=A1=E9=AA=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E5=AE=8C=E5=96=84=E5=8F=98=E6=9B=B4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E8=8F=9C=E5=8D=95=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 付款可付款占用、汇票仅可选未到期收票、正式结算计费方式与金蝶同步、导入运单删除校验,并补充结算菜单与变更明细 SQL。 Co-authored-by: Cursor --- .../dto/PreSettlementDetailAdjustRequest.java | 2 +- .../entity/FormalSettlementChangeRecord.java | 4 + .../pojo/entity/FormalSettlementDetail.java | 8 + .../entity/FormalSettlementDetailFee.java | 8 + .../PaymentApplicationReferenceAmountVO.java | 2 + .../service/impl/BillLedgerServiceImpl.java | 7 +- .../impl/FormalSettlementServiceImpl.java | 183 +++++++++++++++- .../impl/PaymentApplicationServiceImpl.java | 159 +++++++++++--- .../impl/PreSettlementServiceImpl.java | 205 ++++++++++++++++-- .../impl/WaybillImportBatchServiceImpl.java | 12 +- .../blade_formal_settlement_20260818.sql | 42 ++-- ...rmal_settlement_change_record_20260827.sql | 2 + ...ttlement_change_record_detail_20261018.sql | 30 +++ .../blade_pre_settlement_20260818.sql | 2 +- .../blade_settlement_menu_split_20261018.sql | 163 ++++++++++++++ 15 files changed, 745 insertions(+), 84 deletions(-) create mode 100644 doc/sql/transport/blade_formal_settlement_change_record_detail_20261018.sql create mode 100644 doc/sql/transport/blade_settlement_menu_split_20261018.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java index bbfddec7..8d2383a9 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java @@ -33,7 +33,7 @@ public class PreSettlementDetailAdjustRequest implements Serializable { @Schema(description = "预结算明细ID") private Long detailId; - @Schema(description = "调整原因") + @Schema(description = "备注(写入结算明细备注,非必填)") private String changeReason; @Schema(description = "明细费用行") diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java index b3834bc4..8d95679f 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java @@ -25,6 +25,10 @@ public class FormalSettlementChangeRecord extends TenantEntity { private Integer lineNo; private String operationType; private String changeContent; + /** 变更前数据JSON */ + private String beforeData; + /** 变更后数据JSON */ + private String afterData; private String changeReason; private String operatorName; private LocalDateTime changeTime; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java index 899dffe1..96f273cd 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java @@ -8,7 +8,9 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.core.tenant.mp.TenantEntity; @@ -123,6 +125,12 @@ public class FormalSettlementDetail extends TenantEntity { * 批次号 */ private String batchNo; + /** + * 计费方式(来源应收应付货物费用计费类型,非本表字段) + */ + @TableField(exist = false) + @Schema(description = "计费方式") + private String billingType; /** * 运输单价 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java index c4e62e26..0dcae4f5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java @@ -8,7 +8,9 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.core.tenant.mp.TenantEntity; @@ -70,6 +72,12 @@ public class FormalSettlementDetailFee extends TenantEntity { * 费用项JSON */ private String feeItemsJson; + /** + * 命中计费规则JSON(来源费用带回,非本表字段) + */ + @TableField(exist = false) + @Schema(description = "命中计费规则JSON") + private String billingRulesJson; /** * 原金额 */ diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java index 367ae478..d750370b 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java @@ -27,4 +27,6 @@ public class PaymentApplicationReferenceAmountVO implements Serializable { @Schema(description = "结算金额") private BigDecimal settlementAmount; @Schema(description = "累计申请付款金额") private BigDecimal cumulativeAppliedAmount; @Schema(description = "可付款金额") private BigDecimal payableAmount; + /** 该结算单当前为第几次付款(含本次,已作废不计) */ + @Schema(description = "付款次序") private Integer paymentSequence; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java index 0e08c8a2..a2728da7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/BillLedgerServiceImpl.java @@ -104,12 +104,17 @@ public class BillLedgerServiceImpl extends BaseServiceImpl availableList(String keyword, Long deptId, Long selectedId) { + LocalDate today = LocalDate.now(); + // 付款申请仅可选:收票类型、未到期、有可用余额;已选中的汇票始终带回以便回显 return list(Wrappers.lambdaQuery() .and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword) .or().like(BillLedger::getIssuerName, keyword) .or().like(BillLedger::getReceiverName, keyword)) .and(wrapper -> wrapper - .gt(BillLedger::getAvailableBalance, BigDecimal.ZERO) + .and(available -> available + .eq(BillLedger::getBillType, "received") + .ge(BillLedger::getMaturityDate, today) + .gt(BillLedger::getAvailableBalance, BigDecimal.ZERO)) .or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId))) .orderByAsc(BillLedger::getMaturityDate) .orderByDesc(BillLedger::getCreateTime)).stream() diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index aedcfb5c..cbfdb8e0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -315,6 +315,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() @@ -352,6 +353,36 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details) { + if (details == null || details.isEmpty()) { + return; + } + List sourceDetailIds = details.stream() + .map(FormalSettlementDetail::getSourceDetailId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (sourceDetailIds.isEmpty()) { + return; + } + Map billingTypeMap = receivablePayableCargoFeeMapper.selectList( + Wrappers.lambdaQuery() + .in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)) + .stream() + .filter(fee -> Func.isNotEmpty(fee.getBillingType())) + .collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId, + Collectors.mapping(ReceivablePayableCargoFee::getBillingType, + Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), + values -> String.join("、", values))))); + for (FormalSettlementDetail detail : details) { + detail.setBillingType(billingTypeMap.getOrDefault(detail.getSourceDetailId(), "")); + } + } + /** * 为历史正式结算明细补充运单收发货信息,保留明细中已有的快照值。 */ @@ -1090,9 +1121,23 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl detailFees(Long detailId) { FormalSettlementDetail detail = detailMapper.selectById(detailId); if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在"); - return detailFeeMapper.selectList(Wrappers.lambdaQuery() + List fees = detailFeeMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detailId) .orderByAsc(FormalSettlementDetailFee::getLineNo)); + List sourceFeeIds = fees.stream().map(FormalSettlementDetailFee::getSourceFeeId) + .filter(Objects::nonNull).toList(); + if (sourceFeeIds.isEmpty()) { + return fees; + } + Map sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream() + .collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left)); + fees.forEach(fee -> { + ReceivablePayableCargoFee sourceFee = sourceFeeMap.get(fee.getSourceFeeId()); + if (sourceFee != null) { + fee.setBillingRulesJson(sourceFee.getBillingRulesJson()); + } + }); + return fees; } @Override @@ -1102,11 +1147,15 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl existing = detailFees(detail.getId()).stream() .collect(Collectors.toMap(FormalSettlementDetailFee::getId, item -> item)); if (existing.size() != request.getRows().size()) throw new ServiceException("费用调整行数据不完整"); List changes = new java.util.ArrayList<>(); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); for (PreSettlementDetailAdjustRequest.FeeRow row : request.getRows()) { FormalSettlementDetailFee fee = existing.get(row.getId()); if (fee == null) throw new ServiceException("存在无效的货物费用行"); @@ -1119,7 +1168,6 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl rows = detailFees(detail.getId()); detail.setTransportQuantity(rows.stream().map(FormalSettlementDetailFee::getTransportQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setMileage(rows.stream().map(FormalSettlementDetailFee::getMileage).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + // 运输单价无法按行简单求和,与前端暂存逻辑一致:取首行费用单价。 + detail.setUnitPrice(rows.stream().map(FormalSettlementDetailFee::getUnitPrice).filter(Objects::nonNull).findFirst().orElse(null)); detail.setFreightAmount(rows.stream().map(FormalSettlementDetailFee::getFreightAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); detail.setOriginalAmount(rows.stream().map(FormalSettlementDetailFee::getOriginalAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); detail.setSettlementAmountTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); - detail.setSettlementAmountNoTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + boolean hasNoTax = rows.stream().anyMatch(fee -> fee.getSettlementAmountNoTax() != null); + detail.setSettlementAmountNoTax(hasNoTax + ? rows.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add) + : null); detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(detail.getOriginalAmount())); - detail.setRemark(reason); + Map feeItems = new LinkedHashMap<>(); + rows.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) -> + feeItems.merge(name, money(amount), BigDecimal::add))); + detail.setFeeItemsJson(JsonUtil.toJson(feeItems)); + detail.setRemark(detailRemark); detailMapper.updateById(detail); + appendTextChange(changes, beforeData, afterData, "备注", beforeDetailRemark, detailRemark); + if (changes.isEmpty()) { + throw new ServiceException("未修改任何结算明细费用"); + } rebuildSummaryFees(settlement.getId()); refreshSettlementAmount(settlement); saveChange(settlement.getId(), "结算明细项", detail.getLineNo(), "调整", - changes.isEmpty() ? "调整结算明细费用" : String.join(";", changes), reason); + String.join(";", changes), detailRemark, beforeData, afterData); } private void rebuildSummaryFees(Long settlementId) { @@ -1314,10 +1376,18 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl changes = new java.util.ArrayList<>(); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount()); + appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount()); + appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark()); + if (!changes.isEmpty()) { + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "", + beforeData, afterData); + } } for (FormalSettlementSummaryFee manualRow : existingManualRows) { - if (!retainedManualIds.contains(manualRow.getId())) summaryFeeMapper.deleteById(manualRow.getId()); + if (!retainedManualIds.contains(manualRow.getId())) { + summaryFeeMapper.deleteById(manualRow.getId()); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + beforeData.put("费用类型", manualRow.getFeeType()); + beforeData.put("费用项", manualRow.getFeeItem()); + beforeData.put("调整金额", money(manualRow.getAdjustAmount())); + beforeData.put("结算金额", money(manualRow.getSettlementAmount())); + beforeData.put("备注", manualRow.getRemark()); + afterData.put("费用类型", ""); + afterData.put("费用项", ""); + afterData.put("调整金额", BigDecimal.ZERO.setScale(2)); + afterData.put("结算金额", BigDecimal.ZERO.setScale(2)); + afterData.put("备注", ""); + saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", + "删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】", + "", beforeData, afterData); + } } renumberSummaryFees(settlementId); } @@ -1995,14 +2097,77 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl changes = new java.util.ArrayList<>(); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + if (creating) { + changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】"); + beforeData.put("费用类型", ""); + beforeData.put("费用项", ""); + beforeData.put("调整金额", BigDecimal.ZERO.setScale(2)); + beforeData.put("结算金额", BigDecimal.ZERO.setScale(2)); + beforeData.put("备注", ""); + afterData.put("费用类型", row.getFeeType()); + afterData.put("费用项", row.getFeeItem()); + afterData.put("调整金额", money(row.getAdjustAmount())); + afterData.put("结算金额", money(row.getSettlementAmount())); + afterData.put("备注", row.getRemark()); + saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "", + beforeData, afterData); + return; + } + appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType()); + appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem()); + appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount()); + appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount()); + appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark()); + if (changes.isEmpty()) { + return; + } + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "", + beforeData, afterData); + } + + private void appendChange(List changes, Map beforeData, Map afterData, + String fieldName, BigDecimal before, BigDecimal after) { + BigDecimal oldValue = money(before); + BigDecimal newValue = money(after); + if (oldValue.compareTo(newValue) != 0) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + beforeData.put(fieldName, oldValue); + afterData.put(fieldName, newValue); + } + } + + private void appendTextChange(List changes, Map beforeData, Map afterData, + String fieldName, String before, String after) { + String oldValue = before == null ? "" : before; + String newValue = after == null ? "" : after; + if (!Objects.equals(oldValue, newValue)) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + beforeData.put(fieldName, oldValue); + afterData.put(fieldName, newValue); + } + } + private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, String content, String reason) { + saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null); + } + + private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, + String content, String reason, Map beforeData, Map afterData) { FormalSettlementChangeRecord record = new FormalSettlementChangeRecord(); record.setFormalSettlementId(settlementId); record.setChangeType(changeType); record.setLineNo(lineNo); record.setOperationType(operationType); record.setChangeContent(content); + record.setBeforeData(beforeData == null ? null : JsonUtil.toJson(beforeData)); + record.setAfterData(afterData == null ? null : JsonUtil.toJson(afterData)); record.setChangeReason(reason); record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); record.setChangeTime(LocalDateTime.now()); 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 b3643674..6b827dd5 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 @@ -118,6 +118,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -324,6 +325,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl resolvePreSettlements(List ids) { - if (ids.size() <= 1) { + if (ids.isEmpty()) { return List.of(); } List settlements = ids.stream().map(preSettlementMapper::selectById).toList(); @@ -1953,9 +1955,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl !Objects.equals(contractId, item.getContractId()))) { - throw new ServiceException("多选预结算单必须属于同一合同"); + if (settlements.size() > 1) { + Long contractId = settlements.get(0).getContractId(); + if (contractId == null || settlements.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()))) { + throw new ServiceException("多选预结算单必须属于同一合同"); + } } return settlements; } @@ -1982,24 +1986,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl settlements) { - if (settlements.size() > 1) { - settlementRelationMapper.delete(Wrappers.lambdaQuery() - .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); - } else { - settlementRelationMapper.delete(Wrappers.lambdaQuery() - .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()) - .isNotNull(PaymentApplicationSettlement::getPreSettlementId)); + settlementRelationMapper.delete(Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()) + .isNotNull(PaymentApplicationSettlement::getPreSettlementId)); + List targetSettlements = settlements; + if (targetSettlements.isEmpty() && entity.getPreSettlementId() != null) { + PreSettlement single = preSettlementMapper.selectById(entity.getPreSettlementId()); + if (single != null && !Objects.equals(single.getIsDeleted(), 1)) { + targetSettlements = List.of(single); + } + } + if (targetSettlements.isEmpty()) { return; } - BigDecimal totalBase = settlements.stream() + // 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放) + BigDecimal totalBase = targetSettlements.stream() .map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); - for (int index = 0; index < settlements.size(); index++) { - PreSettlement settlement = settlements.get(index); + for (int index = 0; index < targetSettlements.size(); index++) { + PreSettlement settlement = targetSettlements.get(index); BigDecimal base = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); - BigDecimal applied = index == settlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0 - ? remainingApplied : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN); + BigDecimal applied = index == targetSettlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0 + ? remainingApplied + : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN); PaymentApplicationSettlement relation = new PaymentApplicationSettlement(); relation.setPaymentApplicationId(entity.getId()); relation.setPreSettlementId(settlement.getId()); @@ -2049,16 +2059,22 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl wrapper) { - return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream() - .filter(item -> !releasedBillPayment(item)) + return list(wrapper).stream() + .filter(item -> !releasedOccupiedPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } - /** 汇票付款被驳回后不再占用结算单可付款金额,删除草稿由逻辑删除自然释放。 */ - private boolean releasedBillPayment(PaymentApplication payment) { - return payment != null - && isBillPayment(payment.getPaymentMethod()) - && TransportBusiConstant.RETURNED.equals(payment.getApprovalStatus()); + /** + * 已释放占用的付款申请:作废、审核不通过(驳回)不再占用可付款金额; + * 删除草稿由逻辑删除自然释放。生成草稿/审批中/审批通过均占用。 + */ + private boolean releasedOccupiedPayment(PaymentApplication payment) { + if (payment == null || Objects.equals(payment.getIsDeleted(), 1)) { + return true; + } + String status = payment.getApprovalStatus(); + return TransportBusiConstant.VOIDED.equals(status) + || TransportBusiConstant.RETURNED.equals(status); } private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) { @@ -2071,14 +2087,88 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl paymentIds = new LinkedHashSet<>(); + if (preSettlementReference) { + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getPreSettlementId, referenceId) + .eq(PaymentApplicationSettlement::getIsDeleted, 0)); + for (PaymentApplicationSettlement relation : relations) { + if (relation.getPaymentApplicationId() != null) { + paymentIds.add(relation.getPaymentApplicationId()); + } + } + list(Wrappers.lambdaQuery() + .eq(PaymentApplication::getPreSettlementId, referenceId) + .ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)) + .forEach(payment -> paymentIds.add(payment.getId())); + } else { + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getFormalSettlementId, referenceId) + .eq(PaymentApplicationSettlement::getIsDeleted, 0)); + for (PaymentApplicationSettlement relation : relations) { + if (relation.getPaymentApplicationId() != null) { + paymentIds.add(relation.getPaymentApplicationId()); + } + } + list(Wrappers.lambdaQuery() + .eq(PaymentApplication::getSettlementId, referenceId) + .ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)) + .forEach(payment -> paymentIds.add(payment.getId())); + } + if (paymentIds.isEmpty()) { + return 1; + } + long priorCount = listByIds(paymentIds.stream().toList()).stream() + .filter(payment -> !releasedOccupiedPayment(payment)) + .filter(payment -> !Objects.equals(payment.getId(), excludeId)) + .count(); + return (int) priorCount + 1; + } + private BigDecimal cumulativeFormalSettlementApplied(Long settlementId, Long excludeId) { - return sumApplied(Wrappers.lambdaQuery() + // 多选正式结算时按关联行累计占用;单选兼容主表 settlementId + List relations = settlementRelationMapper.selectList( + Wrappers.lambdaQuery() + .eq(PaymentApplicationSettlement::getFormalSettlementId, settlementId) + .eq(PaymentApplicationSettlement::getIsDeleted, 0)); + Set coveredPaymentIds = new HashSet<>(); + BigDecimal relationAmount = BigDecimal.ZERO; + if (!relations.isEmpty()) { + Map payments = listByIds(relations.stream() + .map(PaymentApplicationSettlement::getPaymentApplicationId).filter(Objects::nonNull).distinct().toList()) + .stream().collect(Collectors.toMap(PaymentApplication::getId, item -> item, (left, right) -> left)); + for (PaymentApplicationSettlement relation : relations) { + PaymentApplication payment = payments.get(relation.getPaymentApplicationId()); + if (releasedOccupiedPayment(payment) || Objects.equals(payment.getId(), excludeId)) { + if (payment != null) { + coveredPaymentIds.add(payment.getId()); + } + continue; + } + coveredPaymentIds.add(payment.getId()); + relationAmount = relationAmount.add(money(relation.getAppliedAmount())); + } + } + BigDecimal legacyAmount = sumApplied(Wrappers.lambdaQuery() .eq(PaymentApplication::getSettlementId, settlementId) - .ne(excludeId != null, PaymentApplication::getId, excludeId)); + .ne(excludeId != null, PaymentApplication::getId, excludeId) + .notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)); + return relationAmount.add(legacyAmount); } private BigDecimal cumulativePreSettlementApplied(Long preSettlementId, Long excludeId) { - // 进度预付、尾款付款关联同一预结算单时累计占用可付款额度;多选时按关联行的申请金额累计,避免把整单金额算到第一张预结算上 + // 进度预付、尾款付款关联同一预结算单时累计占用可付款额度;按关联行申请金额累计,兼容历史仅写主表的单据 List relations = settlementRelationMapper.selectList( Wrappers.lambdaQuery() .eq(PaymentApplicationSettlement::getPreSettlementId, preSettlementId) @@ -2091,23 +2181,21 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl item, (left, right) -> left)); for (PaymentApplicationSettlement relation : relations) { PaymentApplication payment = payments.get(relation.getPaymentApplicationId()); - if (payment == null || Objects.equals(payment.getIsDeleted(), 1)) { + if (releasedOccupiedPayment(payment) || Objects.equals(payment.getId(), excludeId)) { + if (payment != null) { + coveredPaymentIds.add(payment.getId()); + } continue; } coveredPaymentIds.add(payment.getId()); - if (Objects.equals(payment.getId(), excludeId) || TransportBusiConstant.VOIDED.equals(payment.getApprovalStatus()) - || releasedBillPayment(payment)) { - continue; - } relationAmount = relationAmount.add(money(relation.getAppliedAmount())); } } BigDecimal legacyAmount = list(Wrappers.lambdaQuery() .eq(PaymentApplication::getPreSettlementId, preSettlementId) .ne(excludeId != null, PaymentApplication::getId, excludeId) - .notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds) - .ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream() - .filter(item -> !releasedBillPayment(item)) + .notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)).stream() + .filter(item -> !releasedOccupiedPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); return relationAmount.add(legacyAmount); } @@ -2150,6 +2238,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl sourcePage = sourceDetailMapper.selectPage( new Page<>(page.getCurrent(), page.getSize()), wrapper); Page> result = new Page<>(sourcePage.getCurrent(), sourcePage.getSize(), sourcePage.getTotal()); - result.setRecords(sourcePage.getRecords().stream().map(this::candidateMap).toList()); + List> records = sourcePage.getRecords().stream().map(this::candidateMap).toList(); + fillCandidateBillingTypes(records); + result.setRecords(records); return result; } @@ -616,7 +618,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl existingMap = detailFees(detail.getId()).stream() .collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity())); if (request.getRows().size() != existingMap.size()) { @@ -636,7 +640,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl beforeFeeItems = parseFeeItems(fee.getFeeItemsJson()); fee.setTransportQuantity(nonNegative(requestRow.getTransportQuantity(), "运输总量")); fee.setMileage(nonNegative(requestRow.getMileage(), "里程")); @@ -649,7 +652,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl> records) { + if (records == null || records.isEmpty()) { + return; + } + List detailIds = records.stream() + .map(item -> Func.toLong(item.get("id"))) + .filter(id -> id != null && id > 0) + .distinct() + .toList(); + if (detailIds.isEmpty()) { + return; + } + Map billingTypeMap = sourceFeeMapper.selectList( + Wrappers.lambdaQuery() + .in(ReceivablePayableCargoFee::getDetailId, detailIds) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)) + .stream() + .filter(fee -> Func.isNotEmpty(fee.getBillingType())) + .collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId, + Collectors.mapping(ReceivablePayableCargoFee::getBillingType, + Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), + values -> String.join("、", values))))); + for (Map record : records) { + Long detailId = Func.toLong(record.get("id")); + record.put("billingType", billingTypeMap.getOrDefault(detailId, "")); + } + } + private void synchronizeDetails(PreSettlement settlement, List requestedIds, boolean allowSourceMismatch) { List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); List existingDetails = listDetails(settlement.getId()); @@ -1001,13 +1036,30 @@ public class PreSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) .eq(PreSettlementSummaryFee::getManualFlag, 0)); - int lineNo = 1; - for (Map.Entry entry : aggregates.entrySet()) { + // 尽量沿用已有系统归集行的序号,避免重建后与手工费用交错顺序被打乱。 + List> orderedAggregates = new ArrayList<>(aggregates.entrySet()); + orderedAggregates.sort((left, right) -> { + PreSettlementSummaryFee leftOld = existingGenerated.get(left.getKey()); + PreSettlementSummaryFee rightOld = existingGenerated.get(right.getKey()); + Integer leftLineNo = leftOld == null ? null : leftOld.getLineNo(); + Integer rightLineNo = rightOld == null ? null : rightOld.getLineNo(); + if (leftLineNo == null && rightLineNo == null) { + return 0; + } + if (leftLineNo == null) { + return 1; + } + if (rightLineNo == null) { + return -1; + } + return Integer.compare(leftLineNo, rightLineNo); + }); + for (Map.Entry entry : orderedAggregates) { String[] keyParts = entry.getKey().split("\\|", 2); PreSettlementSummaryFee old = existingGenerated.get(entry.getKey()); PreSettlementSummaryFee row = new PreSettlementSummaryFee(); row.setPreSettlementId(settlementId); - row.setLineNo(lineNo++); + row.setLineNo(old == null || old.getLineNo() == null ? Integer.MAX_VALUE : old.getLineNo()); row.setFeeType(keyParts[0]); row.setFeeItem(keyParts.length > 1 ? keyParts[1] : ""); // 原金额是明细首次生成时的快照,多次调整后不得被当前结算金额覆盖。 @@ -1023,7 +1075,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestRows) { - if (requestRows == null) return; + if (requestRows == null) { + return; + } + // 空列表表示沿用 rebuildSummaryFees 结果(如更换合同后),不得再按空请求删光合计行。 + if (requestRows.isEmpty()) { + renumberSummaryFees(settlementId); + return; + } Map> allowedManualFees = new LinkedHashMap<>(); if (requestRows.stream().anyMatch(row -> Integer.valueOf(1).equals(row.getManualFlag()))) { for (Map option : feeOptions()) { @@ -1043,6 +1102,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl availableGeneratedRows = new ArrayList<>(existingGeneratedRows); Set retainedManualIds = new LinkedHashSet<>(); Set retainedGeneratedIds = new LinkedHashSet<>(); + int lineNo = 1; for (PreSettlementSaveRequest.SummaryFee requestRow : requestRows) { if (Integer.valueOf(1).equals(requestRow.getManualFlag())) { String feeType = requiredText(requestRow.getFeeType(), "费用类型"); @@ -1059,11 +1119,20 @@ public class PreSettlementServiceImpl extends BaseServiceImpl beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + beforeData.put("费用项", row.getFeeItem()); + beforeData.put("调整金额", before); + beforeData.put("结算金额", money(row.getOriginalAmount().add(before))); + afterData.put("费用项", row.getFeeItem()); + afterData.put("调整金额", row.getAdjustAmount()); + afterData.put("结算金额", money(row.getSettlementAmount())); + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", + "【" + row.getFeeItem() + "-调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】", + "", beforeData, afterData); } } for (PreSettlementSummaryFee manualRow : manualRows) { if (!retainedManualIds.contains(manualRow.getId())) { summaryFeeMapper.deleteById(manualRow.getId()); - saveChange(settlementId, "合计费用项", null, "删除", - "删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), ""); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + beforeData.put("费用类型", manualRow.getFeeType()); + beforeData.put("费用项", manualRow.getFeeItem()); + beforeData.put("调整金额", money(manualRow.getAdjustAmount())); + beforeData.put("结算金额", money(manualRow.getSettlementAmount())); + beforeData.put("备注", manualRow.getRemark()); + afterData.put("费用类型", ""); + afterData.put("费用项", ""); + afterData.put("调整金额", BigDecimal.ZERO.setScale(2)); + afterData.put("结算金额", BigDecimal.ZERO.setScale(2)); + afterData.put("备注", ""); + saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", + "删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】", + "", beforeData, afterData); } } for (PreSettlementSummaryFee generatedRow : existingGeneratedRows) { @@ -1122,32 +1218,63 @@ public class PreSettlementServiceImpl extends BaseServiceImpl fees = detailFees(detail.getId()); + BigDecimal transportQuantity = fees.stream().map(PreSettlementDetailFee::getTransportQuantity) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mileage = fees.stream().map(PreSettlementDetailFee::getMileage) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal freightAmount = fees.stream().map(PreSettlementDetailFee::getFreightAmount) .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal originalAmount = fees.stream().map(PreSettlementDetailFee::getOriginalAmount) .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal settlementAmountTax = fees.stream().map(PreSettlementDetailFee::getSettlementAmountTax) .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal settlementAmountNoTax = fees.stream().map(PreSettlementDetailFee::getSettlementAmountNoTax) + .filter(Objects::nonNull).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); Map feeItems = new LinkedHashMap<>(); fees.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) -> feeItems.merge(name, money(amount), BigDecimal::add))); + // 运输单价无法按行简单求和,与前端暂存逻辑一致:取首行费用单价。 + BigDecimal unitPrice = fees.stream().map(PreSettlementDetailFee::getUnitPrice) + .filter(Objects::nonNull).findFirst().orElse(null); + String remark = fees.stream().map(PreSettlementDetailFee::getRemark) + .filter(Func::isNotEmpty).findFirst().orElse(detail.getRemark()); + detail.setTransportQuantity(money(transportQuantity)); + detail.setMileage(mileage); + detail.setUnitPrice(unitPrice); detail.setFreightAmount(money(freightAmount)); detail.setOriginalAmount(money(originalAmount)); detail.setAdjustAmount(money(settlementAmountTax.subtract(originalAmount))); detail.setSettlementAmountTax(money(settlementAmountTax)); + detail.setSettlementAmountNoTax(fees.stream().anyMatch(fee -> fee.getSettlementAmountNoTax() != null) + ? money(settlementAmountNoTax) : null); detail.setFeeItemsJson(JsonUtil.toJson(feeItems)); + detail.setRemark(limitRemark(remark, 200)); detailMapper.updateById(detail); } + /** + * 结算金额以结算合计为准(含手工新增费用),与详情页合计及付款申请取数口径一致。 + * 合计行为空时回退为明细含税金额合计。 + */ private void refreshSettlementAmount(PreSettlement settlement) { - BigDecimal settlementAmount = listDetails(settlement.getId()).stream() - .map(PreSettlementDetail::getSettlementAmountTax).map(this::money) - .reduce(BigDecimal.ZERO, BigDecimal::add); + List summaryFees = listSummaryFees(settlement.getId()); + BigDecimal settlementAmount; + if (!summaryFees.isEmpty()) { + settlementAmount = summaryFees.stream() + .map(PreSettlementSummaryFee::getSettlementAmount) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } else { + settlementAmount = listDetails(settlement.getId()).stream() + .map(PreSettlementDetail::getSettlementAmountTax) + .map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } settlement.setSettlementAmount(money(settlementAmount)); BigDecimal rate = normalizeRate(settlement.getExchangeRate(), settlement.getCurrency()); settlement.setExchangeRate(rate); @@ -1405,6 +1532,40 @@ public class PreSettlementServiceImpl extends BaseServiceImpl changes = new ArrayList<>(); + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + if (creating) { + changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】"); + beforeData.put("费用类型", ""); + beforeData.put("费用项", ""); + beforeData.put("调整金额", BigDecimal.ZERO.setScale(2)); + beforeData.put("结算金额", BigDecimal.ZERO.setScale(2)); + beforeData.put("备注", ""); + afterData.put("费用类型", row.getFeeType()); + afterData.put("费用项", row.getFeeItem()); + afterData.put("调整金额", money(row.getAdjustAmount())); + afterData.put("结算金额", money(row.getSettlementAmount())); + afterData.put("备注", row.getRemark()); + saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "", + beforeData, afterData); + return; + } + appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType()); + appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem()); + appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount()); + appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount()); + appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark()); + if (changes.isEmpty()) { + return; + } + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "", + beforeData, afterData); + } + private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, String content, String reason) { saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java index 5a40decd..0f92b9b4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -385,9 +385,17 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl idList = Func.toLongList(ids); if (Func.isEmpty(idList)) throw new ServiceException("请选择需要删除的运单批次"); + List batches = listByIds(idList).stream() + .filter(batch -> !Objects.equals(batch.getIsDeleted(), 1)) + .toList(); + // 与前端一致:仅草稿可删;选中含已完成/进行中时整批拒绝 + boolean hasNonDraft = batches.stream() + .anyMatch(batch -> !STATUS_DRAFT.equals(batch.getImportStatus())); + if (hasNonDraft) { + throw new ServiceException("已完成运单不可删除"); + } BusinessRemoveResultVO result = new BusinessRemoveResultVO(); - for (WaybillImportBatch batch : listByIds(idList)) { - if (Objects.equals(batch.getIsDeleted(), 1)) continue; + for (WaybillImportBatch batch : batches) { List waybills = waybillService.list(Wrappers.lambdaQuery() .eq(Waybill::getImportBatchId, batch.getId()).eq(Waybill::getIsDeleted, 0)); if (Func.isNotEmpty(waybills)) { diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql index 98ccfc06..240eafcb 100644 --- a/doc/sql/transport/blade_formal_settlement_20260818.sql +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -44,7 +44,9 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` ( `update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', `change_type` varchar(50) NOT NULL COMMENT '变更类型', `line_no` int(11) DEFAULT NULL COMMENT '行号', `operation_type` varchar(30) NOT NULL COMMENT '操作类型', - `change_content` text NOT NULL COMMENT '变更内容', `change_reason` varchar(200) DEFAULT NULL COMMENT '变更原因', + `change_content` text NOT NULL COMMENT '变更内容', + `before_data` longtext COMMENT '变更前数据JSON', `after_data` longtext COMMENT '变更后数据JSON', + `change_reason` varchar(200) DEFAULT NULL COMMENT '变更原因', `operator_name` varchar(100) DEFAULT NULL COMMENT '操作人', `change_time` datetime NOT NULL COMMENT '变更时间', PRIMARY KEY (`id`), KEY `idx_formal_settlement_change_bill` (`formal_settlement_id`), KEY `idx_formal_settlement_change_time` (`change_time`) @@ -123,17 +125,29 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算付款申请'; INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES -(2090000000000001040,2090000000000001000,'formal_settlement','正式结算单','formal_settlement','/settlement/formal-settlement','iconfont icon-caidanguanli',4,1,0,1,NULL,'',0), -(2090000000000001041,2090000000000001040,'formal_settlement_view','查看','formal_settlement_view','','',1,2,0,1,NULL,'',0), -(2090000000000001042,2090000000000001040,'formal_settlement_add','新增','formal_settlement_add','','',2,2,0,1,NULL,'',0), -(2090000000000001043,2090000000000001040,'formal_settlement_edit','编辑','formal_settlement_edit','','',3,2,0,1,NULL,'',0), -(2090000000000001044,2090000000000001040,'formal_settlement_delete','删除','formal_settlement_delete','','',4,2,0,1,NULL,'',0), -(2090000000000001045,2090000000000001040,'formal_settlement_submit','提交审批','formal_settlement_submit','','',5,2,0,1,NULL,'',0), -(2090000000000001046,2090000000000001040,'formal_settlement_approve','审批','formal_settlement_approve','','',6,2,0,1,NULL,'',0), -(2090000000000001047,2090000000000001040,'formal_settlement_sync','同步金蝶','formal_settlement_sync','','',7,2,0,1,NULL,'',0), -(2090000000000001048,2090000000000001040,'formal_settlement_print','打印','formal_settlement_print','','',8,2,0,1,NULL,'',0), -(2090000000000001049,2090000000000001040,'formal_settlement_export','导出','formal_settlement_export','','',9,2,0,1,NULL,'',0), -(2090000000000001050,2090000000000001040,'formal_settlement_void','作废','formal_settlement_void','','',10,2,0,1,NULL,'',0), -(2090000000000001051,2090000000000001040,'formal_settlement_adjust','明细调整','formal_settlement_adjust','','',11,2,0,1,NULL,'',0), -(2090000000000001052,2090000000000001040,'formal_settlement_payment','付款申请','formal_settlement_payment','','',12,2,0,1,NULL,'',0) +(2090000000000001040,2090000000000001000,'payment_settlement','付款结算单','payment_settlement','/settlement/payment-settlement','iconfont icon-caidanguanli',4,1,0,1,NULL,'',0), +(2090000000000001041,2090000000000001040,'payment_settlement_view','查看','payment_settlement_view','','',1,2,0,1,NULL,'',0), +(2090000000000001042,2090000000000001040,'payment_settlement_add','新增','payment_settlement_add','','',2,2,0,1,NULL,'',0), +(2090000000000001043,2090000000000001040,'payment_settlement_edit','编辑','payment_settlement_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001044,2090000000000001040,'payment_settlement_delete','删除','payment_settlement_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001045,2090000000000001040,'payment_settlement_submit','提交审批','payment_settlement_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001046,2090000000000001040,'payment_settlement_approve','审批','payment_settlement_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001047,2090000000000001040,'payment_settlement_sync','同步金蝶','payment_settlement_sync','','',7,2,0,1,NULL,'',0), +(2090000000000001048,2090000000000001040,'payment_settlement_print','打印','payment_settlement_print','','',8,2,0,1,NULL,'',0), +(2090000000000001049,2090000000000001040,'payment_settlement_export','导出','payment_settlement_export','','',9,2,0,1,NULL,'',0), +(2090000000000001050,2090000000000001040,'payment_settlement_void','作废','payment_settlement_void','','',10,2,0,1,NULL,'',0), +(2090000000000001051,2090000000000001040,'payment_settlement_adjust','明细调整','payment_settlement_adjust','','',11,2,0,1,NULL,'',0), +(2090000000000001052,2090000000000001040,'payment_settlement_payment','付款申请','payment_settlement_payment','','',12,2,0,1,NULL,'',0), +(2090000000000002100,2090000000000001000,'receipt_settlement','收款结算单','receipt_settlement','/settlement/receipt-settlement','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0), +(2090000000000002101,2090000000000002100,'receipt_settlement_view','查看','receipt_settlement_view','','',1,2,0,1,NULL,'',0), +(2090000000000002102,2090000000000002100,'receipt_settlement_add','新增','receipt_settlement_add','','',2,2,0,1,NULL,'',0), +(2090000000000002103,2090000000000002100,'receipt_settlement_edit','编辑','receipt_settlement_edit','','',3,2,0,1,NULL,'',0), +(2090000000000002104,2090000000000002100,'receipt_settlement_delete','删除','receipt_settlement_delete','','',4,2,0,1,NULL,'',0), +(2090000000000002105,2090000000000002100,'receipt_settlement_submit','提交审批','receipt_settlement_submit','','',5,2,0,1,NULL,'',0), +(2090000000000002106,2090000000000002100,'receipt_settlement_approve','审批','receipt_settlement_approve','','',6,2,0,1,NULL,'',0), +(2090000000000002107,2090000000000002100,'receipt_settlement_sync','同步金蝶','receipt_settlement_sync','','',7,2,0,1,NULL,'',0), +(2090000000000002108,2090000000000002100,'receipt_settlement_print','打印','receipt_settlement_print','','',8,2,0,1,NULL,'',0), +(2090000000000002109,2090000000000002100,'receipt_settlement_export','导出','receipt_settlement_export','','',9,2,0,1,NULL,'',0), +(2090000000000002110,2090000000000002100,'receipt_settlement_void','作废','receipt_settlement_void','','',10,2,0,1,NULL,'',0), +(2090000000000002111,2090000000000002100,'receipt_settlement_adjust','明细调整','receipt_settlement_adjust','','',11,2,0,1,NULL,'',0) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql b/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql index 08ba660e..bf2abb67 100644 --- a/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql +++ b/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql @@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` ( `line_no` int(11) DEFAULT NULL COMMENT '行号', `operation_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作类型', `change_content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更内容', + `before_data` longtext COLLATE utf8mb4_general_ci COMMENT '变更前数据JSON', + `after_data` longtext COLLATE utf8mb4_general_ci COMMENT '变更后数据JSON', `change_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因', `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', `change_time` datetime NOT NULL COMMENT '变更时间', diff --git a/doc/sql/transport/blade_formal_settlement_change_record_detail_20261018.sql b/doc/sql/transport/blade_formal_settlement_change_record_detail_20261018.sql new file mode 100644 index 00000000..65eb28e7 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_change_record_detail_20261018.sql @@ -0,0 +1,30 @@ +-- 正式结算变更记录补充前后值明细,与预结算变更记录口径一致。 +SET @before_data_exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement_change_record' + AND COLUMN_NAME = 'before_data' +); +SET @before_data_sql := IF( + @before_data_exists = 0, + 'ALTER TABLE `blade_formal_settlement_change_record` ADD COLUMN `before_data` longtext COLLATE utf8mb4_general_ci COMMENT ''变更前数据JSON'' AFTER `change_content`', + 'SELECT 1' +); +PREPARE before_data_stmt FROM @before_data_sql; +EXECUTE before_data_stmt; +DEALLOCATE PREPARE before_data_stmt; + +SET @after_data_exists := ( + SELECT COUNT(1) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'blade_formal_settlement_change_record' + AND COLUMN_NAME = 'after_data' +); +SET @after_data_sql := IF( + @after_data_exists = 0, + 'ALTER TABLE `blade_formal_settlement_change_record` ADD COLUMN `after_data` longtext COLLATE utf8mb4_general_ci COMMENT ''变更后数据JSON'' AFTER `before_data`', + 'SELECT 1' +); +PREPARE after_data_stmt FROM @after_data_sql; +EXECUTE after_data_stmt; +DEALLOCATE PREPARE after_data_stmt; diff --git a/doc/sql/transport/blade_pre_settlement_20260818.sql b/doc/sql/transport/blade_pre_settlement_20260818.sql index 2079beb9..6f20b639 100644 --- a/doc/sql/transport/blade_pre_settlement_20260818.sql +++ b/doc/sql/transport/blade_pre_settlement_20260818.sql @@ -200,7 +200,7 @@ CREATE TABLE IF NOT EXISTS `blade_pre_settlement_change_record` ( INSERT INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES -(2090000000000001020, 2090000000000001000, 'pre_settlement', '预结算', 'pre_settlement', '/settlement/pre-settlement', 'iconfont icon-caidanguanli', 3, 1, 0, 1, NULL, '', 0), +(2090000000000001020, 2090000000000001000, 'pre_settlement', '预付结算单', 'pre_settlement', '/settlement/pre-settlement', 'iconfont icon-caidanguanli', 3, 1, 0, 1, NULL, '', 0), (2090000000000001021, 2090000000000001020, 'pre_settlement_view', '查看', 'pre_settlement_view', '', '', 1, 2, 0, 1, NULL, '', 0), (2090000000000001022, 2090000000000001020, 'pre_settlement_add', '新增', 'pre_settlement_add', '', '', 2, 2, 0, 1, NULL, '', 0), (2090000000000001023, 2090000000000001020, 'pre_settlement_edit', '编辑', 'pre_settlement_edit', '', '', 3, 2, 0, 1, NULL, '', 0), diff --git a/doc/sql/transport/blade_settlement_menu_split_20261018.sql b/doc/sql/transport/blade_settlement_menu_split_20261018.sql new file mode 100644 index 00000000..27df1058 --- /dev/null +++ b/doc/sql/transport/blade_settlement_menu_split_20261018.sql @@ -0,0 +1,163 @@ +-- 结算菜单调整: +-- 1)预结算 → 预付结算单 +-- 2)正式结算单拆分为:付款结算单(应付)、收款结算单(应收) +-- 执行后需重新登录;已授权「正式结算单/付款结算单」的角色会同步授权「收款结算单」。 + +-- ① 预结算改名 +UPDATE `blade_menu` +SET `name` = '预付结算单' +WHERE `code` = 'pre_settlement' + AND `is_deleted` = 0; + +-- ② 原正式结算单菜单改为「付款结算单」(保留原菜单 id,角色授权不丢) +UPDATE `blade_menu` +SET `code` = 'payment_settlement', + `name` = '付款结算单', + `alias` = 'payment_settlement', + `path` = '/settlement/payment-settlement', + `is_deleted` = 0 +WHERE `code` IN ('formal_settlement', 'payment_settlement') + AND `category` = 1 + AND `is_deleted` = 0; + +-- 付款结算单按钮权限码:formal_settlement_* → payment_settlement_* +UPDATE `blade_menu` child +INNER JOIN `blade_menu` parent + ON parent.`id` = child.`parent_id` + AND parent.`code` = 'payment_settlement' + AND parent.`is_deleted` = 0 +SET child.`code` = REPLACE(child.`code`, 'formal_settlement_', 'payment_settlement_'), + child.`alias` = REPLACE(IFNULL(child.`alias`, child.`code`), 'formal_settlement_', 'payment_settlement_'), + child.`is_deleted` = 0 +WHERE child.`category` = 2 + AND child.`code` LIKE 'formal_settlement_%'; + +-- ③ 新增「收款结算单」菜单 +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT + 2090000000000002100, + parent.`parent_id`, + 'receipt_settlement', + '收款结算单', + 'receipt_settlement', + '/settlement/receipt-settlement', + IFNULL(parent.`source`, 'iconfont icon-caidanguanli'), + IFNULL(parent.`sort`, 4) + 1, + 1, 0, 1, NULL, '', 0 +FROM `blade_menu` parent +WHERE parent.`code` = 'payment_settlement' + AND parent.`is_deleted` = 0 + AND NOT EXISTS ( + SELECT 1 FROM `blade_menu` x WHERE x.`code` = 'receipt_settlement' AND x.`is_deleted` = 0 + ) +LIMIT 1; + +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002101, p.id, 'receipt_settlement_view', '查看', 'receipt_settlement_view', '', '', 1, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_view' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002102, p.id, 'receipt_settlement_add', '新增', 'receipt_settlement_add', '', '', 2, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_add' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002103, p.id, 'receipt_settlement_edit', '编辑', 'receipt_settlement_edit', '', '', 3, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_edit' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002104, p.id, 'receipt_settlement_delete', '删除', 'receipt_settlement_delete', '', '', 4, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_delete' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002105, p.id, 'receipt_settlement_submit', '提交审批', 'receipt_settlement_submit', '', '', 5, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_submit' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002106, p.id, 'receipt_settlement_approve', '审批', 'receipt_settlement_approve', '', '', 6, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_approve' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002107, p.id, 'receipt_settlement_sync', '同步金蝶', 'receipt_settlement_sync', '', '', 7, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_sync' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002108, p.id, 'receipt_settlement_print', '打印', 'receipt_settlement_print', '', '', 8, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_print' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002109, p.id, 'receipt_settlement_export', '导出', 'receipt_settlement_export', '', '', 9, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_export' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002110, p.id, 'receipt_settlement_void', '作废', 'receipt_settlement_void', '', '', 10, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_void' AND x.is_deleted = 0) +LIMIT 1; +INSERT INTO `blade_menu` +(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +SELECT 2090000000000002111, p.id, 'receipt_settlement_adjust', '明细调整', 'receipt_settlement_adjust', '', '', 11, 2, 0, 1, NULL, '', 0 +FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0 + AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_adjust' AND x.is_deleted = 0) +LIMIT 1; + +-- ④ 角色授权:持有付款结算单的角色同步收款结算单(含按钮) +INSERT INTO `blade_role_menu` (`id`, `menu_id`, `role_id`) +SELECT + 2091200000000000000 + CAST(rm.`role_id` AS UNSIGNED) % 100000, + receipt.`id`, + rm.`role_id` +FROM `blade_role_menu` rm +JOIN `blade_menu` payment ON payment.`id` = rm.`menu_id` + AND payment.`code` = 'payment_settlement' + AND payment.`is_deleted` = 0 +JOIN `blade_menu` receipt ON receipt.`code` = 'receipt_settlement' + AND receipt.`is_deleted` = 0 +WHERE NOT EXISTS ( + SELECT 1 FROM `blade_role_menu` x + WHERE x.`menu_id` = receipt.`id` AND x.`role_id` = rm.`role_id` +); + +INSERT INTO `blade_role_menu` (`id`, `menu_id`, `role_id`) +SELECT + 2091300000000000000 + + (CAST(rm.`role_id` AS UNSIGNED) % 100000) * 20 + + (CAST(receipt_btn.`id` AS UNSIGNED) % 20), + receipt_btn.`id`, + rm.`role_id` +FROM `blade_role_menu` rm +JOIN `blade_menu` payment_btn ON payment_btn.`id` = rm.`menu_id` + AND payment_btn.`code` LIKE 'payment_settlement_%' + AND payment_btn.`is_deleted` = 0 +JOIN `blade_menu` receipt_btn + ON receipt_btn.`code` = REPLACE(payment_btn.`code`, 'payment_settlement_', 'receipt_settlement_') + AND receipt_btn.`is_deleted` = 0 +WHERE NOT EXISTS ( + SELECT 1 FROM `blade_role_menu` x + WHERE x.`menu_id` = receipt_btn.`id` AND x.`role_id` = rm.`role_id` +); + +-- ⑤ 残留旧正式结算菜单隐藏 +UPDATE `blade_menu` +SET `is_deleted` = 1 +WHERE `code` = 'formal_settlement' + AND `category` = 1;