From aa0226e77bda016e72b56479632795e113ccd636 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Tue, 20 Oct 2026 14:37:12 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8D=95=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E5=90=8E=E6=8B=92=E7=BB=9D=E5=8F=B8=E6=9C=BA=E6=89=93?= =?UTF-8?q?=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已完成/已取消的运单不再接受在途与节点打卡,卡片仍下发但不可提交。 --- .../impl/DriverWaybillServiceImpl.java | 15 +++++ .../support/WaybillProcessSupport.java | 29 ++++++++ .../support/WaybillProcessSupportTest.java | 66 +++++++++++++++++++ 3 files changed, 110 insertions(+) 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 59e02bb0..1c23ccbc 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 @@ -407,6 +407,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { // 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明) Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true); waybill = normalizeAcceptStatus(waybill); + assertPunchNotEnded(waybill.getBusinessStatus()); String processJson = resolveProcessJson(waybill); Date lastPunchAt = findLastPunchTime(waybill.getId()); @@ -456,6 +457,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { Driver driver = requireCurrentDriver(); Waybill waybill = loadDriverWaybill(dto.getWaybillId()); waybill = normalizeAcceptStatus(waybill); + assertPunchNotEnded(waybill.getBusinessStatus()); String processJson = resolveProcessJson(waybill); Map nodeCfg = findPunchNodeConfig(processJson, nodeCode); @@ -1318,6 +1320,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { .orElse(null); } + /** + * 断言运单未结束。 + *

+ * 运单已完成/已取消后不再接受司机打卡:在途与节点都拦,且先于其它门禁。 + * 结束不可逆,没有解除途径——处理完异常、等到第二天都还是打不了。 + */ + private void assertPunchNotEnded(String businessStatus) { + String reason = WaybillProcessSupport.punchEndedReason(businessStatus); + if (reason != null) { + throw new ServiceException(reason); + } + } + /** * 断言指定节点未被异常门禁阻断。 *

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 1b273c0b..76c0f0bf 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 @@ -133,6 +133,31 @@ public final class WaybillProcessSupport { return ACCEPT_REJECTED.equalsIgnoreCase(stringVal(driverAcceptStatus)); } + /** + * 运单已结束:已完成或已取消。此后不再接受司机打卡。 + *

+ * 只认这两个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡, + * 所以不能按 {@link #isTerminalBusinessStatus} 的「终态或调度中间态」口径判定。 + */ + public static boolean isPunchEnded(String businessStatus) { + String status = stringVal(businessStatus); + return "completed".equalsIgnoreCase(status) || "cancelled".equalsIgnoreCase(status); + } + + /** + * 结束态下的打卡拒绝文案;非结束态返回 null。 + */ + public static String punchEndedReason(String businessStatus) { + String status = stringVal(businessStatus); + if ("completed".equalsIgnoreCase(status)) { + return "运单已完成"; + } + if ("cancelled".equalsIgnoreCase(status)) { + return "运单已取消"; + } + return null; + } + public static boolean isTerminalBusinessStatus(String businessStatus) { return "draft".equals(businessStatus) || "completed".equals(businessStatus) @@ -198,6 +223,10 @@ public final class WaybillProcessSupport { int frequencyDays = parsePositiveInt(transit.get("frequencyDays"), 1); String timeStart = normalizeHm(stringVal(transit.get("timeStart")), "00:00"); String timeEnd = normalizeHm(stringVal(transit.get("timeEnd")), "23:59"); + // 结束不可逆,没有解除途径:卡片仍下发,但不可提交,也不算今日待打 + if (isPunchEnded(businessStatus)) { + return new TransitCheckinDecision(false, true, false, false, frequencyDays, timeStart, timeEnd); + } // punch=是即展示卡片;非进行中仅不可作为「今日待打」 if (!STATUS_RUNNING.equals(businessStatus)) { return new TransitCheckinDecision(true, true, false, false, frequencyDays, timeStart, timeEnd); 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 5583f975..c629f038 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 @@ -3,14 +3,18 @@ package org.springblade.transport.support; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -330,4 +334,66 @@ class WaybillProcessSupportTest { "TRANSPORT_JSON 的回单启用且没有 confirmMode:缺字段当没配「无需确认完成」,否则会跳过本该人工确认的回单"); } + // ------------------------------------------------------------ 结束门禁 + + @Test + @DisplayName("结束门禁:只认已完成与已取消,大小写不敏感") + void punchEndedOnlyForCompletedAndCancelled() { + assertTrue(WaybillProcessSupport.isPunchEnded("completed")); + assertTrue(WaybillProcessSupport.isPunchEnded("COMPLETED")); + assertTrue(WaybillProcessSupport.isPunchEnded("cancelled")); + assertTrue(WaybillProcessSupport.isPunchEnded("Cancelled")); + } + + @Test + @DisplayName("结束门禁:草稿、待执行、待调度、调度中都还能打卡,不算结束") + void punchEndedExcludesStatusesThatStillAllowPunching() { + for (String status : List.of("draft", "pending", "waiting_dispatch", "dispatching", "running")) { + assertFalse(WaybillProcessSupport.isPunchEnded(status), status + " 的司机仍要能打卡"); + assertNull(WaybillProcessSupport.punchEndedReason(status), status + " 不该有结束文案"); + } + assertFalse(WaybillProcessSupport.isPunchEnded(null)); + assertFalse(WaybillProcessSupport.isPunchEnded("")); + assertFalse(WaybillProcessSupport.isPunchEnded(" ")); + } + + @Test + @DisplayName("结束门禁文案:完成与取消各一句,非结束态没有文案") + void punchEndedReasonOnlyForEndedStatuses() { + assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason("completed")); + assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason(" COMPLETED ")); + assertEquals("运单已取消", WaybillProcessSupport.punchEndedReason("cancelled")); + assertNull(WaybillProcessSupport.punchEndedReason(null)); + } + + @Test + @DisplayName("在途卡:已完成的运单今天打过卡也不可再打,且不算今日已打") + void transitCardOnEndedWaybillIsVisibleButNotPunchable() { + LocalDateTime now = LocalDateTime.of(2026, 10, 20, 12, 0); + // 打卡时间就落在 now 所在的那一天 + Date lastPunchAt = Date.from(now.toLocalDate().atStartOfDay(ZoneId.systemDefault()).plusHours(8).toInstant()); + + WaybillProcessSupport.TransitCheckinDecision decision = + WaybillProcessSupport.evaluateTransitCheckin(TRANSPORT_JSON, "completed", lastPunchAt, now); + + assertTrue(decision.visible(), "卡片仍要下发,司机能回看今天打过没有"); + assertFalse(decision.punchEnabled(), "运单已结束,不能提交打卡"); + assertFalse(decision.doneToday(), "结束不是「今日已打卡」,两者的后续动作不同"); + assertFalse(decision.dueToday()); + } + + @Test + @DisplayName("在途卡回归:进行中且今天没打卡时仍可打") + void transitCardOnRunningWaybillStaysPunchable() { + LocalDateTime now = LocalDateTime.of(2026, 10, 20, 12, 0); + + WaybillProcessSupport.TransitCheckinDecision decision = + WaybillProcessSupport.evaluateTransitCheckin(TRANSPORT_JSON, "running", null, now); + + assertTrue(decision.visible()); + assertTrue(decision.punchEnabled()); + assertTrue(decision.dueToday(), "frequencyDays 默认为 1、无历史打卡,且 noon 落在默认时段内"); + assertFalse(decision.doneToday()); + } + }