🐛 修复项目部门可见性、费用项翻倍与配载合同匹配

业务部门变更后本部门可查项目;清洗 freightJson 误写入的费用项并修正预结算合计;配载承运商按客户合同乙方匹配。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-29 14:05:00 +08:00
parent 7ef3b95aab
commit ef05534114
9 changed files with 324 additions and 41 deletions
@@ -56,7 +56,7 @@ public class LoadingManageController extends BladeController {
@GetMapping("/carrier-contracts")
@ApiOperationSupport(order = 13)
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同甲方匹配后对乙方取交集")
@Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同乙方=承运商合同甲方匹配后对乙方取交集")
public R<List<LoadingCarrierContractVO>> carrierContracts(
@RequestParam List<Long> projectIds,
@RequestParam(required = false) List<Long> waybillIds) {
@@ -29,7 +29,7 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
/**
* 查询项目下已审核生效的承运商合同。
* <p>
* 传入 waybillIds 时,按各子运单客户合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
* 传入 waybillIds 时,按各子运单客户合同乙方=承运商合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。
*/
List<LoadingCarrierContractVO> carrierContracts(List<Long> projectIds, List<Long> waybillIds);
@@ -97,6 +97,7 @@ import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.KingdeeArBillSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
@@ -1878,7 +1879,29 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
try {
Object parsed = JsonUtil.parse(json, Map.class);
if (parsed instanceof Map<?, ?> map) {
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
BigDecimal otherFeeAmount = BigDecimal.ZERO;
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
Object value = entry.getValue();
if (Func.isBlank(key)) {
continue;
}
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
otherFeeAmount = otherFeeAmount.add(decimal(value));
continue;
}
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
continue;
}
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
continue;
}
result.put(key, decimal(value));
}
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
result.put("其它费用", otherFeeAmount);
}
}
} catch (RuntimeException ignored) {
// 兼容历史费用JSON,不影响正式结算保存。
@@ -907,7 +907,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
/**
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同甲方匹配后取交集)。
* 同一配载单子运单除同项目外,还必须存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集)。
*/
private void validateCompatibleCarrierPartyB(List<Waybill> waybills) {
if (Func.isEmpty(waybills) || waybills.size() < 2) {
@@ -1008,14 +1008,14 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
/**
* 各子运单按「客户合同甲方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
* 例:子单1可选 A/B/C,子单2可选 B,则交集为 B。
* 各子运单按「客户合同乙方 = 承运商合同甲方」匹配可用承运商合同,再对乙方名称取交集。
* 例:客户合同 盛达(甲)-捷联(乙),承运商合同 捷联(甲)-驰远(乙);子单1可选 A/B/C,子单2可选 B,则交集为 B。
*/
private Set<String> intersectCarrierPartyBs(List<Waybill> waybills, List<ContractManage> projectContracts) {
Set<String> intersection = null;
Map<Long, String> customerPartyAMap = loadCustomerContractPartyAMap(waybills);
Map<Long, String> customerPartyBMap = loadCustomerContractPartyBMap(waybills);
for (Waybill waybill : waybills) {
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyAMap);
Set<String> available = availableCarrierPartyBs(waybill, projectContracts, customerPartyBMap);
if (intersection == null) {
intersection = new LinkedHashSet<>(available);
} else {
@@ -1029,21 +1029,22 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
private Set<String> availableCarrierPartyBs(Waybill waybill, List<ContractManage> projectContracts,
Map<Long, String> customerPartyAMap) {
Map<Long, String> customerPartyBMap) {
if (waybill == null || Func.isEmpty(projectContracts)) {
return Set.of();
}
String partyA = customerPartyAMap.get(waybill.getContractId());
// 客户合同乙方即我方,应等于承运商合同甲方
String partyB = customerPartyBMap.get(waybill.getContractId());
return projectContracts.stream()
.filter(contract -> Objects.equals(contract.getProjectId(), waybill.getProjectId()))
.filter(contract -> Func.isEmpty(partyA) || Objects.equals(normalizePartyName(contract.getPartyA()), partyA))
.filter(contract -> Func.isEmpty(partyB) || Objects.equals(normalizePartyName(contract.getPartyA()), partyB))
.map(ContractManage::getPartyB)
.map(this::normalizePartyName)
.filter(Func::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private Map<Long, String> loadCustomerContractPartyAMap(List<Waybill> waybills) {
private Map<Long, String> loadCustomerContractPartyBMap(List<Waybill> waybills) {
List<Long> contractIds = waybills.stream()
.map(Waybill::getContractId)
.filter(Objects::nonNull)
@@ -1057,7 +1058,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.in(ContractManage::getId, contractIds))
.stream()
.collect(Collectors.toMap(ContractManage::getId,
contract -> normalizePartyName(contract.getPartyA()),
contract -> normalizePartyName(contract.getPartyB()),
(left, right) -> left,
LinkedHashMap::new));
}
@@ -70,6 +70,7 @@ import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.wrapper.PreSettlementWrapper;
import org.springblade.system.cache.UserCache;
import org.springblade.system.feign.ISysClient;
@@ -919,11 +920,12 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
detail.setBatchNo(source.getBatchNo());
detail.setUnitPrice(source.getUnitPrice());
detail.setFreightAmount(money(source.getFreightAmount()));
detail.setFeeItemsJson(source.getFeeItemsJson());
detail.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
BigDecimal originalAmount = sourceOriginalAmount(source);
BigDecimal settlementAmount = resolveSourceSettlementAmount(source, originalAmount);
detail.setOriginalAmount(originalAmount);
detail.setAdjustAmount(money(source.getTotalAmount()).subtract(originalAmount));
detail.setSettlementAmountTax(money(source.getTotalAmount()));
detail.setAdjustAmount(settlementAmount.subtract(originalAmount));
detail.setSettlementAmountTax(settlementAmount);
detail.setCurrency(Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency());
detail.setRemark(source.getRemark());
Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId());
@@ -950,10 +952,13 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
.eq(ReceivablePayableCargoFee::getDetailId, source.getId())
.eq(ReceivablePayableCargoFee::getIsDeleted, 0));
if (sourceFees.isEmpty() || sourceFees.stream().noneMatch(item -> item.getOriginalAmount() != null)) {
return money(source.getTotalAmount());
return resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
}
return sourceFees.stream().map(ReceivablePayableCargoFee::getOriginalAmount)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = BigDecimal.ZERO;
for (ReceivablePayableCargoFee sourceFee : sourceFees) {
total = total.add(resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount()));
}
return money(total);
}
private void copySourceFees(PreSettlementDetail detail, ReceivablePayableDetail source) {
@@ -973,10 +978,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
fee.setMileage(source.getMileage());
fee.setUnitPrice(source.getUnitPrice());
fee.setFreightAmount(money(source.getFreightAmount()));
fee.setFeeItemsJson(source.getFeeItemsJson());
fee.setOriginalAmount(money(source.getTotalAmount()));
fee.setFeeItemsJson(sanitizeFeeItemsJson(source.getFeeItemsJson()));
BigDecimal amount = resolveSourceSettlementAmount(source, money(source.getTotalAmount()));
fee.setOriginalAmount(amount);
fee.setAdjustAmount(BigDecimal.ZERO.setScale(2));
fee.setSettlementAmountTax(money(source.getTotalAmount()));
fee.setSettlementAmountTax(amount);
fee.setRemark(source.getRemark());
detailFeeMapper.insert(fee);
return;
@@ -987,6 +993,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
fee.setId(null);
fee.setPreSettlementDetailId(detail.getId());
fee.setSourceFeeId(sourceFee.getId());
fee.setFeeItemsJson(sanitizeFeeItemsJson(sourceFee.getFeeItemsJson()));
// 历史应付费用可能没有 after_amount,不能在首次打开明细调整时被当成 0。
BigDecimal settlementAmount = sourceFee.getAfterAmount() != null
? money(sourceFee.getAfterAmount())
@@ -994,8 +1001,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
? money(source.getTotalAmount())
: sourceFee.getOriginalAmount() != null
? money(sourceFee.getOriginalAmount()) : BigDecimal.ZERO.setScale(2);
settlementAmount = resolveCargoFeeAmount(sourceFee, settlementAmount);
BigDecimal originalAmount = sourceFee.getOriginalAmount() == null
? settlementAmount : money(sourceFee.getOriginalAmount());
? settlementAmount : resolveCargoFeeAmount(sourceFee, sourceFee.getOriginalAmount());
fee.setOriginalAmount(originalAmount);
fee.setAdjustAmount(settlementAmount.subtract(originalAmount));
fee.setSettlementAmountTax(settlementAmount);
@@ -1012,14 +1020,23 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
.filter(row -> !Integer.valueOf(1).equals(row.getManualFlag()))
.collect(Collectors.toMap(this::summaryKey, Function.identity(), (left, right) -> left));
Map<String, BigDecimal> aggregates = new LinkedHashMap<>();
List<Long> detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList();
List<PreSettlementDetail> details = listDetails(settlementId);
List<Long> detailIds = details.stream().map(PreSettlementDetail::getId).toList();
List<PreSettlementDetailFee> feeRows = detailIds.isEmpty() ? List.of()
: detailFeeMapper.selectList(Wrappers.<PreSettlementDetailFee>lambdaQuery()
.in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds)
.eq(PreSettlementDetailFee::getIsDeleted, 0)
.orderByAsc(PreSettlementDetailFee::getCreateTime));
Set<Long> repairedDetailIds = new LinkedHashSet<>();
for (PreSettlementDetailFee feeRow : feeRows) {
boolean polluted = isPollutedFeeItemsJson(feeRow.getFeeItemsJson());
Map<String, BigDecimal> feeItems = parseFeeItems(feeRow.getFeeItemsJson());
if (polluted) {
repairDetailFeeAmounts(feeRow, feeItems);
if (feeRow.getPreSettlementDetailId() != null) {
repairedDetailIds.add(feeRow.getPreSettlementDetailId());
}
}
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
BigDecimal componentAmount = feeItems.values().stream().map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -1035,8 +1052,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
summaryKey(feeTypeMap.getOrDefault(feeItem,
""), feeItem),
money(amount), BigDecimal::add));
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax())
.subtract(componentAmount));
// 脏数据已在上面修复结算金额;正常明细才用 settlementAmountTax 与费用项差额补「其他费用」
BigDecimal residualAmount = money(money(feeRow.getSettlementAmountTax()).subtract(componentAmount));
if (residualAmount.signum() != 0) {
aggregates.merge(
summaryKey(feeTypeMap.getOrDefault("其他费用", ""), "其他费用"),
@@ -1083,6 +1100,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
summaryFeeMapper.insert(row);
}
renumberSummaryFees(settlementId);
if (!repairedDetailIds.isEmpty()) {
details.stream()
.filter(detail -> repairedDetailIds.contains(detail.getId()))
.forEach(this::refreshDetail);
}
}
private void applySummaryRequest(Long settlementId, List<PreSettlementSaveRequest.SummaryFee> requestRows) {
@@ -1684,7 +1706,29 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
try {
Object parsed = JsonUtil.parse(json, Map.class);
if (parsed instanceof Map<?, ?> map) {
map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value)));
BigDecimal otherFeeAmount = BigDecimal.ZERO;
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
Object value = entry.getValue();
if (Func.isBlank(key)) {
continue;
}
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
otherFeeAmount = otherFeeAmount.add(decimal(value));
continue;
}
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
continue;
}
if (value instanceof Map<?, ?> || value instanceof java.util.Collection<?>) {
continue;
}
result.put(key, decimal(value));
}
if (otherFeeAmount.signum() > 0 && result.keySet().stream()
.noneMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")))) {
result.put("其它费用", otherFeeAmount);
}
}
} catch (Exception ignored) {
// 历史脏数据不影响结算单展示。
@@ -1692,6 +1736,74 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
return result;
}
private boolean isPollutedFeeItemsJson(String json) {
if (Func.isEmpty(json)) {
return false;
}
try {
Object parsed = JsonUtil.parse(json, Map.class);
if (!(parsed instanceof Map<?, ?> map)) {
return false;
}
return map.keySet().stream()
.map(String::valueOf)
.anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
} catch (Exception ignored) {
return false;
}
}
private String sanitizeFeeItemsJson(String json) {
Map<String, BigDecimal> feeItems = parseFeeItems(json);
return JsonUtil.toJson(feeItems);
}
/**
* 来源应付明细金额:若 fee_items_json 混入 freightJson 结构字段,按清洗后费用项重算,避免转预结算翻倍。
*/
private BigDecimal resolveSourceSettlementAmount(ReceivablePayableDetail source, BigDecimal fallback) {
if (source == null) {
return money(fallback);
}
if (!isPollutedFeeItemsJson(source.getFeeItemsJson())) {
return money(fallback);
}
Map<String, BigDecimal> feeItems = parseFeeItems(source.getFeeItemsJson());
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return money(containsFreight ? feeItemTotal : money(source.getFreightAmount()).add(feeItemTotal));
}
private BigDecimal resolveCargoFeeAmount(ReceivablePayableCargoFee sourceFee, BigDecimal fallback) {
if (sourceFee == null) {
return money(fallback);
}
if (!isPollutedFeeItemsJson(sourceFee.getFeeItemsJson())) {
return money(fallback);
}
Map<String, BigDecimal> feeItems = parseFeeItems(sourceFee.getFeeItemsJson());
boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
BigDecimal feeItemTotal = feeItems.values().stream().map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return money(containsFreight ? feeItemTotal : money(sourceFee.getFreightAmount()).add(feeItemTotal));
}
/** 重建合计时顺带修复已落库的脏费用行金额与 fee_items_json。 */
private void repairDetailFeeAmounts(PreSettlementDetailFee feeRow, Map<String, BigDecimal> sanitizedFeeItems) {
boolean containsFreight = sanitizedFeeItems.keySet().stream().anyMatch(this::isFreightFeeItem);
BigDecimal feeItemTotal = sanitizedFeeItems.values().stream().map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal corrected = money(containsFreight
? feeItemTotal
: money(feeRow.getFreightAmount()).add(feeItemTotal));
feeRow.setFeeItemsJson(JsonUtil.toJson(sanitizedFeeItems));
feeRow.setOriginalAmount(corrected);
feeRow.setSettlementAmountTax(corrected);
feeRow.setAdjustAmount(BigDecimal.ZERO.setScale(2));
detailFeeMapper.updateById(feeRow);
}
private Map<String, BigDecimal> normalizeFeeItems(Map<String, BigDecimal> feeItems) {
Map<String, BigDecimal> result = new LinkedHashMap<>();
if (feeItems == null) return result;
@@ -417,11 +417,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
applyUndertakeDeptContains(queryWrapper, projectApply.getUndertakeDeptId());
}
} else if (!Objects.equals(projectApply.getAllDept(), 1)) {
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
Long currentUserId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> {
if (currentDeptId != null) {
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
wrapper.eq(ProjectApply::getBusinessDeptId, currentDeptId)
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
}
wrapper.or().eq(ProjectApply::getCreateUser, currentUserId)
.or().eq(ProjectApply::getHandlerUserId, currentUserId)
@@ -1091,7 +1093,8 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
}
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目立项");
Long currentUserId = AuthUtil.getUserId();
return containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
return Objects.equals(projectApply.getBusinessDeptId(), currentDeptId)
|| containsUndertakeDept(projectApply.getUndertakeDeptId(), currentDeptId)
|| Objects.equals(projectApply.getCreateUser(), currentUserId)
|| Objects.equals(projectApply.getHandlerUserId(), currentUserId)
|| Objects.equals(projectApply.getPrincipalUserId(), currentUserId);
@@ -238,11 +238,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
applyUndertakeDeptContains(queryWrapper, projectChange.getUndertakeDeptId());
}
} else if (!Objects.equals(projectChange.getAllDept(), 1)) {
// 非超管默认收口:业务部门、平台公司、创建人/经办人/负责人可见
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
Long currentUserId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> {
if (currentDeptId != null) {
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
wrapper.eq(ProjectChange::getBusinessDeptId, currentDeptId)
.or().apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
}
wrapper.or().eq(ProjectChange::getCreateUser, currentUserId)
.or().eq(ProjectChange::getHandlerUserId, currentUserId)
@@ -502,7 +504,13 @@ public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMappe
return true;
}
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
if (currentDeptId == null || Func.isBlank(projectChange.getUndertakeDeptId())) {
if (currentDeptId == null) {
return false;
}
if (Objects.equals(projectChange.getBusinessDeptId(), currentDeptId)) {
return true;
}
if (Func.isBlank(projectChange.getUndertakeDeptId())) {
return false;
}
return List.of(projectChange.getUndertakeDeptId().split(",")).stream()
@@ -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.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
import org.springframework.context.annotation.Lazy;
@@ -478,6 +479,8 @@ public class ReceivablePayableDetailServiceImpl
? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
}
});
// 历史脏数据:fee_items_json 误含 freightJson 结构字段导致结算金额翻倍,打开详情时自动修复
repairPollutedFreightFeeItems(detail, rows);
ReceivablePayableFeeDetailVO result = buildFeeDetail(rows);
// 只展示本明细已生成的费用项。合同上其他计费方案的费用项不能叠到当前明细。
return result;
@@ -1265,15 +1268,15 @@ 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.getPartyA())) {
if (customerContract == null || Func.isEmpty(customerContract.getPartyB())) {
return null;
}
return contractManageService.getOne(Wrappers.<ContractManage>lambdaQuery()
.eq(ContractManage::getIsDeleted, 0)
.eq(ContractManage::getProjectId, waybill.getProjectId())
.eq(ContractManage::getPartyA, customerContract.getPartyA())
.eq(ContractManage::getPartyA, customerContract.getPartyB())
.eq(ContractManage::getPartyB, carrierName)
.eq(ContractManage::getContractCategory, "承运商合同")
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
@@ -2005,12 +2008,15 @@ public class ReceivablePayableDetailServiceImpl
private ReceivablePayableCargoFee buildCargoFee(Long detailId, Waybill waybill) {
BigDecimal quantity = money(waybill.getQuantity());
BigDecimal unitPrice = money(waybill.getUnitPrice());
BigDecimal freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
BigDecimal otherFeeAmount = money(waybill.getOtherFeeTotal());
Map<String, Object> feeItems = parseMap(waybill.getFreightJson());
if (feeItems.isEmpty() && otherFeeAmount.compareTo(BigDecimal.ZERO) > 0) {
feeItems.put("其它费用", otherFeeAmount);
// 优先取运单 freightJson 中的运费;没有再回退数量×单价,避免与费用项重复累加
BigDecimal freightAmount = waybillFreight(waybill);
if (freightAmount == null) {
freightAmount = quantity.multiply(unitPrice).setScale(2, RoundingMode.HALF_UP);
} else {
freightAmount = money(freightAmount);
}
// freightJson 是币种/运费合计等结构体,不能整包当作费用项目 Map
Map<String, Object> feeItems = resolveWaybillFeeItems(waybill);
BigDecimal feeItemTotal = feeItems.values().stream().map(this::decimal).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = freightAmount.add(feeItemTotal).setScale(2, RoundingMode.HALF_UP);
ReceivablePayableCargoFee cargoFee = new ReceivablePayableCargoFee();
@@ -2513,7 +2519,7 @@ public class ReceivablePayableDetailServiceImpl
if (Func.isEmpty(vo.getDataSource())) {
vo.setDataSource(isManualFee(row) ? FEE_SOURCE_MANUAL : FEE_SOURCE_AUTO);
}
Map<String, Object> feeItems = parseMap(row.getFeeItemsJson());
Map<String, Object> feeItems = sanitizeFeeItems(parseMap(row.getFeeItemsJson()));
feeItemNames.addAll(feeItems.keySet());
vo.setFeeItems(feeItems);
vo.setTransportQuantityText(formatQuantity(row.getTransportQuantity(), row.getQuantityUnit()));
@@ -2530,6 +2536,52 @@ public class ReceivablePayableDetailServiceImpl
return vo;
}
/**
* 修复误把运单 freightJson 结构字段写入 fee_items_json 的历史费用行,
* 并按「运输费字段 + 清洗后费用项」重算结算金额,消除翻倍。
*/
private void repairPollutedFreightFeeItems(ReceivablePayableDetail detail, List<ReceivablePayableCargoFee> rows) {
if (detail == null || rows == null || rows.isEmpty()) {
return;
}
boolean repaired = false;
for (ReceivablePayableCargoFee row : rows) {
Map<String, Object> rawFeeItems = parseMap(row.getFeeItemsJson());
if (!isPollutedFreightFeeItems(rawFeeItems)) {
continue;
}
Map<String, Object> sanitized = sanitizeFeeItems(rawFeeItems);
Map<String, BigDecimal> feeItemAmounts = new LinkedHashMap<>();
sanitized.forEach((name, value) -> feeItemAmounts.put(name, decimal(value)));
BigDecimal afterAmount = adjustedAfterAmount(money(row.getFreightAmount()), feeItemAmounts);
row.setFeeItemsJson(JsonUtil.toJson(sanitized));
row.setOriginalAmount(afterAmount);
row.setAfterAmount(afterAmount);
row.setAdjustAmount(BigDecimal.ZERO);
cargoFeeMapper.updateById(row);
repaired = true;
}
if (!repaired) {
return;
}
BigDecimal freight = rows.stream().map(fee -> money(fee.getFreightAmount()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = rows.stream().map(fee -> money(fee.getAfterAmount()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
detail.setFreightAmount(freight);
detail.setOtherFeeAmount(total.subtract(freight).setScale(2, RoundingMode.HALF_UP));
detail.setTotalAmount(total);
detail.setFeeItemsJson(aggregateFeeItemsJson(rows));
updateById(detail);
}
private boolean isPollutedFreightFeeItems(Map<String, Object> feeItems) {
if (feeItems == null || feeItems.isEmpty()) {
return false;
}
return feeItems.keySet().stream().anyMatch(TransportBusinessSupport::isFreightJsonMetaKey);
}
private List<String> contractFeeItemNames(Long contractId) {
if (contractId == null) return List.of();
ContractManage contract = contractManageService.getById(contractId);
@@ -3000,7 +3052,7 @@ public class ReceivablePayableDetailServiceImpl
private String aggregateFeeItemsJson(List<ReceivablePayableCargoFee> fees) {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) ->
fees.forEach(fee -> sanitizeFeeItems(parseMap(fee.getFeeItemsJson())).forEach((name, value) ->
feeItems.merge(name, decimal(value), BigDecimal::add)));
return JsonUtil.toJson(feeItems);
}
@@ -3456,6 +3508,74 @@ public class ReceivablePayableDetailServiceImpl
return code;
}
/**
* 从运单 freightJson / otherFeeTotal 解析费用项目(名称→金额)。
* 仅保留真正的费用项;结构字段(currency、totalFreightAmount 等)不会写入。
*/
private Map<String, Object> resolveWaybillFeeItems(Waybill waybill) {
Map<String, Object> feeItems = new LinkedHashMap<>();
Map<String, Object> freight = waybillFreightMap(waybill);
freight.forEach((key, value) -> {
if (Func.isBlank(key) || TransportBusinessSupport.isFreightJsonMetaKey(key)) {
return;
}
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
return;
}
// 兼容历史「费用名→金额」扁平 JSON(键名通常含「费」)
if (!key.contains("费")) {
return;
}
feeItems.put(key, decimal(value));
});
BigDecimal otherFeeAmount = money(waybillOtherFee(waybill));
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
feeItems.put("其它费用", otherFeeAmount);
}
return feeItems;
}
/**
* 清洗已落库的 fee_items_json:过滤误写入的 freightJson 结构字段,
* 并将 otherFreightAmount / otherFeeTotal 归一为「其它费用」。
*/
private Map<String, Object> sanitizeFeeItems(Map<String, Object> raw) {
Map<String, Object> feeItems = new LinkedHashMap<>();
if (raw == null || raw.isEmpty()) {
return feeItems;
}
BigDecimal otherFeeAmount = BigDecimal.ZERO;
for (Map.Entry<String, Object> entry : raw.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (Func.isBlank(key)) {
continue;
}
if ("otherFreightAmount".equals(key) || "otherFeeTotal".equals(key) || "otherAmount".equals(key)) {
otherFeeAmount = otherFeeAmount.add(decimal(value));
continue;
}
if (TransportBusinessSupport.isFreightJsonMetaKey(key)) {
continue;
}
if (value instanceof Map<?, ?> || value instanceof Collection<?>) {
continue;
}
feeItems.put(key, decimal(value));
}
if (otherFeeAmount.compareTo(BigDecimal.ZERO) > 0 && !containsOtherFeeItem(feeItems.keySet())) {
feeItems.put("其它费用", otherFeeAmount);
}
return feeItems;
}
private boolean containsOtherFeeItem(Collection<String> names) {
if (names == null || names.isEmpty()) {
return false;
}
return names.stream().anyMatch(name -> name != null && (name.contains("其它费") || name.contains("其他费")));
}
private Map<String, Object> parseMap(String json) {
if (Func.isEmpty(json)) {
return new LinkedHashMap<>();
@@ -37,6 +37,7 @@ import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -87,9 +88,24 @@ public final class TransportBusinessSupport {
/** 临时额度管理菜单编码 */
public static final String MENU_TEMPORARY_CREDIT_LIMIT = "temporary_credit_limit";
/**
* 运单 freightJson 结构字段,不是费用项目名。
* 误写入 fee_items_json / 预结算合计时需过滤,否则会出现 totalFreightAmount 等英文费用项并导致金额翻倍。
*/
public static final Set<String> FREIGHT_JSON_META_KEYS = Set.of(
"currency", "totalFreightAmount", "otherFreightAmount", "freightItems",
"freightTotal", "totalFreight", "totalAmount", "freight", "freightAmount",
"transportFee", "otherFeeTotal", "otherAmount"
);
private TransportBusinessSupport() {
}
/** 是否运单运费 JSON 的结构字段名(不可作为费用项展示)。 */
public static boolean isFreightJsonMetaKey(String key) {
return Func.isNotBlank(key) && FREIGHT_JSON_META_KEYS.contains(key);
}
/**
* 是否超级管理员:当前用户任一角色别名等于 administrator 即为超级管理员。
*