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 7682948d..8de4c8b9 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 @@ -53,6 +53,7 @@ public final class WaybillProcessSupport { public static final String ACCEPT_REJECTED = "rejected"; private static final String CONFIRM_YES = "yes"; private static final String CONFIRM_NO_ACCEPT = "no_confirm_accept"; + private static final String CONFIRM_NO_COMPLETE = "no_confirm_complete"; private static final String NODE_TRANSIT = "transit"; private static final String NODE_TRANSIT_NAME = "在途"; /** 异常处置终态:completed=已完成,revoked=已撤销。只有终态解除打卡门禁 */ @@ -244,6 +245,21 @@ public final class WaybillProcessSupport { return result; } + /** + * 本次打卡的节点是否是司机可打卡节点的末位。 + *

+ * 末位取「司机可打卡节点」序列:接单与回单不打卡,不参与排位—— + * 回单只要启用就必然排在配置最末,按全部启用节点取末位会永远取到回单。 + */ + public static boolean isLastDriverPunchNode(String processJson, String nodeCode) { + List> nodes = listDriverPunchNodes(processJson); + if (nodes.isEmpty()) { + return false; + } + String code = stringVal(nodeCode); + return !code.isEmpty() && code.equals(nodeKey(nodes.get(nodes.size() - 1))); + } + /** * 启用中的过程节点(按配置顺序,含接单/回单)。 */ @@ -591,6 +607,36 @@ public final class WaybillProcessSupport { return isAcceptNode(stringVal(node.get("key")), stringVal(node.get("name"))); } + /** + * 是否到货 / 卸货 / 签收节点:key 或中文名任一命中即算。 + */ + public static boolean isDeliveryNode(String nodeCode, String nodeName) { + String key = stringVal(nodeCode); + String name = stringVal(nodeName); + return "arrival".equals(key) || "到货".equals(name) + || "unload".equals(key) || "卸货".equals(name) + || "sign".equals(key) || "签收".equals(name); + } + + /** + * 回单是否无需确认完成:配置里没有回单节点、回单未启用,或启用且配置为「无需确认完成」。 + *

+ * 未启用的回单节点不看 confirmMode——没有回单环节就不会有人来确认; + * 启用但 confirmMode 缺失按「需确认完成」算。 + */ + public static boolean returnNeedsNoConfirmation(String processJson) { + for (Map node : parseProcessNodes(processJson)) { + if (!isReturnNode(node)) { + continue; + } + if (!isEnabled(node)) { + return true; + } + return CONFIRM_NO_COMPLETE.equals(stringVal(node.get("confirmMode"))); + } + return true; + } + private static boolean isTransitNode(Map node) { String key = stringVal(node.get("key")); String name = stringVal(node.get("name")); 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 3584712d..fa3334c2 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 @@ -172,4 +172,144 @@ class WaybillProcessSupportTest { assertFalse(WaybillProcessSupport.sameNodeCode(null, null)); } + // ------------------------------------------------------------ 到货/卸货/签收 + + @Test + @DisplayName("到货/卸货/签收:key 或中文名任一命中即可,两者都为空不算") + void deliveryNodeMatchesKeyOrName() { + assertTrue(WaybillProcessSupport.isDeliveryNode("arrival", "到货")); + assertTrue(WaybillProcessSupport.isDeliveryNode("unload", "卸货")); + assertTrue(WaybillProcessSupport.isDeliveryNode("sign", "签收")); + assertTrue(WaybillProcessSupport.isDeliveryNode("arrival", ""), "只有 key 也要认,别把中文名当必要条件"); + assertTrue(WaybillProcessSupport.isDeliveryNode("", "签收"), "只有中文名也要认——key 可能被改成自定义编码"); + assertFalse(WaybillProcessSupport.isDeliveryNode("dispatch", "发货")); + assertFalse(WaybillProcessSupport.isDeliveryNode("transit", "在途")); + assertFalse(WaybillProcessSupport.isDeliveryNode("", "")); + assertFalse(WaybillProcessSupport.isDeliveryNode(null, null)); + } + + // ------------------------------------------------------------ 末位司机打卡节点 + + /** 末位是「到货」,其后没有打卡节点 */ + private static final String ARRIVAL_LAST_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"arrival","name":"到货","enabled":true,"punch":true}] + """; + + /** 末位是「卸货」 */ + private static final String UNLOAD_LAST_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"unload","name":"卸货","enabled":true,"punch":true}] + """; + + /** 末位是「在途」 */ + private static final String TRANSIT_LAST_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"transit","name":"在途","type":"transit","enabled":true,"punch":true}] + """; + + /** 末位是「发货」 */ + private static final String DISPATCH_LAST_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"dispatch","name":"发货","enabled":true,"punch":true}] + """; + + @Test + @DisplayName("末位打卡节点取「司机可打卡节点」的末位:接单与回单就算启用也不算") + void lastDriverPunchNodeSkipsAcceptAndReturn() { + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "sign"), + "回单启用在配置里排最末,但回单不打卡,末位打卡节点仍是签收"); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "unload")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "arrival")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "return")); + } + + @Test + @DisplayName("末位是签收 / 到货 / 卸货才同时成立「末位」与「到货卸货签收」;末位是在途 / 发货只满足前者") + void lastNodeIsDeliveryOnlyForDeliveryTypes() { + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "sign") + && WaybillProcessSupport.isDeliveryNode("sign", "签收")); + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(ARRIVAL_LAST_JSON, "arrival") + && WaybillProcessSupport.isDeliveryNode("arrival", "到货")); + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(UNLOAD_LAST_JSON, "unload") + && WaybillProcessSupport.isDeliveryNode("unload", "卸货")); + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(TRANSIT_LAST_JSON, "transit"), + "在途排在末位时它确实是末位打卡节点"); + assertFalse(WaybillProcessSupport.isDeliveryNode("transit", "在途"), "在途不是可完成节点"); + assertTrue(WaybillProcessSupport.isLastDriverPunchNode(DISPATCH_LAST_JSON, "dispatch")); + assertFalse(WaybillProcessSupport.isDeliveryNode("dispatch", "发货"), "发货不是可完成节点"); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(DISPATCH_LAST_JSON, "unload"), + "该配置里没有卸货节点,不能因为编码眼熟就认下"); + } + + @Test + @DisplayName("没有可打卡节点(或没有过程配置)时,谁都不是末位") + void lastDriverPunchNodeWithoutPunchNodes() { + String onlyAcceptAndReturn = """ + [{"key":"accept","name":"接单","enabled":true,"punch":true}, + {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true}] + """; + + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(onlyAcceptAndReturn, "accept")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(onlyAcceptAndReturn, "return")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode("", "sign")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(null, "sign")); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, null)); + assertFalse(WaybillProcessSupport.isLastDriverPunchNode(TRANSPORT_JSON, "")); + } + + // ------------------------------------------------------------ 回单是否无需确认完成 + + /** 回单启用且配置为「无需确认完成」 */ + private static final String RETURN_NO_CONFIRM_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"sign","name":"签收","enabled":true,"punch":true}, + {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"no_confirm_complete"}] + """; + + /** 回单启用且配置为「需确认完成」 */ + private static final String RETURN_NEED_CONFIRM_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"sign","name":"签收","enabled":true,"punch":true}, + {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}] + """; + + /** 回单未启用,但 confirmMode 写着「需确认完成」 */ + private static final String RETURN_DISABLED_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"sign","name":"签收","enabled":true,"punch":true}, + {"key":"return","name":"回单","type":"confirm","enabled":false,"punch":true,"confirmMode":"yes"}] + """; + + /** 配置里根本没有回单节点 */ + private static final String NO_RETURN_JSON = """ + [{"key":"load","name":"装货","enabled":true,"punch":true}, + {"key":"sign","name":"签收","enabled":true,"punch":true}] + """; + + @Test + @DisplayName("回单启用时要看 confirmMode:只有「无需确认完成」才成立") + void enabledReturnNeedsConfirmMode() { + assertTrue(WaybillProcessSupport.returnNeedsNoConfirmation(RETURN_NO_CONFIRM_JSON)); + assertFalse(WaybillProcessSupport.returnNeedsNoConfirmation(RETURN_NEED_CONFIRM_JSON), + "回单需要确认完成时,打完签收不能算整单完成"); + } + + @Test + @DisplayName("回单未启用、或配置里没有回单节点,一律视为无需确认完成") + void absentOrDisabledReturnNeedsNoConfirmation() { + assertTrue(WaybillProcessSupport.returnNeedsNoConfirmation(RETURN_DISABLED_JSON), + "回单未启用时 confirmMode 写着需确认也不算数:没启用的环节不会有人来确认"); + assertTrue(WaybillProcessSupport.returnNeedsNoConfirmation(NO_RETURN_JSON)); + assertTrue(WaybillProcessSupport.returnNeedsNoConfirmation("")); + assertTrue(WaybillProcessSupport.returnNeedsNoConfirmation(null)); + } + + @Test + @DisplayName("回单启用但 confirmMode 缺失时按「需确认完成」算") + void enabledReturnWithoutConfirmModeNeedsConfirmation() { + assertFalse(WaybillProcessSupport.returnNeedsNoConfirmation(TRANSPORT_JSON), + "TRANSPORT_JSON 的回单启用且没有 confirmMode:缺字段当没配「无需确认完成」,否则会跳过本该人工确认的回单"); + } + }