From 3d3ed0ca273e50f32bdee975c8e99e7fd50e3a15 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Wed, 7 Oct 2026 13:24:02 +0800 Subject: [PATCH] =?UTF-8?q?:recycle:=20=E6=94=B6=E7=B4=A7=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E9=97=A8=E7=A6=81=E6=9F=A5=E8=AF=A2=E3=80=81=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E5=B9=B3=E8=A1=8C=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E8=8A=82=E7=82=B9=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/DriverWaybillServiceImpl.java | 31 +++----- .../impl/ExceptionDisposalServiceImpl.java | 11 +-- .../support/WaybillProcessSupport.java | 45 +++++------ .../impl/ExceptionDisposalReportGateTest.java | 78 ++++++++++++------- .../support/WaybillProcessSupportTest.java | 65 ++++++---------- 5 files changed, 110 insertions(+), 120 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java index c9138ac3..a2c9236c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java @@ -114,12 +114,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { private static final DateTimeFormatter DATE_DOT_SHORT = DateTimeFormatter.ofPattern("MM.dd"); private static final DateTimeFormatter TIME_HM = DateTimeFormatter.ofPattern("HH:mm"); - /** - * 异常处置终态:已完成 / 已撤销。只有终态解除打卡门禁——「处理中」只表示有人 - * 跟进过,问题可能仍在,不放行。 - */ - private static final List SETTLED_EXCEPTION_STATUSES = Arrays.asList("completed", "revoked"); - /** 被异常阻断时的不可打卡原因(司机端展示) */ private static final String LOCK_REASON_EXCEPTION = "该运单存在未处理异常,处理完成后才能打卡"; @@ -811,17 +805,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return false; } // 不复用 latestNodePunchMap:它的 key 被 toLowerCase 过,与过程配置 key 比不出来 - String key = nodeCode.trim(); return nodePunchMapper.selectList(Wrappers.lambdaQuery() .eq(WaybillNodePunch::getWaybillId, waybillId)) .stream() - .map(punch -> Func.toStr(punch.getNodeCode(), "").trim()) - .anyMatch(key::equalsIgnoreCase); + .anyMatch(punch -> WaybillProcessSupport.sameNodeCode(nodeCode, punch.getNodeCode())); } @Override public void assertCurrentDriverOwnsWaybill(Long waybillId) { - // 与司机打卡、司机接单共用同一套归属判定,不另立第二套规则 + // 归属口径与司机打卡、司机接单一致:都按司机档案自述绑定的车牌判定 Driver driver = requireCurrentDriver(); loadDriverWaybill(waybillId, currentBoundPlates(driver)); } @@ -864,7 +856,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return exceptionDisposalMapper.selectList(Wrappers.lambdaQuery() .eq(ExceptionDisposal::getWaybillId, waybillId) .eq(ExceptionDisposal::getIsDeleted, 0) - .notIn(ExceptionDisposal::getDisposalStatus, SETTLED_EXCEPTION_STATUSES) + .notIn(ExceptionDisposal::getDisposalStatus, WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES) .orderByAsc(ExceptionDisposal::getReportTime) .orderByAsc(ExceptionDisposal::getId)); } @@ -1107,18 +1099,17 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { // 它作用于整张运单,所以每个可打卡节点——包括已打卡的与在途的——都挂标签、都算相关。 // 相关只有两种来源:归属本节点的、以及整单的;被前序节点异常牵连阻断的节点 // 只是不能打卡,那条异常并不归属它,故不进入它的相关列表。 + // 未了结异常已按上报时间升序取出,过滤后仍保持该顺序;id 转字符串是因为主键是 int64, + // 前端直收数字会丢精度。 List unresolved = unresolvedExceptions(waybill.getId()); - List unresolvedCodes = unresolved.stream() - .map(item -> Func.toStr(item.getNodeCode(), "").trim()) - .collect(Collectors.toList()); - List unresolvedIds = unresolved.stream() - .map(ExceptionDisposal::getId) - .collect(Collectors.toList()); - boolean hasWaybillException = WaybillProcessSupport.hasWaybillLevelException(unresolvedCodes); + boolean hasWaybillException = unresolved.stream() + .anyMatch(item -> WaybillProcessSupport.isWaybillLevelException(item.getNodeCode())); for (DriverPunchNodeVO vo : nodes) { vo.setHasWaybillException(hasWaybillException); - vo.setRelatedExceptionIds( - WaybillProcessSupport.relatedExceptionIds(vo.getKey(), unresolvedCodes, unresolvedIds)); + vo.setRelatedExceptionIds(unresolved.stream() + .filter(item -> WaybillProcessSupport.relatesToNode(vo.getKey(), item.getNodeCode())) + .map(item -> String.valueOf(item.getId())) + .collect(Collectors.toList())); } for (DriverPunchNodeVO vo : nodes) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index c56778a7..5086aaf5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -54,6 +54,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -182,12 +183,12 @@ public class ExceptionDisposalServiceImpl * 节点编码忽略大小写:历史数据里的写法不一定与过程配置的 key 完全一致。 */ private boolean hasUnresolvedNodeException(Long waybillId, String nodeCode) { - List existing = baseMapper.selectList(Wrappers.lambdaQuery() + Long count = baseMapper.selectCount(Wrappers.lambdaQuery() .eq(ExceptionDisposal::getWaybillId, waybillId) - .eq(ExceptionDisposal::getIsDeleted, 0)); - return existing.stream().anyMatch(item -> - nodeCode.equalsIgnoreCase(Func.toStr(item.getNodeCode(), "").trim()) - && !WaybillProcessSupport.isSettledDisposal(item.getDisposalStatus())); + .eq(ExceptionDisposal::getIsDeleted, 0) + .notIn(ExceptionDisposal::getDisposalStatus, WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES) + .apply("LOWER(node_code) = {0}", nodeCode.toLowerCase(Locale.ROOT))); + return count != null && count > 0; } /** 按运单补齐运单号 / 车牌 / 项目 / 承运商等展示字段 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java index f890f5f7..dc0649d3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java @@ -58,6 +58,8 @@ public final class WaybillProcessSupport { /** 异常处置终态:completed=已完成,revoked=已撤销。只有终态解除打卡门禁 */ private static final String DISPOSAL_COMPLETED = "completed"; private static final String DISPOSAL_REVOKED = "revoked"; + /** 异常处置终态状态码集合:不在此集合内即未了结(processing 也在未了结之列) */ + public static final List SETTLED_DISPOSAL_STATUSES = List.of(DISPOSAL_COMPLETED, DISPOSAL_REVOKED); private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("H:mm"); private static final DateTimeFormatter HM_PADDED = DateTimeFormatter.ofPattern("HH:mm"); @@ -391,42 +393,31 @@ public final class WaybillProcessSupport { } /** - * 是否存在未了结的整单异常。 + * 两个节点编码是否指同一个节点:两侧去空白、忽略大小写。 *

