diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java index 936ec485..ce4c9830 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java @@ -103,6 +103,18 @@ public class ContractChange extends TenantEntity { @Schema(description = "乙方") private String partyB; + @Schema(description = "甲方客商明细JSON") + private String partyAJson; + + @Schema(description = "乙方客商明细JSON") + private String partyBJson; + + @Schema(description = "丙方客商明细JSON") + private String partyCJson; + + @Schema(description = "丙方") + private String partyC; + @Schema(description = "开始日期") private LocalDate startDate; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java index 655ae234..67e108f7 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java @@ -79,6 +79,18 @@ public class ContractManage extends TenantEntity { @Schema(description = "乙方") private String partyB; + @Schema(description = "甲方客商明细JSON") + private String partyAJson; + + @Schema(description = "乙方客商明细JSON") + private String partyBJson; + + @Schema(description = "丙方客商明细JSON") + private String partyCJson; + + @Schema(description = "丙方") + private String partyC; + @Schema(description = "开始日期") private LocalDate startDate; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java index 1a1283de..90129764 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/LoadingManage.java @@ -86,6 +86,12 @@ public class LoadingManage extends TenantEntity { @Schema(description = "承运商合同ID") private Long carrierContractId; + @Schema(description = "承运客商明细行ID") + private String carrierPartyId; + + @Schema(description = "承运客商角色:B/C") + private String carrierPartyRole; + @Schema(description = "发货地址") private String departureAddress; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index a3157095..bd85768e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -136,6 +136,12 @@ public class Waybill extends TenantEntity { @Schema(description = "承运商合同ID") private Long carrierContractId; + @Schema(description = "承运客商明细行ID") + private String carrierPartyId; + + @Schema(description = "承运客商角色:B/C") + private String carrierPartyRole; + @Schema(description = "司机ID") private Long driverId; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java index 22a40b87..76417029 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/LoadingCarrierContractVO.java @@ -55,4 +55,17 @@ public class LoadingCarrierContractVO implements Serializable { @Schema(description = "承运商名称") private String carrierName; + @Schema(description = "承运客商明细行ID") + private String carrierPartyId; + + @Schema(description = "承运客商角色:B/C") + private String carrierPartyRole; + + @Schema(description = "统一信用代码") + private String creditCode; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "客商档案ID") + private Long customerId; + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java index 1ce202dd..f040a9ae 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderCarrierVO.java @@ -34,4 +34,17 @@ public class MasterOrderCarrierVO implements Serializable { @Schema(description = "承运商名称") private String carrierName; + @Schema(description = "承运客商明细行ID") + private String carrierPartyId; + + @Schema(description = "承运客商角色:B/C") + private String carrierPartyRole; + + @Schema(description = "统一信用代码") + private String creditCode; + + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "客商档案ID") + private Long customerId; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java index 2e9fdbc0..11eccfd6 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/ContractManageExcel.java @@ -66,6 +66,8 @@ public class ContractManageExcel implements Serializable { private String partyA; @ExcelProperty("乙方") private String partyB; + @ExcelProperty("丙方") + private String partyC; @ExcelProperty("开始日期") private LocalDate startDate; @ExcelProperty("结束日期") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java index bb66d1c9..9620d52c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java @@ -42,6 +42,7 @@ import org.springblade.transport.pojo.vo.ContractChangeVO; import org.springblade.transport.pojo.vo.ContractManageVO; import org.springblade.transport.service.IContractChangeService; import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.support.ContractPartySupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.wrapper.ContractChangeWrapper; import org.springframework.stereotype.Service; @@ -308,7 +309,20 @@ public class ContractChangeServiceImpl extends BaseServiceImpl data = new LinkedHashMap<>(); data.put("contractNo", contractManage.getContractNo()); data.put("contractName", contractManage.getContractName()); + data.put("partyA", contractManage.getPartyA()); data.put("partyB", contractManage.getPartyB()); + data.put("partyC", contractManage.getPartyC()); + data.put("partyAJson", contractManage.getPartyAJson()); + data.put("partyBJson", contractManage.getPartyBJson()); + data.put("partyCJson", contractManage.getPartyCJson()); data.put("startDate", contractManage.getStartDate()); data.put("endDate", contractManage.getEndDate()); data.put("contractFormat", contractManage.getContractFormat()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 581a3f37..b2f92153 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -35,6 +35,7 @@ import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.ILoadingManageService; import org.springblade.transport.service.IProcessConfigService; import org.springblade.transport.service.IReceivablePayableDetailService; +import org.springblade.transport.support.ContractPartySupport; import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.LoadableAdmissionSupport; import org.springblade.transport.support.TransportBusinessSupport; @@ -118,23 +119,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl contracts = availableCarrierContracts(projectIds); + Set allowedCarrierNames = null; if (Func.isNotEmpty(waybillIds)) { List waybills = loadWaybills(waybillIds); - Set allowedPartyBs = intersectCarrierPartyBs(waybills, contracts); - if (allowedPartyBs.isEmpty()) { + allowedCarrierNames = intersectCarrierPartyBs(waybills, contracts); + if (allowedCarrierNames.isEmpty()) { return List.of(); } + Set allowedNames = allowedCarrierNames; contracts = contracts.stream() - .filter(contract -> allowedPartyBs.contains(normalizePartyName(contract.getPartyB()))) + .filter(contract -> ContractPartySupport.carrierPartyNames(contract).stream() + .anyMatch(allowedNames::contains)) .toList(); } - return contracts.stream().map(contract -> { - LoadingCarrierContractVO option = new LoadingCarrierContractVO(); - option.setId(contract.getId()); - option.setContractName(contract.getContractName()); - option.setCarrierName(contract.getPartyB()); - return option; - }).toList(); + List options = ContractPartySupport.expandCarrierOptions(contracts); + if (allowedCarrierNames == null) { + return options; + } + Set allowedNames = allowedCarrierNames; + return options.stream() + .filter(option -> allowedNames.contains(normalizePartyName(option.getCarrierName()))) + .toList(); } @Override @@ -973,6 +978,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills) { if (!Objects.equals(loadingManage.getCarrierType(), "承运商")) { loadingManage.setCarrierContractId(null); + loadingManage.setCarrierPartyId(null); + loadingManage.setCarrierPartyRole(null); if (!Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) { loadingManage.setCarrierName(null); } @@ -983,6 +990,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl selectedParty = ContractPartySupport.findCarrierParty( + carrierContract, loadingManage.getCarrierPartyId(), loadingManage.getCarrierName()); + if (selectedParty == null && Func.isNotEmpty(loadingManage.getCarrierName()) + && !ContractPartySupport.matchesCarrier(carrierContract, loadingManage.getCarrierName(), loadingManage.getCarrierPartyId())) { + throw new ServiceException("所选承运商不属于该承运商合同的乙方或丙方客商"); + } + if (selectedParty != null) { + Object customerName = selectedParty.get("customerName"); + Object partyId = selectedParty.get("id"); + Object partyRole = selectedParty.get("partyRole"); + loadingManage.setCarrierName(customerName == null ? null : normalizePartyName(String.valueOf(customerName))); + loadingManage.setCarrierPartyId(partyId == null ? null : normalizePartyName(String.valueOf(partyId))); + loadingManage.setCarrierPartyRole(partyRole == null ? null : ContractPartySupport.normalizeRole(String.valueOf(partyRole))); + } else if (Func.isEmpty(loadingManage.getCarrierName())) { + loadingManage.setCarrierName(ContractPartySupport.firstPartyName(carrierContract.getPartyBJson(), carrierContract.getPartyB())); + loadingManage.setCarrierPartyRole(ContractPartySupport.ROLE_B); + } if (Func.isNotEmpty(childWaybills)) { List projectContracts = availableCarrierContracts(projectIdsOf(childWaybills)); Set allowedPartyBs = intersectCarrierPartyBs(childWaybills, projectContracts); - String partyB = normalizePartyName(carrierContract.getPartyB()); - if (Func.isNotEmpty(allowedPartyBs) && !allowedPartyBs.contains(partyB)) { - throw new ServiceException("所选承运商合同乙方与子运单可共同承运的乙方不一致,可选:" + String carrierName = normalizePartyName(loadingManage.getCarrierName()); + if (Func.isNotEmpty(allowedPartyBs) && !allowedPartyBs.contains(carrierName)) { + throw new ServiceException("所选承运客商与子运单可共同承运的客商不一致,可选:" + String.join("、", allowedPartyBs)); } if (allowedPartyBs.isEmpty()) { - throw new ServiceException("所选运单在同一项目下没有共同的承运商合同乙方,不能配载到同一配载单"); + throw new ServiceException("所选运单在同一项目下没有共同的承运商合同客商,不能配载到同一配载单"); } } - loadingManage.setCarrierName(carrierContract.getPartyB()); } private void validateCompletedCarrierContract(LoadingManage loadingManage) { @@ -1054,12 +1079,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl intersectCarrierPartyBs(List waybills, List projectContracts) { Set intersection = null; - Map customerPartyBMap = loadCustomerContractPartyBMap(waybills); + Map> customerPartyBMap = loadCustomerContractPartyBMap(waybills); for (Waybill waybill : waybills) { Set available = availableCarrierPartyBs(waybill, projectContracts, customerPartyBMap); if (intersection == null) { @@ -1075,23 +1099,23 @@ public class LoadingManageServiceImpl extends BaseServiceImpl availableCarrierPartyBs(Waybill waybill, List projectContracts, - Map customerPartyBMap) { + Map> customerPartyBMap) { if (waybill == null || Func.isEmpty(projectContracts)) { return Set.of(); } Long contractId = waybill.getContractId(); - // 运单没有客户合同时不按乙方过滤;客户合同乙方即我方,应等于承运商合同甲方 - String partyB = contractId == null ? "" : normalizePartyName(customerPartyBMap.get(contractId)); + Set customerPartyBs = contractId == null + ? Set.of() + : customerPartyBMap.getOrDefault(contractId, Set.of()); return projectContracts.stream() .filter(contract -> Objects.equals(contract.getProjectId(), waybill.getProjectId())) - .filter(contract -> Func.isEmpty(partyB) || Objects.equals(normalizePartyName(contract.getPartyA()), partyB)) - .map(ContractManage::getPartyB) - .map(this::normalizePartyName) + .filter(contract -> customerPartyBs.isEmpty() || partyNamesOverlap(partyANames(contract), customerPartyBs)) + .flatMap(contract -> ContractPartySupport.carrierPartyNames(contract).stream()) .filter(Func::isNotEmpty) .collect(Collectors.toCollection(LinkedHashSet::new)); } - private Map loadCustomerContractPartyBMap(List waybills) { + private Map> loadCustomerContractPartyBMap(List waybills) { List contractIds = waybills.stream() .map(Waybill::getContractId) .filter(Objects::nonNull) @@ -1105,11 +1129,55 @@ public class LoadingManageServiceImpl extends BaseServiceImpl normalizePartyName(contract.getPartyB()), + this::partyBNames, (left, right) -> left, LinkedHashMap::new)); } + private Set partyANames(ContractManage contract) { + Set names = ContractPartySupport.parsePartyRows(contract.getPartyAJson()).stream() + .map(row -> normalizePartyName(String.valueOf(row.get("customerName")))) + .filter(Func::isNotEmpty) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (names.isEmpty() && Func.isNotEmpty(contract.getPartyA())) { + for (String part : contract.getPartyA().split("[、,]")) { + String name = normalizePartyName(part); + if (Func.isNotEmpty(name)) { + names.add(name); + } + } + } + return names; + } + + private Set partyBNames(ContractManage contract) { + Set names = ContractPartySupport.parsePartyRows(contract.getPartyBJson()).stream() + .map(row -> normalizePartyName(String.valueOf(row.get("customerName")))) + .filter(Func::isNotEmpty) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (names.isEmpty() && Func.isNotEmpty(contract.getPartyB())) { + for (String part : contract.getPartyB().split("[、,]")) { + String name = normalizePartyName(part); + if (Func.isNotEmpty(name)) { + names.add(name); + } + } + } + return names; + } + + private boolean partyNamesOverlap(Set left, Set right) { + if (Func.isEmpty(left) || Func.isEmpty(right)) { + return false; + } + for (String name : left) { + if (right.contains(name)) { + return true; + } + } + return false; + } + private String normalizePartyName(String value) { return value == null ? "" : value.trim(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java index d459c234..d42b3d8d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -30,6 +30,7 @@ import org.springblade.transport.service.IDriverService; import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.ITransportPlanService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.ContractPartySupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.WaybillWrapper; @@ -87,11 +88,15 @@ public class MasterOrderServiceImpl extends BaseServiceImpl carriers(Long id) { - return availableCarrierContracts(getRequired(id)).stream().map(contract -> { + return ContractPartySupport.expandCarrierOptions(availableCarrierContracts(getRequired(id))).stream().map(option -> { MasterOrderCarrierVO carrier = new MasterOrderCarrierVO(); - carrier.setContractId(contract.getId()); - carrier.setContractName(contract.getContractName()); - carrier.setCarrierName(contract.getPartyB()); + carrier.setContractId(option.getId()); + carrier.setContractName(option.getContractName()); + carrier.setCarrierName(option.getCarrierName()); + carrier.setCarrierPartyId(option.getCarrierPartyId()); + carrier.setCarrierPartyRole(option.getCarrierPartyRole()); + carrier.setCreditCode(option.getCreditCode()); + carrier.setCustomerId(option.getCustomerId()); return carrier; }).toList(); } @@ -267,7 +272,11 @@ public class MasterOrderServiceImpl extends BaseServiceImpl wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) .orderByDesc(ContractManage::getCreateTime)) - .stream().filter(contract -> Func.isNotEmpty(contract.getPartyB())).toList(); + .stream().filter(contract -> Func.isNotEmpty(ContractPartySupport.carrierPartyNames(contract))).toList(); } private void validateCarrier(Map dispatch, Map availableCarrierContracts) { @@ -615,11 +624,29 @@ public class MasterOrderServiceImpl extends BaseServiceImpl selectedParty = ContractPartySupport.findCarrierParty(carrierContract, carrierPartyId, carrierName); + if (selectedParty != null) { + Object selectedName = selectedParty.get("customerName"); + Object selectedPartyId = selectedParty.get("id"); + Object selectedPartyRole = selectedParty.get("partyRole"); + if (selectedName != null) { + dispatch.put("carrierName", String.valueOf(selectedName).trim()); + } + if (selectedPartyId != null) { + dispatch.put("carrierPartyId", String.valueOf(selectedPartyId).trim()); + } + if (selectedPartyRole != null) { + dispatch.put("carrierPartyRole", ContractPartySupport.normalizeRole(String.valueOf(selectedPartyRole))); + } + } } String transportType = string(dispatch, "transportType", ""); boolean road = transportType.toLowerCase().contains("road") || transportType.contains("公路"); 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 2a3e7e1d..a0ee4769 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 @@ -61,6 +61,7 @@ import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IPreSettlementService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.ContractPartySupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper; @@ -566,6 +567,12 @@ public class ReceivablePayableDetailServiceImpl item.put("id", contract.getId()); item.put("contractNo", contract.getContractNo()); item.put("contractName", contract.getContractName()); + item.put("partyA", contract.getPartyA()); + item.put("partyB", contract.getPartyB()); + item.put("partyC", contract.getPartyC()); + item.put("partyAJson", contract.getPartyAJson()); + item.put("partyBJson", contract.getPartyBJson()); + item.put("partyCJson", contract.getPartyCJson()); item.put("billingPlanJson", contract.getBillingPlanJson()); return item; }).toList(); @@ -1258,9 +1265,7 @@ public class ReceivablePayableDetailServiceImpl && !Objects.equals(waybill.getProjectId(), contract.getProjectId())) { return false; } - String carrierName = trimToNull(waybill.getCarrierName()); - String partyB = trimToNull(contract.getPartyB()); - return carrierName != null && carrierName.equals(partyB); + return ContractPartySupport.matchesCarrier(contract, waybill.getCarrierName(), waybill.getCarrierPartyId()); } private ContractManage findCarrierContract(Waybill waybill) { @@ -1268,20 +1273,50 @@ public class ReceivablePayableDetailServiceImpl if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) { return null; } - // 承运商合同甲方应与运单绑定的客户合同乙方(我方)一致,乙方必须是运单选择的承运商。 + // 承运商合同甲方应与运单绑定的客户合同乙方(我方)一致,乙/丙方必须包含运单选择的承运商。 ContractManage customerContract = contractManageService.getById(waybill.getContractId()); if (customerContract == null || Func.isEmpty(customerContract.getPartyB())) { return null; } - return contractManageService.getOne(Wrappers.lambdaQuery() + List contracts = contractManageService.list(Wrappers.lambdaQuery() .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getProjectId, waybill.getProjectId()) - .eq(ContractManage::getPartyA, customerContract.getPartyB()) - .eq(ContractManage::getPartyB, carrierName) .eq(ContractManage::getContractCategory, "承运商合同") .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) - .orderByDesc(ContractManage::getCreateTime), false); + .orderByDesc(ContractManage::getCreateTime)); + Set customerPartyBs = splitPartyNames(customerContract.getPartyBJson(), customerContract.getPartyB()); + for (ContractManage contract : contracts) { + Set partyANames = splitPartyNames(contract.getPartyAJson(), contract.getPartyA()); + boolean partyAMatched = partyANames.isEmpty() || partyANames.stream().anyMatch(customerPartyBs::contains) + || customerPartyBs.contains(trimToNull(contract.getPartyA())); + if (!partyAMatched) { + continue; + } + if (ContractPartySupport.matchesCarrier(contract, carrierName, waybill.getCarrierPartyId())) { + return contract; + } + } + return null; + } + + private Set splitPartyNames(String partyJson, String fallback) { + Set names = new LinkedHashSet<>(); + for (Map row : ContractPartySupport.parsePartyRows(partyJson)) { + Object customerName = row.get("customerName"); + if (customerName != null && Func.isNotEmpty(String.valueOf(customerName).trim())) { + names.add(String.valueOf(customerName).trim()); + } + } + if (names.isEmpty() && Func.isNotEmpty(fallback)) { + for (String part : fallback.split("[、,]")) { + String name = trimToNull(part); + if (name != null) { + names.add(name); + } + } + } + return names; } private String trimToNull(String value) { @@ -2058,13 +2093,31 @@ public class ReceivablePayableDetailServiceImpl */ private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly, boolean strictPlan) { - List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); + List> plans = billingPlansForWaybill(contract, waybill); Map plan = "__matched__".equals(planId) ? resolveDefaultBillingPlan(plans, waybill.getTransportType()) : resolveBillingPlan(plans, planId); return calculatedFees(waybill, plan, matchOnly, strictPlan); } + /** + * 按运单客商过滤合同计费方案:承运商合同用承运客商,客户合同用客户名称。 + */ + private List> billingPlansForWaybill(ContractManage contract, Waybill waybill) { + if (contract == null) { + return List.of(); + } + if (waybill == null) { + return parseList(contract.getBillingPlanJson()); + } + if ("承运商合同".equals(contract.getContractCategory())) { + return ContractPartySupport.filterBillingPlans( + contract.getBillingPlanJson(), null, waybill.getCarrierName(), waybill.getCarrierPartyId()); + } + return ContractPartySupport.filterBillingPlans( + contract.getBillingPlanJson(), null, waybill.getCustomerName(), null); + } + private List calculatedFees(Waybill waybill, Map plan, boolean matchOnly) { return calculatedFees(waybill, plan, matchOnly, false); 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 0e10fbf3..c692583e 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 @@ -69,6 +69,7 @@ import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.ContractPartySupport; import org.springblade.transport.support.LiveProcessConfigSupport; import org.springblade.transport.support.LoadableAdmissionSupport; import org.springblade.transport.support.TransportBusinessSupport; @@ -2253,6 +2254,8 @@ public class WaybillServiceImpl extends BaseServiceImpl private void validateCarrierContract(Waybill waybill) { if ("自运".equals(waybill.getCarrierType())) { waybill.setCarrierContractId(null); + waybill.setCarrierPartyId(null); + waybill.setCarrierPartyRole(null); return; } if (Func.isEmpty(waybill.getCarrierContractId())) { @@ -2262,8 +2265,24 @@ public class WaybillServiceImpl extends BaseServiceImpl if (carrierContract == null || Objects.equals(carrierContract.getIsDeleted(), 1) || !"承运商合同".equals(carrierContract.getContractCategory()) || !Objects.equals(carrierContract.getProjectId(), waybill.getProjectId()) - || !Objects.equals(carrierContract.getPartyB(), waybill.getCarrierName())) { - throw new ServiceException("承运商合同必须属于所选项目,且合同乙方须与承运商一致"); + || !ContractPartySupport.matchesCarrier(carrierContract, waybill.getCarrierName(), waybill.getCarrierPartyId())) { + throw new ServiceException("承运商合同必须属于所选项目,且承运商须为合同乙方或丙方客商"); + } + Map selectedParty = ContractPartySupport.findCarrierParty( + carrierContract, waybill.getCarrierPartyId(), waybill.getCarrierName()); + if (selectedParty != null) { + Object customerName = selectedParty.get("customerName"); + Object partyId = selectedParty.get("id"); + Object partyRole = selectedParty.get("partyRole"); + if (customerName != null) { + waybill.setCarrierName(String.valueOf(customerName).trim()); + } + if (partyId != null) { + waybill.setCarrierPartyId(String.valueOf(partyId).trim()); + } + if (partyRole != null) { + waybill.setCarrierPartyRole(ContractPartySupport.normalizeRole(String.valueOf(partyRole))); + } } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ContractPartySupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ContractPartySupport.java new file mode 100644 index 00000000..58b638d1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ContractPartySupport.java @@ -0,0 +1,507 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.vo.LoadingCarrierContractVO; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 合同甲/乙/丙多方客商明细支持 + * + * @author Chill + */ +public final class ContractPartySupport { + + public static final String ROLE_A = "A"; + public static final String ROLE_B = "B"; + public static final String ROLE_C = "C"; + + private ContractPartySupport() { + } + + /** + * 规范化三方客商明细并回写名称汇总 + */ + public static void normalizeAndFillSummary(ContractManage contractManage) { + List> partyARows = normalizePartyRows(contractManage.getPartyAJson(), true); + List> partyBRows = normalizePartyRows(contractManage.getPartyBJson(), true); + List> partyCRows = normalizePartyRows(contractManage.getPartyCJson(), false); + contractManage.setPartyAJson(toJsonOrNull(partyARows)); + contractManage.setPartyBJson(toJsonOrNull(partyBRows)); + contractManage.setPartyCJson(toJsonOrNull(partyCRows)); + contractManage.setPartyA(joinCustomerNames(partyARows)); + contractManage.setPartyB(joinCustomerNames(partyBRows)); + contractManage.setPartyC(joinCustomerNames(partyCRows)); + } + + /** + * 提交级校验:甲/乙至少一行,计费方案必须绑定明细客商 + */ + public static void validateRequiredParties(ContractManage contractManage, boolean requireCustomerId) { + List> partyARows = parsePartyRows(contractManage.getPartyAJson()); + List> partyBRows = parsePartyRows(contractManage.getPartyBJson()); + List> partyCRows = parsePartyRows(contractManage.getPartyCJson()); + if (partyARows.isEmpty()) { + throw new ServiceException("请至少添加一个甲方客商"); + } + if (partyBRows.isEmpty()) { + throw new ServiceException("请至少添加一个乙方客商"); + } + validatePartyRows("甲方", partyARows, requireCustomerId); + validatePartyRows("乙方", partyBRows, requireCustomerId); + if (!partyCRows.isEmpty()) { + validatePartyRows("丙方", partyCRows, requireCustomerId); + } + } + + /** + * 校验计费方案均已绑定合同明细中的客商 + */ + public static void validateBillingPlanBindings(ContractManage contractManage) { + String billingPlanJson = contractManage.getBillingPlanJson(); + if (Func.isEmpty(billingPlanJson)) { + return; + } + Object parsed = JsonUtil.parse(billingPlanJson, List.class); + if (!(parsed instanceof List plans) || plans.isEmpty()) { + return; + } + Map> partyByRowId = indexPartyRows(contractManage); + Set customerKeys = partyByRowId.values().stream() + .map(ContractPartySupport::customerMatchKey) + .filter(Func::isNotEmpty) + .collect(Collectors.toSet()); + for (int index = 0; index < plans.size(); index++) { + Object value = plans.get(index); + if (!(value instanceof Map plan)) { + throw new ServiceException("计费方案设置格式不正确"); + } + String partyRowId = trimToNull(plan.get("partyRowId")); + String customerId = trimToNull(plan.get("customerId")); + String customerName = trimToNull(plan.get("customerName")); + String partyRole = normalizeRole(trimToNull(plan.get("partyRole"))); + if (Func.isEmpty(partyRowId) && Func.isEmpty(customerId) && Func.isEmpty(customerName)) { + throw new ServiceException("计费方案第" + (index + 1) + "个必须选择适用客商"); + } + boolean matched = false; + if (Func.isNotEmpty(partyRowId) && partyByRowId.containsKey(partyRowId)) { + matched = true; + } else { + String key = customerMatchKey(customerId, customerName, partyRole); + matched = Func.isNotEmpty(key) && customerKeys.contains(key); + if (!matched && Func.isNotEmpty(customerName)) { + matched = partyByRowId.values().stream() + .anyMatch(row -> Objects.equals(trimToNull(row.get("customerName")), customerName) + && (Func.isEmpty(partyRole) || Objects.equals(normalizeRole(String.valueOf(row.get("partyRole"))), partyRole))); + } + } + if (!matched) { + throw new ServiceException("计费方案第" + (index + 1) + "个适用客商不在合同甲/乙/丙明细中"); + } + } + } + + public static List> parsePartyRows(String partyJson) { + if (Func.isEmpty(partyJson)) { + return List.of(); + } + try { + Object parsed = JsonUtil.parse(partyJson, List.class); + if (!(parsed instanceof List rows) || rows.isEmpty()) { + return List.of(); + } + List> result = new ArrayList<>(); + for (Object row : rows) { + if (row instanceof Map map) { + Map values = new LinkedHashMap<>(); + map.forEach((key, value) -> values.put(String.valueOf(key), value)); + result.add(values); + } + } + return result; + } catch (Exception exception) { + return List.of(); + } + } + + public static List> carrierPartyRows(ContractManage contractManage) { + List> rows = new ArrayList<>(); + parsePartyRows(contractManage == null ? null : contractManage.getPartyBJson()).forEach(row -> { + row.put("partyRole", ROLE_B); + rows.add(row); + }); + parsePartyRows(contractManage == null ? null : contractManage.getPartyCJson()).forEach(row -> { + row.put("partyRole", ROLE_C); + rows.add(row); + }); + return rows; + } + + public static Set carrierPartyNames(ContractManage contractManage) { + Set names = new LinkedHashSet<>(); + for (Map row : carrierPartyRows(contractManage)) { + String name = normalizePartyName(trimToNull(row.get("customerName"))); + if (Func.isNotEmpty(name)) { + names.add(name); + } + } + // 兼容历史仅有汇总字段的合同 + if (names.isEmpty() && contractManage != null) { + String partyB = normalizePartyName(contractManage.getPartyB()); + if (Func.isNotEmpty(partyB)) { + names.add(partyB); + } + String partyC = normalizePartyName(contractManage.getPartyC()); + if (Func.isNotEmpty(partyC)) { + names.add(partyC); + } + } + return names; + } + + public static boolean matchesCarrier(ContractManage contractManage, String carrierName, String carrierPartyId) { + if (contractManage == null) { + return false; + } + List> rows = carrierPartyRows(contractManage); + if (Func.isNotEmpty(carrierPartyId)) { + for (Map row : rows) { + if (Objects.equals(trimToNull(row.get("id")), trimToNull(carrierPartyId))) { + return true; + } + } + } + String normalizedName = normalizePartyName(carrierName); + if (Func.isEmpty(normalizedName)) { + return false; + } + if (rows.isEmpty()) { + return Objects.equals(normalizePartyName(contractManage.getPartyB()), normalizedName) + || Objects.equals(normalizePartyName(contractManage.getPartyC()), normalizedName); + } + return rows.stream() + .anyMatch(row -> Objects.equals(normalizePartyName(trimToNull(row.get("customerName"))), normalizedName)); + } + + public static Map findCarrierParty(ContractManage contractManage, String carrierPartyId, String carrierName) { + List> rows = carrierPartyRows(contractManage); + if (Func.isNotEmpty(carrierPartyId)) { + for (Map row : rows) { + if (Objects.equals(trimToNull(row.get("id")), trimToNull(carrierPartyId))) { + return row; + } + } + } + String normalizedName = normalizePartyName(carrierName); + if (Func.isEmpty(normalizedName)) { + return null; + } + for (Map row : rows) { + if (Objects.equals(normalizePartyName(trimToNull(row.get("customerName"))), normalizedName)) { + return row; + } + } + return null; + } + + public static List> filterBillingPlans(String billingPlanJson, Long customerId, String customerName, String partyRowId) { + List> plans = parseObjectList(billingPlanJson); + if (plans.isEmpty()) { + return plans; + } + String normalizedName = normalizePartyName(customerName); + String normalizedPartyRowId = trimToNull(partyRowId); + String customerIdText = customerId == null ? null : String.valueOf(customerId); + List> matched = plans.stream() + .filter(plan -> { + if (Func.isNotEmpty(normalizedPartyRowId) + && Objects.equals(trimToNull(plan.get("partyRowId")), normalizedPartyRowId)) { + return true; + } + if (Func.isNotEmpty(customerIdText) + && Objects.equals(trimToNull(plan.get("customerId")), customerIdText)) { + return true; + } + return Func.isNotEmpty(normalizedName) + && Objects.equals(normalizePartyName(trimToNull(plan.get("customerName"))), normalizedName); + }) + .collect(Collectors.toCollection(ArrayList::new)); + // 历史方案未绑定客商时,不强制过滤,避免老数据无法计费 + if (matched.isEmpty() && plans.stream().noneMatch(ContractPartySupport::hasCustomerBinding)) { + return plans; + } + return matched; + } + + public static List expandCarrierOptions(Collection contracts) { + if (Func.isEmpty(contracts)) { + return List.of(); + } + List options = new ArrayList<>(); + for (ContractManage contract : contracts) { + if (contract == null) { + continue; + } + List> rows = carrierPartyRows(contract); + if (rows.isEmpty()) { + LoadingCarrierContractVO option = new LoadingCarrierContractVO(); + option.setId(contract.getId()); + option.setContractName(contract.getContractName()); + option.setCarrierName(contract.getPartyB()); + option.setCarrierPartyRole(ROLE_B); + options.add(option); + continue; + } + for (Map row : rows) { + LoadingCarrierContractVO option = new LoadingCarrierContractVO(); + option.setId(contract.getId()); + option.setContractName(contract.getContractName()); + option.setCarrierName(trimToNull(row.get("customerName"))); + option.setCarrierPartyId(trimToNull(row.get("id"))); + option.setCarrierPartyRole(normalizeRole(String.valueOf(row.get("partyRole")))); + option.setCreditCode(trimToNull(row.get("creditCode"))); + option.setCustomerId(toLong(row.get("customerId"))); + options.add(option); + } + } + return options; + } + + public static String normalizePartyName(String value) { + return Func.isEmpty(value) ? "" : value.trim(); + } + + public static String normalizeRole(String role) { + if (Func.isEmpty(role)) { + return null; + } + String text = role.trim().toUpperCase(); + if ("A".equals(text) || "甲方".equals(role) || "甲".equals(role)) { + return ROLE_A; + } + if ("B".equals(text) || "乙方".equals(role) || "乙".equals(role)) { + return ROLE_B; + } + if ("C".equals(text) || "丙方".equals(role) || "丙".equals(role)) { + return ROLE_C; + } + return text; + } + + public static String firstPartyName(String partyJson, String fallback) { + List> rows = parsePartyRows(partyJson); + if (!rows.isEmpty()) { + String name = trimToNull(rows.get(0).get("customerName")); + if (Func.isNotEmpty(name)) { + return name; + } + } + return TransportBusinessSupport.trimToNull(fallback); + } + + private static List> normalizePartyRows(String partyJson, boolean keepEmptyPlaceholder) { + List> source = parsePartyRows(partyJson); + List> result = new ArrayList<>(); + for (Map sourceRow : source) { + Map row = new LinkedHashMap<>(); + String id = trimToNull(sourceRow.get("id")); + if (Func.isEmpty(id)) { + id = String.valueOf(IdWorker.getId()); + } + row.put("id", id); + Long customerId = toLong(sourceRow.get("customerId")); + if (customerId != null) { + row.put("customerId", customerId); + } + String customerName = trimToNull(sourceRow.get("customerName")); + if (Func.isNotEmpty(customerName)) { + row.put("customerName", customerName); + } + String creditCode = trimToNull(sourceRow.get("creditCode")); + if (Func.isNotEmpty(creditCode)) { + row.put("creditCode", creditCode); + } + Long contactId = toLong(sourceRow.get("contactId")); + if (contactId != null) { + row.put("contactId", contactId); + } + String contactName = trimToNull(sourceRow.get("contactName")); + if (Func.isNotEmpty(contactName)) { + row.put("contactName", contactName); + } + String contactPhone = trimToNull(sourceRow.get("contactPhone")); + if (Func.isNotEmpty(contactPhone)) { + row.put("contactPhone", contactPhone); + } + if (Func.isEmpty(customerName) && customerId == null && !keepEmptyPlaceholder) { + continue; + } + result.add(row); + } + return result; + } + + private static void validatePartyRows(String roleLabel, List> rows, boolean requireCustomerId) { + Set customerKeys = new LinkedHashSet<>(); + for (int index = 0; index < rows.size(); index++) { + Map row = rows.get(index); + String customerName = trimToNull(row.get("customerName")); + Long customerId = toLong(row.get("customerId")); + if (Func.isEmpty(customerName)) { + throw new ServiceException(roleLabel + "第" + (index + 1) + "行请选择客商"); + } + if (requireCustomerId && customerId == null) { + throw new ServiceException(roleLabel + "第" + (index + 1) + "行请选择有效客商"); + } + String contactName = trimToNull(row.get("contactName")); + String contactPhone = trimToNull(row.get("contactPhone")); + if (Func.isNotEmpty(contactName) && Func.isEmpty(contactPhone)) { + throw new ServiceException(roleLabel + "第" + (index + 1) + "行联系人需带出联系电话"); + } + String key = customerId == null ? customerName : String.valueOf(customerId); + if (!customerKeys.add(key)) { + throw new ServiceException(roleLabel + "存在重复客商:" + customerName); + } + } + } + + private static Map> indexPartyRows(ContractManage contractManage) { + Map> indexed = new LinkedHashMap<>(); + putIndexedRows(indexed, parsePartyRows(contractManage.getPartyAJson()), ROLE_A); + putIndexedRows(indexed, parsePartyRows(contractManage.getPartyBJson()), ROLE_B); + putIndexedRows(indexed, parsePartyRows(contractManage.getPartyCJson()), ROLE_C); + return indexed; + } + + private static void putIndexedRows(Map> indexed, List> rows, String role) { + for (Map row : rows) { + Map copy = new LinkedHashMap<>(row); + copy.put("partyRole", role); + String id = trimToNull(copy.get("id")); + if (Func.isNotEmpty(id)) { + indexed.put(id, copy); + } + } + } + + private static String customerMatchKey(Map row) { + return customerMatchKey(trimToNull(row.get("customerId")), trimToNull(row.get("customerName")), + normalizeRole(String.valueOf(row.get("partyRole")))); + } + + private static String customerMatchKey(String customerId, String customerName, String partyRole) { + if (Func.isNotEmpty(customerId)) { + return normalizeRole(partyRole) + ":" + customerId; + } + if (Func.isNotEmpty(customerName)) { + return normalizeRole(partyRole) + ":" + customerName; + } + return null; + } + + private static boolean hasCustomerBinding(Map plan) { + return Func.isNotEmpty(trimToNull(plan.get("partyRowId"))) + || Func.isNotEmpty(trimToNull(plan.get("customerId"))) + || Func.isNotEmpty(trimToNull(plan.get("customerName"))); + } + + private static List> parseObjectList(String json) { + if (Func.isEmpty(json)) { + return List.of(); + } + try { + Object parsed = JsonUtil.parse(json, List.class); + if (!(parsed instanceof List list) || list.isEmpty()) { + return List.of(); + } + List> result = new ArrayList<>(); + for (Object item : list) { + if (item instanceof Map map) { + Map values = new LinkedHashMap<>(); + map.forEach((key, value) -> values.put(String.valueOf(key), value)); + result.add(values); + } + } + return result; + } catch (Exception exception) { + return Collections.emptyList(); + } + } + + private static String joinCustomerNames(List> rows) { + List names = rows.stream() + .map(row -> trimToNull(row.get("customerName"))) + .filter(Func::isNotEmpty) + .distinct() + .toList(); + return names.isEmpty() ? null : String.join("、", names); + } + + private static String toJsonOrNull(List> rows) { + return rows == null || rows.isEmpty() ? null : JsonUtil.toJson(rows); + } + + private static String trimToNull(Object value) { + if (value == null) { + return null; + } + String text = String.valueOf(value).trim(); + return text.isEmpty() || "null".equalsIgnoreCase(text) ? null : text; + } + + private static Long toLong(Object value) { + if (value == null) { + return null; + } + if (value instanceof Number number) { + return number.longValue(); + } + String text = trimToNull(value); + if (Func.isEmpty(text)) { + return null; + } + try { + return Long.valueOf(text); + } catch (NumberFormatException exception) { + return null; + } + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java index 30fe801d..00429ba8 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java @@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.StringUtil; import org.springblade.hcs.config.HcsProperties; import org.springblade.hcs.constant.HcsConstant; @@ -61,6 +62,7 @@ import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.HcsContractCheckLog; import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.support.ContractPartySupport; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -532,44 +534,74 @@ public class HcsContractSyncSupport { } /** - * signer 相对方(签署方):甲/乙双方。fdType 档案优先:客商档案为内部组织(customerKind=internal/内部组织) + * signer 相对方(签署方):甲/乙/丙多方客商。fdType 档案优先:客商档案为内部组织(customerKind=internal/内部组织) * 的一方为我方签约主体传 inner,另一方传 customer(外部相对方,2026-10-14 联调口径); * 双方均非内部组织或查不到档案时按合同类别推定(承运商合同甲=inner,客户合同乙=inner,其余 customer); - * 丙方(三方合同)TMS 无签约主体字段来源,暂不支持 + * 多方时按明细展开,同地位多客商分别签署 */ private List buildSigners(ContractManage contractManage) { List signers = new ArrayList<>(); - // 甲乙方各反查一次客商档案,编号与内部组织判定共用查询结果 - CustomerArchive partyAArchive = queryCustomerArchive(contractManage.getPartyA()); - CustomerArchive partyBArchive = queryCustomerArchive(contractManage.getPartyB()); + List partyANames = partyNames(contractManage.getPartyAJson(), contractManage.getPartyA()); + List partyBNames = partyNames(contractManage.getPartyBJson(), contractManage.getPartyB()); + List partyCNames = partyNames(contractManage.getPartyCJson(), contractManage.getPartyC()); + String firstPartyA = partyANames.isEmpty() ? null : partyANames.get(0); + String firstPartyB = partyBNames.isEmpty() ? null : partyBNames.get(0); + CustomerArchive partyAArchive = queryCustomerArchive(firstPartyA); + CustomerArchive partyBArchive = queryCustomerArchive(firstPartyB); boolean partyAInternal = HcsCreditCheckSupport.isInternalCustomer(partyAArchive); boolean partyBInternal = HcsCreditCheckSupport.isInternalCustomer(partyBArchive); String partyAType; String partyBType; + String partyCType; if (partyAInternal || partyBInternal) { - // 档案命中:内部组织一方=我方签约主体(inner),外部一方统一 customer partyAType = partyAInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER; partyBType = partyBInternal ? HcsConstant.SIGNER_FD_TYPE_INNER : HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; } else if ("承运商合同".equals(contractManage.getContractCategory())) { - // 兜底:档案未命中内部组织,按类别推定——我方为付款/发包方,签约主体在甲方 partyAType = HcsConstant.SIGNER_FD_TYPE_INNER; partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; } else if ("客户合同".equals(contractManage.getContractCategory())) { - // 兜底:我方为收款/承运方,签约主体在乙方 partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; partyBType = HcsConstant.SIGNER_FD_TYPE_INNER; + partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; } else { - // 兜底:其他类别双方统一 customer(2026-10-14 联调口径) partyAType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; partyBType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + partyCType = HcsConstant.SIGNER_FD_TYPE_CUSTOMER; + } + for (String partyName : partyANames) { + CustomerArchive archive = Objects.equals(partyName, firstPartyA) ? partyAArchive : queryCustomerArchive(partyName); + signers.add(signer("甲", partyAType, partyName, archive)); + } + for (String partyName : partyBNames) { + CustomerArchive archive = Objects.equals(partyName, firstPartyB) ? partyBArchive : queryCustomerArchive(partyName); + signers.add(signer("乙", partyBType, partyName, archive)); + } + for (String partyName : partyCNames) { + signers.add(signer("丙", partyCType, partyName, queryCustomerArchive(partyName))); } - // 甲方相对方:fdStanding=甲(编号按名称反查,反查不到留空,§9-U6) - signers.add(signer("甲", partyAType, contractManage.getPartyA(), partyAArchive)); - // 乙方相对方:fdStanding=乙 - signers.add(signer("乙", partyBType, contractManage.getPartyB(), partyBArchive)); return signers; } + private List partyNames(String partyJson, String fallback) { + List names = new ArrayList<>(); + for (Map row : ContractPartySupport.parsePartyRows(partyJson)) { + Object customerName = row.get("customerName"); + if (customerName != null && Func.isNotEmpty(String.valueOf(customerName).trim())) { + names.add(String.valueOf(customerName).trim()); + } + } + if (names.isEmpty() && Func.isNotEmpty(fallback)) { + for (String part : fallback.split("[、,]")) { + if (Func.isNotEmpty(part) && Func.isNotEmpty(part.trim())) { + names.add(part.trim()); + } + } + } + return names; + } + /** * 单个相对方:fdType 由签约主体落位规则决定(我方主体 inner,外部相对方 customer) */ diff --git a/doc/sql/transport/blade_contract_party_multi_20261102.sql b/doc/sql/transport/blade_contract_party_multi_20261102.sql new file mode 100644 index 00000000..6be20f99 --- /dev/null +++ b/doc/sql/transport/blade_contract_party_multi_20261102.sql @@ -0,0 +1,53 @@ +-- 合同甲/乙/丙多方客商明细 + 运单/配载承运客商明细定位字段 +-- utf8mb4_general_ci + +-- 合同管理:多方客商 JSON 与丙方汇总 +ALTER TABLE `blade_contract_manage` + ADD COLUMN `party_a_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '甲方客商明细JSON' AFTER `party_b`, + ADD COLUMN `party_b_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '乙方客商明细JSON' AFTER `party_a_json`, + ADD COLUMN `party_c_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '丙方客商明细JSON' AFTER `party_b_json`, + ADD COLUMN `party_c` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '丙方' AFTER `party_c_json`; + +-- 合同变更:同步多方字段 +ALTER TABLE `blade_contract_change` + ADD COLUMN `party_a_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '甲方客商明细JSON' AFTER `party_b`, + ADD COLUMN `party_b_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '乙方客商明细JSON' AFTER `party_a_json`, + ADD COLUMN `party_c_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '丙方客商明细JSON' AFTER `party_b_json`, + ADD COLUMN `party_c` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '丙方' AFTER `party_c_json`; + +-- 历史数据迁移:原甲/乙方名称生成单行明细(仅名称,后续编辑补全客商ID) +UPDATE `blade_contract_manage` +SET `party_a_json` = CASE + WHEN `party_a` IS NOT NULL AND TRIM(`party_a`) <> '' AND (`party_a_json` IS NULL OR TRIM(`party_a_json`) = '') + THEN CONCAT('[{"id":"', REPLACE(UUID(), '-', ''), '","customerName":"', REPLACE(REPLACE(`party_a`, '\\', '\\\\'), '"', '\\"'), '"}]') + ELSE `party_a_json` +END, +`party_b_json` = CASE + WHEN `party_b` IS NOT NULL AND TRIM(`party_b`) <> '' AND (`party_b_json` IS NULL OR TRIM(`party_b_json`) = '') + THEN CONCAT('[{"id":"', REPLACE(UUID(), '-', ''), '","customerName":"', REPLACE(REPLACE(`party_b`, '\\', '\\\\'), '"', '\\"'), '"}]') + ELSE `party_b_json` +END +WHERE `is_deleted` = 0; + +UPDATE `blade_contract_change` +SET `party_a_json` = CASE + WHEN `party_a` IS NOT NULL AND TRIM(`party_a`) <> '' AND (`party_a_json` IS NULL OR TRIM(`party_a_json`) = '') + THEN CONCAT('[{"id":"', REPLACE(UUID(), '-', ''), '","customerName":"', REPLACE(REPLACE(`party_a`, '\\', '\\\\'), '"', '\\"'), '"}]') + ELSE `party_a_json` +END, +`party_b_json` = CASE + WHEN `party_b` IS NOT NULL AND TRIM(`party_b`) <> '' AND (`party_b_json` IS NULL OR TRIM(`party_b_json`) = '') + THEN CONCAT('[{"id":"', REPLACE(UUID(), '-', ''), '","customerName":"', REPLACE(REPLACE(`party_b`, '\\', '\\\\'), '"', '\\"'), '"}]') + ELSE `party_b_json` +END +WHERE `is_deleted` = 0; + +-- 运单:承运客商明细定位 +ALTER TABLE `blade_waybill` + ADD COLUMN `carrier_party_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承运客商明细行ID' AFTER `carrier_contract_id`, + ADD COLUMN `carrier_party_role` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承运客商角色 B/C' AFTER `carrier_party_id`; + +-- 配载:承运客商明细定位 +ALTER TABLE `blade_loading_manage` + ADD COLUMN `carrier_party_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承运客商明细行ID' AFTER `carrier_contract_id`, + ADD COLUMN `carrier_party_role` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承运客商角色 B/C' AFTER `carrier_party_id`;