🐛 配载准入的本单豁免只对正式单生效,合同不属于本项目点名运单与其原因单一来源

This commit is contained in:
2026-10-18 15:52:07 +08:00
parent f9453e4fac
commit ecb15ca766
5 changed files with 188 additions and 89 deletions
@@ -155,9 +155,12 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
prepare(loadingManage);
validate(loadingManage, false);
List<Long> oldWaybillIds = new ArrayList<>();
boolean ownChildrenAdmitted = false;
if (Func.isNotEmpty(loadingManage.getId())) {
LoadingManage oldRecord = loadEditable(loadingManage.getId(), true);
oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
// 草稿只预占运单号、未过判据,草稿转正式不得沿用本单豁免
ownChildrenAdmitted = !STATUS_DRAFT.equals(oldRecord.getBusinessStatus());
loadingManage.setLoadingNo(oldRecord.getLoadingNo());
loadingManage.setDeptId(oldRecord.getDeptId());
loadingManage.setDeptName(oldRecord.getDeptName());
@@ -168,10 +171,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
if (Func.isEmpty(loadingManage.getLoadingNo())) {
loadingManage.setLoadingNo(nextCode());
}
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
List<Waybill> waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), ownChildrenAdmitted);
validateCarrierContract(loadingManage, true, waybills);
boolean result = saveOrUpdate(loadingManage);
writeWaybillsForDriver(loadingManage, oldWaybillIds);
writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills);
return result;
}
@@ -417,10 +420,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setDeptId(oldRecord.getDeptId());
loadingManage.setDeptName(oldRecord.getDeptName());
loadingManage.setBusinessStatus(STATUS_PENDING);
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
validateCarrierContract(loadingManage, true);
List<Waybill> waybills = validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()), true);
validateCarrierContract(loadingManage, true, waybills);
boolean result = updateById(loadingManage);
writeWaybillsForDriver(loadingManage, oldWaybillIds);
writeWaybillsForDriver(loadingManage, oldWaybillIds, waybills);
return result;
}
@@ -742,57 +745,46 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
TransportBusinessSupport.validateLength(loadingManage.getTaskRemark(), 500, "任务备注不能超过500字");
}
private void validateWaybillsAvailable(LoadingManage loadingManage, List<Long> waybillIds) {
private List<Waybill> validateWaybillsAvailable(LoadingManage loadingManage, List<Long> waybillIds, boolean ownChildrenAdmitted) {
if (Func.isEmpty(waybillIds)) {
throw new ServiceException("请选择待配载运单");
}
List<Waybill> waybills = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIds));
List<Waybill> waybills = loadWaybills(waybillIds);
if (waybills.size() != waybillIds.size()) {
throw new ServiceException("待配载运单不存在或已删除");
}
String ownLoadingNo = loadingManage.getLoadingNo();
for (Waybill waybill : waybills) {
TransportBusinessSupport.assertCurrentDept(waybill.getDeptId(), "运单管理");
// 本单既有子运单在先前提交时已过准入判据,重新提交/编辑不再判:
// 不需接单项目的票提交后即为进行中,重复判会让本单再也保存不了
if (LoadableAdmissionSupport.isOwnChild(waybill, ownLoadingNo)) {
if (ownChildrenAdmitted && LoadableAdmissionSupport.isOwnChild(waybill, ownLoadingNo)) {
continue;
}
if (!LoadableAdmissionSupport.isLoadable(waybill, ownLoadingNo)) {
throw new ServiceException(unloadableReason(waybill));
LoadableAdmissionSupport.UnloadableReason reason =
LoadableAdmissionSupport.unloadableReason(waybill, ownLoadingNo);
if (reason != null) {
throw new ServiceException(unloadableMessage(waybill, reason));
}
}
validateSameProject(waybills);
return waybills;
}
/**
* 逐张准入失败的可读原因。判据本身在可配载支持类里,这里只把命中的那一条翻成文案。
*
* @param waybill 未通过准入判据的运单
* @return 含运单号与原因的提示
* 逐张准入失败的可读原因:判据只给原因枚举,运单号与文案在这里拼。
*/
private String unloadableReason(Waybill waybill) {
private String unloadableMessage(Waybill waybill, LoadableAdmissionSupport.UnloadableReason reason) {
String prefix = "运单" + waybill.getWaybillNo();
if (Func.isNotEmpty(waybill.getLoadingNo())) {
return prefix + "已关联其他配载单";
}
if (!LoadableAdmissionSupport.isRoadTransport(waybill.getTransportType())) {
return prefix + "运输方式不是公路,不可配载";
}
if (!LoadableAdmissionSupport.LOADABLE_BUSINESS_STATUS.equals(waybill.getBusinessStatus())) {
return prefix + "当前状态不是待执行,不可配载";
}
return prefix + "不可配载";
return prefix + switch (reason) {
case NOT_ROAD -> "运输方式不是公路,不可配载";
case NOT_PENDING -> "当前状态不是待执行,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他配载单";
};
}
/**
* 一张配载单的全部子运单必须属于同一个项目,否则点名与其它子运单项目不同的运单。
* <p>
* 查询条件表达不了跨行约束,只能整批取回后在内存里比。
*
* @param waybills 本次要写入的子运单全集
*/
private void validateSameProject(List<Waybill> waybills) {
Long projectId = waybills.get(0).getProjectId();
@@ -807,6 +799,10 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
private void validateCarrierContract(LoadingManage loadingManage, boolean required) {
validateCarrierContract(loadingManage, required, null);
}
private void validateCarrierContract(LoadingManage loadingManage, boolean required, List<Waybill> waybills) {
if (!Objects.equals(loadingManage.getCarrierType(), "承运商")) {
loadingManage.setCarrierContractId(null);
if (!Objects.equals(loadingManage.getCarrierType(), CARRIER_SELF)) {
@@ -825,9 +821,19 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
if (carrierContract == null) {
throw new ServiceException("所选承运商合同不存在、未审核通过或已失效");
}
List<Waybill> childWaybills = waybills == null
? loadWaybills(waybillIds(loadingManage.getWaybillIdsJson()))
: waybills;
// 合同自带项目与乙方:合同的项目不属于本单子运单的项目,等于换了项目却没换乙方
if (!projectIdsByWaybills(loadingManage).contains(carrierContract.getProjectId())) {
throw new ServiceException("所选承运商合同不属于本项目");
if (!projectIdsOf(childWaybills).contains(carrierContract.getProjectId())) {
String waybillNos = childWaybills.stream()
.filter(waybill -> !Objects.equals(waybill.getProjectId(), carrierContract.getProjectId()))
.map(Waybill::getWaybillNo)
.filter(Func::isNotEmpty)
.collect(Collectors.joining("、"));
throw new ServiceException(Func.isEmpty(waybillNos)
? "所选承运商合同不属于本项目"
: "所选承运商合同不属于运单" + waybillNos + "的项目");
}
loadingManage.setCarrierName(carrierContract.getPartyB());
}
@@ -845,16 +851,17 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
}
}
private List<Long> projectIdsByWaybills(LoadingManage loadingManage) {
List<Long> waybillIds = waybillIds(loadingManage.getWaybillIdsJson());
private List<Waybill> loadWaybills(List<Long> waybillIds) {
if (Func.isEmpty(waybillIds)) {
return List.of();
}
return waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.select(Waybill::getProjectId)
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIds))
.stream().map(Waybill::getProjectId).filter(Objects::nonNull).distinct().toList();
.in(Waybill::getId, waybillIds));
}
private List<Long> projectIdsOf(List<Waybill> waybills) {
return waybills.stream().map(Waybill::getProjectId).filter(Objects::nonNull).distinct().toList();
}
private List<ContractManage> availableCarrierContracts(List<Long> projectIds) {
@@ -870,9 +877,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
* 按主键取一张可用的承运商合同。
* <p>
* 不按项目过滤,才能把「合同不可用」与「合同不属于本项目」分成两种提示。
*
* @param contractId 合同主键
* @return 可用的合同;不存在或不可用返回 null
* 取不到可用的合同返回 null。
*/
private ContractManage availableCarrierContractById(Long contractId) {
if (Func.isEmpty(contractId)) {
@@ -899,9 +904,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
* <p>
* 推导按「接单事实已清、尚未接单」的口径取值,故清接单事实放在状态写入之后,两者口径一致。
*/
private void writeWaybillsForDriver(LoadingManage loadingManage, List<Long> oldWaybillIds) {
syncAssociatedWaybills(loadingManage,
deriveWaybillBusinessStatuses(waybillIds(loadingManage.getWaybillIdsJson())), oldWaybillIds);
private void writeWaybillsForDriver(LoadingManage loadingManage, List<Long> oldWaybillIds, List<Waybill> waybills) {
syncAssociatedWaybills(loadingManage, deriveWaybillBusinessStatuses(waybills), oldWaybillIds);
// 接单事实不清,被拒的票对新司机不可见——司机端一切入口都排除已拒绝的运单
clearAssociatedWaybillAcceptRecords(loadingManage);
}
@@ -913,16 +917,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
* 快照只作兜底、永不优先,否则项目改配置后写入值与读时的归一化结果会分叉。
* 项目为空且快照也为空 → 视为不需要接单 → 进行中。
*/
private Map<Long, String> deriveWaybillBusinessStatuses(List<Long> waybillIds) {
private Map<Long, String> deriveWaybillBusinessStatuses(List<Waybill> waybills) {
Map<Long, String> businessStatusByWaybillId = new LinkedHashMap<>();
if (Func.isEmpty(waybillIds)) {
if (Func.isEmpty(waybills)) {
return businessStatusByWaybillId;
}
List<Waybill> waybills = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.select(Waybill::getId, Waybill::getProjectId, Waybill::getProcessJson)
.eq(Waybill::getIsDeleted, 0)
.in(Waybill::getId, waybillIds));
// 一次查全所有项目的生效配置,不逐张回查
Map<Long, String> liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService,
waybills.stream().map(Waybill::getProjectId).toList());
for (Waybill waybill : waybills) {
@@ -943,9 +942,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
syncAssociatedWaybills(loadingManage, statusByWaybillId, oldWaybillIds);
}
/**
* 逐张写状态:同状态的票合成一条批量更新,配载字段对每张票都写全。
*/
private void syncAssociatedWaybills(LoadingManage loadingManage, Map<Long, String> statusByWaybillId, List<Long> oldWaybillIds) {
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
if (Func.isNotEmpty(oldWaybillIds)) {
@@ -55,6 +55,23 @@ public final class LoadableAdmissionSupport {
private LoadableAdmissionSupport() {
}
/**
* 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。
* <p>
* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。
*/
public enum UnloadableReason {
/** 运输方式不是公路 */
NOT_ROAD,
/** 业务状态不是待执行 */
NOT_PENDING,
/** 已关联其它配载单 */
ON_OTHER_MANIFEST
}
/**
* 是否公路运输。未知或为空一律按非公路处理。
*
@@ -77,16 +94,6 @@ public final class LoadableAdmissionSupport {
return false;
}
/**
* 该运单此刻是否可被配载:要求未关联任何配载单。
*
* @param waybill 运单
* @return true-可配载
*/
public static boolean isLoadable(Waybill waybill) {
return isLoadable(waybill, null);
}
/**
* 该运单此刻是否可被配载。
* <p>
@@ -98,15 +105,28 @@ public final class LoadableAdmissionSupport {
* @return true-可配载
*/
public static boolean isLoadable(Waybill waybill, String ownLoadingNo) {
if (waybill == null) {
return false;
return unloadableReason(waybill, ownLoadingNo) == null;
}
/**
* 该运单此刻不可配载的原因。
*
* @param waybill 运单
* @param ownLoadingNo 本配载单号;新建时传 null
* @return 命中的第一条原因;可配载时返回 null
*/
public static UnloadableReason unloadableReason(Waybill waybill, String ownLoadingNo) {
if (waybill == null || !isRoadTransport(waybill.getTransportType())) {
return UnloadableReason.NOT_ROAD;
}
if (!isRoadTransport(waybill.getTransportType())
|| !LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) {
return false;
if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) {
return UnloadableReason.NOT_PENDING;
}
String loadingNo = text(waybill.getLoadingNo());
return loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo));
if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) {
return null;
}
return UnloadableReason.ON_OTHER_MANIFEST;
}
/**