- * 整单异常不归属任何过程节点,它的节点编码为空;它作用于整张运单, - * 所以该运单每个可打卡节点都要挂上「整单异常」标记。 + * 空与空不算相等——节点编码为空表示整单,它不归属任何过程节点。 */ - public static boolean hasWaybillLevelException(List unresolvedNodeCodes) { - if (unresolvedNodeCodes == null) { - return false; - } - return unresolvedNodeCodes.stream().anyMatch(code -> stringVal(code).isEmpty()); + public static boolean sameNodeCode(String left, String right) { + String leftCode = stringVal(left); + String rightCode = stringVal(right); + return !leftCode.isEmpty() && leftCode.equalsIgnoreCase(rightCode); } /** - * 与某节点相关的未了结异常 id,一律以字符串返回。 + * 该异常是否为整单异常:异常不归属任何过程节点时,节点编码为空。 + */ + public static boolean isWaybillLevelException(String exceptionNodeCode) { + return stringVal(exceptionNodeCode).isEmpty(); + } + + /** + * 该异常是否与某节点相关。 *

* 相关只有两种来源:归属该节点的异常,以及整单异常(节点编码为空,对每个节点都算相关)。 * 被前序节点异常牵连阻断的节点不算相关——它自己并没有出过事。 - *

- * 顺序保持入参顺序,调用方按上报时间升序取出未了结异常即可。 - * id 转成字符串是因为主键是 int64,前端直收数字会丢精度。 */ - public static List relatedExceptionIds(String nodeKey, List unresolvedNodeCodes, - List unresolvedIds) { - if (unresolvedNodeCodes == null || unresolvedIds == null) { - return Collections.emptyList(); - } - String key = stringVal(nodeKey); - List related = new ArrayList<>(); - int size = Math.min(unresolvedNodeCodes.size(), unresolvedIds.size()); - for (int i = 0; i < size; i++) { - String code = stringVal(unresolvedNodeCodes.get(i)); - if (code.isEmpty() || code.equalsIgnoreCase(key)) { - related.add(String.valueOf(unresolvedIds.get(i))); - } - } - return related; + public static boolean relatesToNode(String nodeKey, String exceptionNodeCode) { + return isWaybillLevelException(exceptionNodeCode) || sameNodeCode(nodeKey, exceptionNodeCode); } /** diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java index 900b19ac..6d0fee09 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java @@ -21,7 +21,8 @@ import org.springblade.transport.service.IDriverWaybillService; import org.springblade.transport.service.IWaybillService; import java.lang.reflect.Field; -import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,7 +75,7 @@ class ExceptionDisposalReportGateTest { inject("baseMapper", disposalMapper); when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill()); - when(disposalMapper.selectList(any())).thenReturn(List.of()); + when(disposalMapper.selectCount(any())).thenReturn(0L); when(disposalMapper.insert(any(ExceptionDisposal.class))).thenReturn(1); } @@ -120,7 +121,7 @@ class ExceptionDisposalReportGateTest { @DisplayName("节点已有待处理异常时拒绝再报") void pendingNodeExceptionRejectsReport() { stubNode("load", "装货", "cargo"); - stubUnresolvedNodeException("load", "pending"); + stubUnresolvedCount(1L); ServiceException exception = submitExpectRejected("load"); @@ -131,46 +132,55 @@ class ExceptionDisposalReportGateTest { @DisplayName("节点异常已有人跟进(处理中)时同样拒绝:处理中不解除任何门禁") void processingNodeExceptionRejectsReport() { stubNode("load", "装货", "cargo"); - stubUnresolvedNodeException("load", "processing"); + stubUnresolvedCount(1L); ServiceException exception = submitExpectRejected("load"); assertEquals("该节点已有未处理异常,处理完成后才能再次上报", exception.getMessage()); + List params = capturedUnresolvedParams(); + assertTrue(params.contains("completed") && params.contains("revoked"), + "未了结 = 处置状态不在这两个终态里,故「处理中」也占位:" + params); } @Test @DisplayName("该节点的异常已完成、或被撤销后,节点重新可以上报") void settledNodeExceptionAllowsReportAgain() { stubNode("load", "装货", "cargo"); - when(disposalMapper.selectList(any())).thenReturn(List.of( - nodeException("load", "completed"), - nodeException("load", "revoked"))); + stubUnresolvedCount(0L); submitOk("load"); assertEquals("load", savedDisposal().getNodeCode()); + assertTrue(capturedUnresolvedParams().containsAll(List.of("completed", "revoked")), + "只有已完成、已撤销解除门禁,SQL 必须把它们排除在未了结之外"); } @Test @DisplayName("别的节点的未了结异常不占用本节点:约束只落在异常自己归属的节点上") void otherNodesExceptionDoesNotBlockReport() { stubNode("unload", "卸货", "cargo"); - when(disposalMapper.selectList(any())).thenReturn(List.of(nodeException("load", "pending"))); + stubUnresolvedCount(0L); submitOk("unload"); assertEquals("unload", savedDisposal().getNodeCode()); + assertTrue(capturedUnresolvedParams().contains("unload"), + "节点归属条件落在 SQL 上,查的是本次上报的节点:" + capturedUnresolvedParams()); } @Test @DisplayName("节点编码大小写不敏感:库里存 LOAD、过程配置叫 load,仍算同一节点") void nodeCodeComparisonIgnoresCase() { stubNode("load", "装货", "cargo"); - when(disposalMapper.selectList(any())).thenReturn(List.of(nodeException("LOAD", "pending"))); + stubUnresolvedCount(1L); ServiceException exception = submitExpectRejected("load"); assertEquals("该节点已有未处理异常,处理完成后才能再次上报", exception.getMessage()); + assertTrue(capturedUnresolvedQuery().getTargetSql().contains("LOWER(node_code) ="), + "忽略大小写要落在 SQL 上:" + capturedUnresolvedQuery().getTargetSql()); + assertTrue(capturedUnresolvedParams().contains("load"), + "参数取节点 key 的小写形式:" + capturedUnresolvedParams()); } @Test @@ -193,15 +203,14 @@ class ExceptionDisposalReportGateTest { submitOk("load"); - ArgumentCaptor> captor = - ArgumentCaptor.forClass(LambdaQueryWrapper.class); - verify(disposalMapper).selectList(captor.capture()); - String sql = captor.getValue().getTargetSql(); + String sql = capturedUnresolvedQuery().getTargetSql(); assertTrue(sql.contains("waybill_id"), "未了结异常必须限在本运单内,否则别的运单的异常会挡住这次上报:" + sql); assertTrue(sql.contains("is_deleted"), "已删除的异常不算未了结:" + sql); + assertTrue(capturedUnresolvedParams().contains(WAYBILL_ID), + "运单条件取本次上报的运单:" + capturedUnresolvedParams()); } // ------------------------------------------------------------ 整单异常不受门禁 @@ -209,7 +218,7 @@ class ExceptionDisposalReportGateTest { @Test @DisplayName("不带节点的整单异常不受门禁约束,可有未了结整单异常仍继续上报") void waybillLevelReportIsNotGated() { - when(disposalMapper.selectList(any())).thenReturn(List.of(nodeException(null, "pending"))); + stubUnresolvedCount(1L); submitOk(null); @@ -218,6 +227,7 @@ class ExceptionDisposalReportGateTest { assertNull(captor.getValue().getNodeCode(), "整单异常没有节点,不得凭空补一个"); verify(driverWaybillService, never()).findPunchNode(any(), any()); verify(driverWaybillService, never()).isNodePunched(any(), any()); + verify(disposalMapper, never()).selectCount(any()); } @Test @@ -268,8 +278,33 @@ class ExceptionDisposalReportGateTest { when(driverWaybillService.isNodePunched(WAYBILL_ID, key)).thenReturn(false); } - private void stubUnresolvedNodeException(String nodeCode, String status) { - when(disposalMapper.selectList(any())).thenReturn(List.of(nodeException(nodeCode, status))); + private void stubUnresolvedCount(long count) { + when(disposalMapper.selectCount(any())).thenReturn(count); + } + + /** 上报门禁查未了结异常的那条 count 查询 */ + private LambdaQueryWrapper capturedUnresolvedQuery() { + ArgumentCaptor> captor = + ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(disposalMapper).selectCount(captor.capture()); + return captor.getValue(); + } + + /** 上述查询的参数值,集合型参数摊平成元素 */ + private List capturedUnresolvedParams() { + LambdaQueryWrapper query = capturedUnresolvedQuery(); + // MyBatis-Plus 的条件参数是惰性的:取过 SQL 片段之后 paramNameValuePairs 里才有值 + query.getTargetSql(); + Collection values = query.getParamNameValuePairs().values(); + List params = new ArrayList<>(); + for (Object value : values) { + if (value instanceof Collection nested) { + params.addAll(nested); + } else { + params.add(value); + } + } + return params; } private ServiceException submitExpectRejected(String nodeCode) { @@ -307,17 +342,6 @@ class ExceptionDisposalReportGateTest { return request; } - private ExceptionDisposal nodeException(String nodeCode, String status) { - ExceptionDisposal disposal = new ExceptionDisposal(); - disposal.setId(6101L); - disposal.setWaybillId(WAYBILL_ID); - disposal.setNodeCode(nodeCode); - disposal.setDisposalStatus(status); - disposal.setIsDeleted(0); - disposal.setReportTime(LocalDateTime.of(2026, 10, 6, 9, 30)); - return disposal; - } - private Map node(String key, String name, String type) { Map cfg = new HashMap<>(); cfg.put("key", key); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java index 477f70ca..3584712d 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/WaybillProcessSupportTest.java @@ -135,58 +135,41 @@ class WaybillProcessSupportTest { @Test @DisplayName("整单异常对每个节点都算相关,节点自己的异常只算它自己那个节点") - void relatedExceptionsCoverWaybillLevelForEveryNode() { - List nodeCodes = Arrays.asList("load", ""); - List ids = Arrays.asList(11L, 12L); - - assertEquals(Arrays.asList("11", "12"), WaybillProcessSupport.relatedExceptionIds("load", nodeCodes, ids)); - assertEquals(Collections.singletonList("12"), WaybillProcessSupport.relatedExceptionIds("sign", nodeCodes, ids), + void relatesToNodeCoversWaybillLevelForEveryNode() { + assertTrue(WaybillProcessSupport.relatesToNode("load", null), + "整单异常不归属任何节点,对每个节点都算相关"); + assertTrue(WaybillProcessSupport.relatesToNode("load", "")); + assertTrue(WaybillProcessSupport.relatesToNode("load", "load")); + assertFalse(WaybillProcessSupport.relatesToNode("sign", "load"), "装货的异常不归属签收——签收只是被它牵连阻断,不该显示成自己也出了事"); } @Test - @DisplayName("相关异常 id 保持入参顺序,即上报时间升序") - void relatedExceptionIdsKeepReportOrder() { - List nodeCodes = Arrays.asList("", "load"); - List ids = Arrays.asList(21L, 22L); - - assertEquals(Arrays.asList("21", "22"), WaybillProcessSupport.relatedExceptionIds("load", nodeCodes, ids)); + @DisplayName("节点编码忽略大小写,两侧空白不影响判定") + void relatesToNodeIgnoresCaseAndBlank() { + assertTrue(WaybillProcessSupport.relatesToNode("load", "LOAD")); + assertTrue(WaybillProcessSupport.relatesToNode(" load ", " LOAD ")); + assertFalse(WaybillProcessSupport.relatesToNode("load", "unload")); } @Test - @DisplayName("节点编码忽略大小写") - void relatedExceptionIdsIgnoreCase() { - assertEquals(Collections.singletonList("31"), - WaybillProcessSupport.relatedExceptionIds( - "load", Collections.singletonList("LOAD"), Collections.singletonList(31L))); - } - - @Test - @DisplayName("id 一律下发字符串:主键是 int64,前端直收数字会丢精度") - void relatedExceptionIdsAreStrings() { - List ids = WaybillProcessSupport.relatedExceptionIds( - "load", Collections.singletonList("load"), Collections.singletonList(9007199254740993L)); - - assertEquals(Collections.singletonList("9007199254740993"), ids); - } - - @Test - @DisplayName("整单异常的判定只看有没有不归属节点的异常") + @DisplayName("整单异常的判定只看节点编码是否为空") void waybillLevelExceptionFlag() { - assertTrue(WaybillProcessSupport.hasWaybillLevelException(Arrays.asList("load", ""))); - assertTrue(WaybillProcessSupport.hasWaybillLevelException(Collections.singletonList(null))); - assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.singletonList("load"))); - assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.emptyList())); - assertFalse(WaybillProcessSupport.hasWaybillLevelException(null)); + assertTrue(WaybillProcessSupport.isWaybillLevelException(null)); + assertTrue(WaybillProcessSupport.isWaybillLevelException("")); + assertTrue(WaybillProcessSupport.isWaybillLevelException(" ")); + assertFalse(WaybillProcessSupport.isWaybillLevelException("load")); } @Test - @DisplayName("异常全部了结后,整单标记与相关 id 都回到「无」") - void withoutUnresolvedExceptionsNothingIsTagged() { - assertFalse(WaybillProcessSupport.hasWaybillLevelException(Collections.emptyList())); - assertTrue(WaybillProcessSupport - .relatedExceptionIds("load", Collections.emptyList(), Collections.emptyList()) - .isEmpty()); + @DisplayName("节点编码相同判定:忽略大小写与两侧空白,空与空不算相等") + void sameNodeCodeIgnoresCaseAndBlank() { + assertTrue(WaybillProcessSupport.sameNodeCode("load", "LOAD")); + assertTrue(WaybillProcessSupport.sameNodeCode(" load ", "load")); + assertFalse(WaybillProcessSupport.sameNodeCode("load", "unload")); + assertFalse(WaybillProcessSupport.sameNodeCode("load", ""), "节点编码为空表示整单,不等于任何节点"); + assertFalse(WaybillProcessSupport.sameNodeCode("", ""), "空与空不算相等"); + assertFalse(WaybillProcessSupport.sameNodeCode(null, null)); } }