✨ 合同支持甲乙丙多方客商及分客商计费

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 16:46:31 +08:00
parent 0b2348f8fc
commit 91dddd0e02
16 changed files with 930 additions and 66 deletions
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
}
@@ -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;
}
@@ -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("结束日期")
@@ -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<ContractChangeMap
target.setSignType(source.getSignType());
target.setContractNo(source.getContractNo());
target.setContractName(source.getContractName());
target.setPartyB(source.getPartyB());
target.setPartyAJson(source.getPartyAJson());
target.setPartyBJson(source.getPartyBJson());
target.setPartyCJson(source.getPartyCJson());
ContractManage partyHolder = new ContractManage();
partyHolder.setPartyAJson(target.getPartyAJson());
partyHolder.setPartyBJson(target.getPartyBJson());
partyHolder.setPartyCJson(target.getPartyCJson());
ContractPartySupport.normalizeAndFillSummary(partyHolder);
target.setPartyAJson(partyHolder.getPartyAJson());
target.setPartyBJson(partyHolder.getPartyBJson());
target.setPartyCJson(partyHolder.getPartyCJson());
target.setPartyA(partyHolder.getPartyA());
target.setPartyB(partyHolder.getPartyB());
target.setPartyC(partyHolder.getPartyC());
target.setStartDate(source.getStartDate());
target.setEndDate(source.getEndDate());
target.setContractFormat(source.getContractFormat());
@@ -341,7 +355,12 @@ public class ContractChangeServiceImpl extends BaseServiceImpl<ContractChangeMap
data.put("contractNo", contractManage.getContractNo());
data.put("contractName", contractManage.getContractName());
data.put("signType", contractManage.getSignType());
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());
@@ -47,6 +47,7 @@ import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.ContractPartySupport;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.wrapper.ContractManageWrapper;
@@ -403,7 +404,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
request.setSignType(change.getSignType());
request.setContractNo(change.getContractNo());
request.setContractName(change.getContractName());
request.setPartyB(change.getPartyB());
request.setPartyAJson(change.getPartyAJson());
request.setPartyBJson(change.getPartyBJson());
request.setPartyCJson(change.getPartyCJson());
ContractPartySupport.normalizeAndFillSummary(request);
request.setStartDate(change.getStartDate());
request.setEndDate(change.getEndDate());
request.setContractFormat(change.getContractFormat());
@@ -464,13 +468,23 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate);
candidate.setPartyB(TransportBusinessSupport.trimToNull(request.getPartyB()));
candidate.setPartyAJson(request.getPartyAJson());
candidate.setPartyBJson(request.getPartyBJson());
candidate.setPartyCJson(request.getPartyCJson());
ContractPartySupport.normalizeAndFillSummary(candidate);
ContractPartySupport.validateRequiredParties(candidate, true);
ContractPartySupport.validateBillingPlanBindings(candidate);
validateContractPartyCombinationUnique(candidate);
validatePaymentRatios(request.getPaymentRatioJson());
applyContractNo(source, request.getContractNo(), false);
source.setContractName(request.getContractName());
source.setSignType(request.getSignType());
source.setPartyB(request.getPartyB());
source.setPartyAJson(candidate.getPartyAJson());
source.setPartyBJson(candidate.getPartyBJson());
source.setPartyCJson(candidate.getPartyCJson());
source.setPartyA(candidate.getPartyA());
source.setPartyB(candidate.getPartyB());
source.setPartyC(candidate.getPartyC());
source.setStartDate(request.getStartDate());
source.setEndDate(request.getEndDate());
source.setContractFormat(request.getContractFormat());
@@ -947,8 +961,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setOrganizationName(TransportBusinessSupport.trimToNull(contractManage.getOrganizationName()));
contractManage.setContractCategory(TransportBusinessSupport.trimToNull(contractManage.getContractCategory()));
contractManage.setSignType(TransportBusinessSupport.trimToNull(contractManage.getSignType()));
contractManage.setPartyA(TransportBusinessSupport.trimToNull(contractManage.getPartyA()));
contractManage.setPartyB(TransportBusinessSupport.trimToNull(contractManage.getPartyB()));
contractManage.setPartyAJson(TransportBusinessSupport.trimToNull(contractManage.getPartyAJson()));
contractManage.setPartyBJson(TransportBusinessSupport.trimToNull(contractManage.getPartyBJson()));
contractManage.setPartyCJson(TransportBusinessSupport.trimToNull(contractManage.getPartyCJson()));
ContractPartySupport.normalizeAndFillSummary(contractManage);
contractManage.setSettlementMode(TransportBusinessSupport.trimToNull(contractManage.getSettlementMode()));
contractManage.setContractFormat(TransportBusinessSupport.trimToNull(contractManage.getContractFormat()));
contractManage.setRemark(TransportBusinessSupport.trimToNull(contractManage.getRemark()));
@@ -1005,6 +1021,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
// 合同名称允许重复,唯一性由合同编号保证。
TransportBusinessSupport.validateLength(contractManage.getMainContent(), 1000, "合同主要内容不能超过1000字");
TransportBusinessSupport.validateLength(contractManage.getRemark(), 2000, "其他事项说明不能超过2000字");
// 草稿不强制甲/乙客商与计费绑定,提交临时/正式时再完整校验
}
private void validateBillingPlans(String billingPlanJson) {
@@ -1146,8 +1163,8 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类型");
TransportBusinessSupport.validateRequired(contractManage.getProjectName(), "请选择所属项目");
TransportBusinessSupport.validateRequired(contractManage.getOrganizationName(), "请选择合同负责部门");
TransportBusinessSupport.validateRequired(contractManage.getPartyA(), "请选择甲方");
TransportBusinessSupport.validateRequired(contractManage.getPartyB(), "请选择乙方");
ContractPartySupport.validateRequiredParties(contractManage, true);
ContractPartySupport.validateBillingPlanBindings(contractManage);
validateContractAmountRequired(contractManage);
}
@@ -1298,7 +1315,12 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
Map<String, Object> 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());
@@ -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<LoadingManageMappe
return List.of();
}
List<ContractManage> contracts = availableCarrierContracts(projectIds);
Set<String> allowedCarrierNames = null;
if (Func.isNotEmpty(waybillIds)) {
List<Waybill> waybills = loadWaybills(waybillIds);
Set<String> allowedPartyBs = intersectCarrierPartyBs(waybills, contracts);
if (allowedPartyBs.isEmpty()) {
allowedCarrierNames = intersectCarrierPartyBs(waybills, contracts);
if (allowedCarrierNames.isEmpty()) {
return List.of();
}
Set<String> 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<LoadingCarrierContractVO> options = ContractPartySupport.expandCarrierOptions(contracts);
if (allowedCarrierNames == null) {
return options;
}
Set<String> allowedNames = allowedCarrierNames;
return options.stream()
.filter(option -> allowedNames.contains(normalizePartyName(option.getCarrierName())))
.toList();
}
@Override
@@ -973,6 +978,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
private void validateCarrierContract(LoadingManage loadingManage, boolean required, List<Waybill> 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<LoadingManageMappe
throw new ServiceException("请选择承运商合同");
}
loadingManage.setCarrierName(null);
loadingManage.setCarrierPartyId(null);
loadingManage.setCarrierPartyRole(null);
return;
}
ContractManage carrierContract = availableCarrierContractById(loadingManage.getCarrierContractId());
@@ -1003,19 +1012,35 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
? "所选承运商合同不属于本项目"
: "所选承运商合同不属于运单" + waybillNos + "的项目");
}
Map<String, Object> 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<ContractManage> projectContracts = availableCarrierContracts(projectIdsOf(childWaybills));
Set<String> 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<LoadingManageMappe
}
/**
* 各子运单按「客户合同乙方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
* 例:客户合同 盛达(甲)-捷联(乙),承运商合同 捷联(甲)-驰远(乙);子单1可选 A/B/C,子单2可选 B,则交集为 B。
* 各子运单按「客户合同乙方 ≈ 承运商合同甲方」匹配可用承运商合同,再对乙/丙方客商名称取交集。
*/
private Set<String> intersectCarrierPartyBs(List<Waybill> waybills, List<ContractManage> projectContracts) {
Set<String> intersection = null;
Map<Long, String> customerPartyBMap = loadCustomerContractPartyBMap(waybills);
Map<Long, Set<String>> customerPartyBMap = loadCustomerContractPartyBMap(waybills);
for (Waybill waybill : waybills) {
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyBMap);
if (intersection == null) {
@@ -1075,23 +1099,23 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
private Set<String> availableCarrierPartyBs(Waybill waybill, List<ContractManage> projectContracts,
Map<Long, String> customerPartyBMap) {
Map<Long, Set<String>> customerPartyBMap) {
if (waybill == null || Func.isEmpty(projectContracts)) {
return Set.of();
}
Long contractId = waybill.getContractId();
// 运单没有客户合同时不按乙方过滤;客户合同乙方即我方,应等于承运商合同甲方
String partyB = contractId == null ? "" : normalizePartyName(customerPartyBMap.get(contractId));
Set<String> 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<Long, String> loadCustomerContractPartyBMap(List<Waybill> waybills) {
private Map<Long, Set<String>> loadCustomerContractPartyBMap(List<Waybill> waybills) {
List<Long> contractIds = waybills.stream()
.map(Waybill::getContractId)
.filter(Objects::nonNull)
@@ -1105,11 +1129,55 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.in(ContractManage::getId, contractIds))
.stream()
.collect(Collectors.toMap(ContractManage::getId,
contract -> normalizePartyName(contract.getPartyB()),
this::partyBNames,
(left, right) -> left,
LinkedHashMap::new));
}
private Set<String> partyANames(ContractManage contract) {
Set<String> 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<String> partyBNames(ContractManage contract) {
Set<String> 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<String> left, Set<String> 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();
}
@@ -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<MasterOrderMapper, M
@Override
public List<MasterOrderCarrierVO> 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<MasterOrderMapper, M
waybill.setContractId(masterOrder.getContractId()); waybill.setContractName(masterOrder.getContractName());
waybill.setCustomerName(masterOrder.getCustomerName()); waybill.setMasterNo(masterOrder.getMasterNo());
waybill.setTransportType(string(dispatch, "transportType")); waybill.setCarrierType(string(dispatch, "carrierType"));
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId")); waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
waybill.setCarrierContractId(longValue(dispatch, "carrierContractId"));
waybill.setCarrierName(string(dispatch, "carrierName"));
waybill.setCarrierPartyId(string(dispatch, "carrierPartyId"));
waybill.setCarrierPartyRole(string(dispatch, "carrierPartyRole"));
waybill.setDriverName(string(dispatch, "driverName"));
waybill.setDriverPhone(string(dispatch, "driverPhone")); waybill.setVehicleNo(string(dispatch, "vehicleNo")); waybill.setTrailerVehicleNo(string(dispatch, "trailerVehicleNo"));
// 归属主键是档案 ID:调度带了就用它,否则按手机号解析;解析入口只返回 null 或唯一命中的
// 档案 ID,承运商司机解析为空也照常派单,落空时留 WARN
@@ -603,7 +612,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
.and(wrapper -> 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<String, Object> dispatch, Map<Long, ContractManage> availableCarrierContracts) {
@@ -615,11 +624,29 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
}
}
String carrierName = string(dispatch, "carrierName");
String carrierPartyId = string(dispatch, "carrierPartyId");
Long carrierContractId = longValue(dispatch, "carrierContractId");
ContractManage carrierContract = carrierContractId == null ? null : availableCarrierContracts.get(carrierContractId);
if ("承运商".equals(carrierType) && (carrierContract == null
|| !Objects.equals(carrierContract.getPartyB(), carrierName))) {
throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙方");
|| !ContractPartySupport.matchesCarrier(carrierContract, carrierName, carrierPartyId))) {
throw new ServiceException("所选承运商不属于总单客户合同对应项目的有效承运商合同乙/丙方客商");
}
if ("承运商".equals(carrierType) && carrierContract != null) {
Map<String, Object> 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("公路");
@@ -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.<ContractManage>lambdaQuery()
List<ContractManage> contracts = contractManageService.list(Wrappers.<ContractManage>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<String> customerPartyBs = splitPartyNames(customerContract.getPartyBJson(), customerContract.getPartyB());
for (ContractManage contract : contracts) {
Set<String> 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<String> splitPartyNames(String partyJson, String fallback) {
Set<String> names = new LinkedHashSet<>();
for (Map<String, Object> 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<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, ContractManage contract, String planId,
boolean matchOnly, boolean strictPlan) {
List<Map<String, Object>> plans = parseList(contract == null ? null : contract.getBillingPlanJson());
List<Map<String, Object>> plans = billingPlansForWaybill(contract, waybill);
Map<String, Object> plan = "__matched__".equals(planId)
? resolveDefaultBillingPlan(plans, waybill.getTransportType())
: resolveBillingPlan(plans, planId);
return calculatedFees(waybill, plan, matchOnly, strictPlan);
}
/**
* 按运单客商过滤合同计费方案:承运商合同用承运客商,客户合同用客户名称。
*/
private List<Map<String, Object>> 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<ReceivablePayableCargoFee> calculatedFees(Waybill waybill, Map<String, Object> plan,
boolean matchOnly) {
return calculatedFees(waybill, plan, matchOnly, false);
@@ -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<WaybillMapper, Waybill>
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<WaybillMapper, Waybill>
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<String, Object> 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)));
}
}
}
@@ -0,0 +1,507 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* 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<Map<String, Object>> partyARows = normalizePartyRows(contractManage.getPartyAJson(), true);
List<Map<String, Object>> partyBRows = normalizePartyRows(contractManage.getPartyBJson(), true);
List<Map<String, Object>> 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<Map<String, Object>> partyARows = parsePartyRows(contractManage.getPartyAJson());
List<Map<String, Object>> partyBRows = parsePartyRows(contractManage.getPartyBJson());
List<Map<String, Object>> 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<String, Map<String, Object>> partyByRowId = indexPartyRows(contractManage);
Set<String> 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<Map<String, Object>> 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<Map<String, Object>> result = new ArrayList<>();
for (Object row : rows) {
if (row instanceof Map<?, ?> map) {
Map<String, Object> 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<Map<String, Object>> carrierPartyRows(ContractManage contractManage) {
List<Map<String, Object>> 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<String> carrierPartyNames(ContractManage contractManage) {
Set<String> names = new LinkedHashSet<>();
for (Map<String, Object> 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<Map<String, Object>> rows = carrierPartyRows(contractManage);
if (Func.isNotEmpty(carrierPartyId)) {
for (Map<String, Object> 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<String, Object> findCarrierParty(ContractManage contractManage, String carrierPartyId, String carrierName) {
List<Map<String, Object>> rows = carrierPartyRows(contractManage);
if (Func.isNotEmpty(carrierPartyId)) {
for (Map<String, Object> 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<String, Object> row : rows) {
if (Objects.equals(normalizePartyName(trimToNull(row.get("customerName"))), normalizedName)) {
return row;
}
}
return null;
}
public static List<Map<String, Object>> filterBillingPlans(String billingPlanJson, Long customerId, String customerName, String partyRowId) {
List<Map<String, Object>> 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<Map<String, Object>> 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<LoadingCarrierContractVO> expandCarrierOptions(Collection<ContractManage> contracts) {
if (Func.isEmpty(contracts)) {
return List.of();
}
List<LoadingCarrierContractVO> options = new ArrayList<>();
for (ContractManage contract : contracts) {
if (contract == null) {
continue;
}
List<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> normalizePartyRows(String partyJson, boolean keepEmptyPlaceholder) {
List<Map<String, Object>> source = parsePartyRows(partyJson);
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> sourceRow : source) {
Map<String, Object> 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<Map<String, Object>> rows, boolean requireCustomerId) {
Set<String> customerKeys = new LinkedHashSet<>();
for (int index = 0; index < rows.size(); index++) {
Map<String, Object> 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<String, Map<String, Object>> indexPartyRows(ContractManage contractManage) {
Map<String, Map<String, Object>> 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<String, Map<String, Object>> indexed, List<Map<String, Object>> rows, String role) {
for (Map<String, Object> row : rows) {
Map<String, Object> 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<String, Object> 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<String, Object> plan) {
return Func.isNotEmpty(trimToNull(plan.get("partyRowId")))
|| Func.isNotEmpty(trimToNull(plan.get("customerId")))
|| Func.isNotEmpty(trimToNull(plan.get("customerName")));
}
private static List<Map<String, Object>> 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<Map<String, Object>> result = new ArrayList<>();
for (Object item : list) {
if (item instanceof Map<?, ?> map) {
Map<String, Object> 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<Map<String, Object>> rows) {
List<String> 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<Map<String, Object>> 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;
}
}
}
@@ -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<HcsSignerDTO> buildSigners(ContractManage contractManage) {
List<HcsSignerDTO> signers = new ArrayList<>();
// 甲乙方各反查一次客商档案,编号与内部组织判定共用查询结果
CustomerArchive partyAArchive = queryCustomerArchive(contractManage.getPartyA());
CustomerArchive partyBArchive = queryCustomerArchive(contractManage.getPartyB());
List<String> partyANames = partyNames(contractManage.getPartyAJson(), contractManage.getPartyA());
List<String> partyBNames = partyNames(contractManage.getPartyBJson(), contractManage.getPartyB());
List<String> 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<String> partyNames(String partyJson, String fallback) {
List<String> names = new ArrayList<>();
for (Map<String, Object> 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)
*/
@@ -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`;