✨ 运单结束后拒绝司机打卡
已完成/已取消的运单不再接受在途与节点打卡,卡片仍下发但不可提交。
This commit is contained in:
+15
@@ -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<String, Object> nodeCfg = findPunchNodeConfig(processJson, nodeCode);
|
||||
@@ -1318,6 +1320,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言运单未结束。
|
||||
* <p>
|
||||
* 运单已完成/已取消后不再接受司机打卡:在途与节点都拦,且先于其它门禁。
|
||||
* 结束不可逆,没有解除途径——处理完异常、等到第二天都还是打不了。
|
||||
*/
|
||||
private void assertPunchNotEnded(String businessStatus) {
|
||||
String reason = WaybillProcessSupport.punchEndedReason(businessStatus);
|
||||
if (reason != null) {
|
||||
throw new ServiceException(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言指定节点未被异常门禁阻断。
|
||||
* <p>
|
||||
|
||||
+29
@@ -133,6 +133,31 @@ public final class WaybillProcessSupport {
|
||||
return ACCEPT_REJECTED.equalsIgnoreCase(stringVal(driverAcceptStatus));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运单已结束:已完成或已取消。此后不再接受司机打卡。
|
||||
* <p>
|
||||
* 只认这两个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡,
|
||||
* 所以不能按 {@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);
|
||||
|
||||
+66
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user