♻️ 整理异常处置路线调整的注释、参数命名与终态守卫

注释只留状态与约束,去掉推理与取舍表述;firstNonBlank / rewriteSegmentPoint 参数改为有语义的名字;reject / revoke / adjustRoute 共用 assertNotSettled,文案沿用「该异常已处理」。
This commit is contained in:
2026-10-23 01:29:09 +08:00
parent 87b473594a
commit aa7caf5a4c
@@ -440,10 +440,7 @@ public class ExceptionDisposalServiceImpl
throw new ServiceException("请选择需要拒绝的异常");
}
ExceptionDisposal disposal = getExisting(request.getId());
// 已完成、已撤销都算已了结:再拒绝一次会覆盖最新处置结论,并往处置记录里多添一条
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
throw new ServiceException("该异常已处理");
}
assertNotSettled(disposal);
String rejectReason = Func.toStr(request.getRejectReason(), "").trim();
if (Func.isBlank(rejectReason)) {
throw new ServiceException("请填写拒绝原因");
@@ -467,14 +464,12 @@ public class ExceptionDisposalServiceImpl
public void revoke(Long id) {
ExceptionDisposal disposal = getExisting(id);
Long currentUserId = AuthUtil.getUserId();
// 撤销是上报人收回自己的上报,不是一次处置:别的账号(包括调度员)要推翻上报只能走拒绝
// 撤销是上报人收回自己的上报,不是一次处置
if (currentUserId == null || !Objects.equals(disposal.getReporterId(), currentUserId)) {
throw new ServiceException("只能撤销本人上报的异常");
}
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
throw new ServiceException("该异常已处理");
}
// 不写跟进记录,也不动 latest_follow_content:处置时间线里不该出现一条由上报人「处置」的记录
assertNotSettled(disposal);
// 不写跟进记录,也不动 latest_follow_content
disposal.setDisposalStatus(STATUS_REVOKED);
updateById(disposal);
}
@@ -486,25 +481,23 @@ public class ExceptionDisposalServiceImpl
throw new ServiceException("请选择需要调整路线的异常");
}
ExceptionDisposal disposal = getExisting(request.getId());
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
throw new ServiceException("该异常已处理");
}
assertNotSettled(disposal);
Waybill waybill = disposal.getWaybillId() == null ? null : waybillService.getById(disposal.getWaybillId());
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
throw new ServiceException("关联运单不存在");
}
// 路线改在谁身上跟着配载单走:配载单是这趟车的执行主体,司机端打卡读的就是它的路线
// 路线改在谁身上跟着配载单走:配载单是这趟车的执行主体
LoadingManage loading = findLoading(waybill.getLoadingNo());
if (loading == null && Func.isNotBlank(waybill.getLoadingNo())) {
throw new ServiceException("运单已关联配载单,请在配载单中修改");
}
// 校验与拼装全部在写入之前完成:任何一条不通过都不该留下半条处置记录
// 校验与拼装全部在写入之前完成
List<Station> stations = validateRouteNodes(request.getRouteNodes(), loading);
List<String> sourceAddresses = loading != null
? loadingStationAddresses(loading)
: waybillStationAddresses(waybill);
List<String> targetAddresses = stations.stream().map(Station::address).toList();
// 首站地址只读;原数据里首站地址缺失时无从比对,放行而不是拒绝整次调整
// 首站地址不可修改;原数据首站地址为空时跳过比对
if (!sourceAddresses.isEmpty() && Func.isNotBlank(sourceAddresses.get(0))
&& !Objects.equals(sourceAddresses.get(0), targetAddresses.get(0))) {
throw new ServiceException("起始地址不允许修改");
@@ -515,8 +508,7 @@ public class ExceptionDisposalServiceImpl
}
RouteChangePlan plan = planRouteChange(loading, waybill, stations, sourceAddresses, targetAddresses);
// 事务内顺序固定:跟进记录 → 载体路线 → 异常状态。
// 少了任何一步都读不出「谁在什么时候把路线改成了什么」,所以任一步失败都整体回滚
// 事务内顺序固定:跟进记录 → 载体路线 → 异常状态
insertFollowRecord(disposal.getId(), plan.followContent());
if (loading != null) {
applyLoadingRoute(loading, plan);
@@ -528,6 +520,13 @@ public class ExceptionDisposalServiceImpl
updateById(disposal);
}
/** 已了结(已完成 / 已撤销)的异常不允许再次处置 */
private void assertNotSettled(ExceptionDisposal disposal) {
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
throw new ServiceException("该异常已处理");
}
}
/** 一条跟进记录:处置记录里「先写记录再改状态」的顺序由调用方的事务保证 */
private void insertFollowRecord(Long disposalId, String content) {
ExceptionDisposalFollowRecord record = new ExceptionDisposalFollowRecord();
@@ -553,7 +552,7 @@ public class ExceptionDisposalServiceImpl
/**
* 校验并规范化站点:站数至少两个、首站 start、末站 end、其余 transit,地址都不能为空。
* <p>
* kind 不额外接受「按位置猜」的写法:站序就是落库顺序,kind 与位置不一致时写下去的首末站地址会是错的。
* kind 由位置决定,必须与站序一致。
*/
private List<Station> validateRouteNodes(List<ExceptionDisposalRouteAdjustRequest.RouteNode> routeNodes,
LoadingManage loading) {
@@ -597,10 +596,9 @@ public class ExceptionDisposalServiceImpl
}
/**
* 站点标记里的数字是配载单运单集合里的位次(1 起),越界的标记会让司机端找不到对应子运单,
* 那样的路线读不出装卸与货物,宁可报错也不落库。
* 站点标记里的数字是配载单运单集合里的位次(1 起),越界时拒绝。
* <p>
* 没有配载单就没有子运单序号可言:运单的路线段没有装卸归属,站点标记不参与落库。
* 没有配载单时没有子运单序号,站点标记不参与落库。
*/
private void assertTagsMatchChildWaybills(List<Station> stations, LoadingManage loading) {
if (loading == null) {
@@ -626,7 +624,7 @@ public class ExceptionDisposalServiceImpl
List<?> waybillIds = JsonUtil.parse(waybillIdsJson, List.class);
return waybillIds == null ? 0 : waybillIds.size();
} catch (Exception ignored) {
// 运单集合不是 JSON 数组时按没有子运单处理,站点标记一律对不上
// 运单集合不是 JSON 数组时按没有子运单处理
return 0;
}
}
@@ -645,7 +643,7 @@ public class ExceptionDisposalServiceImpl
/**
* 运单当前路线的站点地址:首段出发地是首站,其后每段的到达地各成一站。
* 段里缺地址时退化用段名——运单详情与调整页用的也是这两列。
* 段里缺地址时用站名替代。
*/
private List<String> waybillStationAddresses(Waybill waybill) {
List<Map<String, Object>> segments = routeSegments(waybill.getRouteJson());
@@ -739,7 +737,7 @@ public class ExceptionDisposalServiceImpl
parts.add("到货地 " + displayAddress(sourceEnd) + " → " + displayAddress(targetEnd));
}
if (parts.isEmpty()) {
// 首末站都没动,变化只能落在途经站:地址细节塞不进 500 字的跟进记录,按站数说明
// 首末站未变时按途经站数量说明变化
parts.add("途经站由 " + transitCount(sourceAddresses) + " 个改为 " + transitCount(targetAddresses) + " 个");
}
return ROUTE_CHANGE_PREFIX + String.join(";", parts);
@@ -770,7 +768,7 @@ public class ExceptionDisposalServiceImpl
// 站点行没有独立的站名,展示地址与地址取同一份文本
row.put("displayAddress", station.address());
row.put("tags", station.tags());
// locked 只影响 PC 编辑弹窗能否拖动,司机端不读;这里写入的路线始终可继续调整
// locked 只影响 PC 编辑弹窗能否拖动,司机端不读
row.put("locked", false);
rows.add(row);
}
@@ -798,20 +796,20 @@ public class ExceptionDisposalServiceImpl
return JsonUtil.toJson(rewritten);
}
private void rewriteSegmentPoint(Map<String, Object> segment, String point, String targetAddress) {
String currentAddress = firstNonBlank(text(segment.get(point + "Address")),
text(segment.get(point + "Name")), "");
/** 站点地址与站名写同一份文本 */
private void rewriteSegmentPoint(Map<String, Object> segment, String pointKey, String targetAddress) {
String currentAddress = firstNonBlank(text(segment.get(pointKey + "Address")),
text(segment.get(pointKey + "Name")), "");
if (Objects.equals(currentAddress, targetAddress)) {
return;
}
segment.put(point + "Address", targetAddress);
segment.put(point + "Name", targetAddress);
segment.put(pointKey + "Address", targetAddress);
segment.put(pointKey + "Name", targetAddress);
}
/**
* 识别路线段形状的 route_json。运单历史上存在两种形状,另一种是 PC 写下的节点行
* (address + tags + 运单号),司机端按段解析读不出来,因此这里只认段:元素带段字段、
* 且不带 address / tags。
* 识别路线段形状的 route_json:元素带段字段,且不带 address / tags。
* 另一形状是 PC 写下的节点行(address + tags + 运单号),司机端按段解析读不出来。
*/
private List<Map<String, Object>> routeSegments(String routeJson) {
List<Map<String, Object>> elements = jsonObjectList(routeJson);
@@ -829,8 +827,7 @@ public class ExceptionDisposalServiceImpl
/**
* 任务信息是整列覆盖:读出原对象,只把本次变更记录插到最前面,其余键原样写回。
* 原来的内容不是 JSON 对象(坏数据或历史数组)时按空对象重建,只丢这一列的无法解析部分,
* 不能让一次路线调整因为附件信息列而失败。
* 原来的内容不是 JSON 对象(坏数据或历史数组)时按空对象重建。
*/
private String appendRouteChangeRecord(String taskInfoJson, String content) {
Map<String, Object> taskInfo = parseTaskInfo(taskInfoJson);
@@ -849,7 +846,7 @@ public class ExceptionDisposalServiceImpl
return JsonUtil.toJson(taskInfo);
}
/** 任务信息只认 JSON 对象:数组与坏数据都按空对象重建,这一列不能挡住整次路线调整 */
/** 任务信息只认 JSON 对象:数组与坏数据都按空对象重建 */
private Map<String, Object> parseTaskInfo(String taskInfoJson) {
Map<String, Object> taskInfo = new LinkedHashMap<>();
if (Func.isBlank(taskInfoJson)) {
@@ -889,7 +886,7 @@ public class ExceptionDisposalServiceImpl
}
}
/** 解析成对象数组;坏 JSON 与空值都按空数组处理,路线坏了不该让整次操作崩掉 */
/** 解析成对象数组;坏 JSON 与空值都按空数组处理 */
private List<Map<String, Object>> jsonObjectList(String json) {
List<Map<String, Object>> result = new ArrayList<>();
if (Func.isBlank(json)) {
@@ -916,18 +913,18 @@ public class ExceptionDisposalServiceImpl
return value == null ? "" : String.valueOf(value).trim();
}
private String firstNonBlank(String first, String second, String fallback) {
if (Func.isNotBlank(first)) {
return first;
private String firstNonBlank(String addressValue, String nameValue, String fallbackValue) {
if (Func.isNotBlank(addressValue)) {
return addressValue;
}
return Func.isNotBlank(second) ? second : fallback;
return Func.isNotBlank(nameValue) ? nameValue : fallbackValue;
}
/** 规范化后的一个站点:地址 + 该站的装卸标记 */
private record Station(String address, List<String> tags) {
}
/** 一次路线调整要落库的全部内容,写入阶段只负责 set 与 update */
/** 一次路线调整要落库的全部内容 */
private record RouteChangePlan(String followContent,
String routeJson,
String departureAddress,
@@ -1008,8 +1005,8 @@ public class ExceptionDisposalServiceImpl
/**
* 详情页的可编辑站点与配载单信息。
* <p>
* 配载单是这趟车的执行主体,站点取它的站点行;运单没有配载单号、或配载单已被删除 / 解绑时退回
* 运单自己的路线段——历史异常仍要能打开,不能因为配载单不在了就打不开详情。
* 站点取配载单的站点行;运单没有配载单号,或配载单已删除时退回运单的路线段。
* 运单不存在时三项各给空值。
*/
private void fillRouteNodes(ExceptionDisposalVO vo, Waybill waybill) {
if (vo == null) {
@@ -1061,7 +1058,7 @@ public class ExceptionDisposalServiceImpl
/**
* 没有配载单时从路线段推导站点:首段出发地是首站,其后每段的到达地各成一站。
* 地址口径与改路线时的比对一致(地址优先、站名兜底),否则用户什么都没改直接提交也会被判成改过地址。
* 地址口径与改路线时的比对一致(地址优先、站名兜底)。
*/
private List<ExceptionDisposalVO.RouteNode> waybillRouteNodes(Waybill waybill) {
List<ExceptionDisposalVO.RouteNode> nodes = new ArrayList<>();
@@ -1078,7 +1075,7 @@ public class ExceptionDisposalServiceImpl
return stationKinds(nodes);
}
/** 站序声明:首站 start、末站 end、其余 transit;不足两个站就没有首末可言,不给前端调整入口 */
/** 站序声明:首站 start、末站 end、其余 transit;不足两个站时返回空列表 */
private List<ExceptionDisposalVO.RouteNode> stationKinds(List<ExceptionDisposalVO.RouteNode> nodes) {
if (nodes.size() < 2) {
return new ArrayList<>();
@@ -1092,7 +1089,7 @@ public class ExceptionDisposalServiceImpl
/**
* 站点标记指向配载单运单集合里的第 N 张子运单,取其项目名去重。
* 标记对不上子运单(越界、子运单已不存在)时跳过,一个坏标记不该让整个详情打不开。
* 标记对不上子运单(越界、子运单已不存在)时跳过。
*/
private List<String> stationProjects(List<String> tags, Map<Integer, Waybill> childWaybills) {
Set<String> projects = new LinkedHashSet<>();
@@ -1111,7 +1108,7 @@ public class ExceptionDisposalServiceImpl
/**
* 按配载单运单集合的位次取子运单,位次从 0 起(站点标记里的 N 减一)。
* 位次不能因为某张子运单查不到而前移,否则标记会指到别的子运单上。
* 位次不因缺子运单而前移。
*/
private Map<Integer, Waybill> childWaybillsByOrder(LoadingManage loading) {
Map<Integer, Waybill> childWaybills = new LinkedHashMap<>();
@@ -1153,7 +1150,7 @@ public class ExceptionDisposalServiceImpl
}
}
} catch (Exception ignored) {
// 运单集合坏了就没有子运单可言,详情退回运单自己的路线,不影响打开
// 运单集合不是 JSON 数组时按没有子运单处理
return new ArrayList<>();
}
return waybillIds;