✨ 运单结束后拒绝司机打卡
已完成/已取消的运单不再接受在途与节点打卡,卡片仍下发但不可提交。
This commit is contained in:
+15
@@ -407,6 +407,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
// 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
|
// 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
|
||||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true);
|
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), true);
|
||||||
waybill = normalizeAcceptStatus(waybill);
|
waybill = normalizeAcceptStatus(waybill);
|
||||||
|
assertPunchNotEnded(waybill.getBusinessStatus());
|
||||||
|
|
||||||
String processJson = resolveProcessJson(waybill);
|
String processJson = resolveProcessJson(waybill);
|
||||||
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
||||||
@@ -456,6 +457,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
Driver driver = requireCurrentDriver();
|
Driver driver = requireCurrentDriver();
|
||||||
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
Waybill waybill = loadDriverWaybill(dto.getWaybillId());
|
||||||
waybill = normalizeAcceptStatus(waybill);
|
waybill = normalizeAcceptStatus(waybill);
|
||||||
|
assertPunchNotEnded(waybill.getBusinessStatus());
|
||||||
|
|
||||||
String processJson = resolveProcessJson(waybill);
|
String processJson = resolveProcessJson(waybill);
|
||||||
Map<String, Object> nodeCfg = findPunchNodeConfig(processJson, nodeCode);
|
Map<String, Object> nodeCfg = findPunchNodeConfig(processJson, nodeCode);
|
||||||
@@ -1318,6 +1320,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言运单未结束。
|
||||||
|
* <p>
|
||||||
|
* 运单已完成/已取消后不再接受司机打卡:在途与节点都拦,且先于其它门禁。
|
||||||
|
* 结束不可逆,没有解除途径——处理完异常、等到第二天都还是打不了。
|
||||||
|
*/
|
||||||
|
private void assertPunchNotEnded(String businessStatus) {
|
||||||
|
String reason = WaybillProcessSupport.punchEndedReason(businessStatus);
|
||||||
|
if (reason != null) {
|
||||||
|
throw new ServiceException(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 断言指定节点未被异常门禁阻断。
|
* 断言指定节点未被异常门禁阻断。
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
+29
@@ -133,6 +133,31 @@ public final class WaybillProcessSupport {
|
|||||||
return ACCEPT_REJECTED.equalsIgnoreCase(stringVal(driverAcceptStatus));
|
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) {
|
public static boolean isTerminalBusinessStatus(String businessStatus) {
|
||||||
return "draft".equals(businessStatus)
|
return "draft".equals(businessStatus)
|
||||||
|| "completed".equals(businessStatus)
|
|| "completed".equals(businessStatus)
|
||||||
@@ -198,6 +223,10 @@ public final class WaybillProcessSupport {
|
|||||||
int frequencyDays = parsePositiveInt(transit.get("frequencyDays"), 1);
|
int frequencyDays = parsePositiveInt(transit.get("frequencyDays"), 1);
|
||||||
String timeStart = normalizeHm(stringVal(transit.get("timeStart")), "00:00");
|
String timeStart = normalizeHm(stringVal(transit.get("timeStart")), "00:00");
|
||||||
String timeEnd = normalizeHm(stringVal(transit.get("timeEnd")), "23:59");
|
String timeEnd = normalizeHm(stringVal(transit.get("timeEnd")), "23:59");
|
||||||
|
// 结束不可逆,没有解除途径:卡片仍下发,但不可提交,也不算今日待打
|
||||||
|
if (isPunchEnded(businessStatus)) {
|
||||||
|
return new TransitCheckinDecision(false, true, false, false, frequencyDays, timeStart, timeEnd);
|
||||||
|
}
|
||||||
// punch=是即展示卡片;非进行中仅不可作为「今日待打」
|
// punch=是即展示卡片;非进行中仅不可作为「今日待打」
|
||||||
if (!STATUS_RUNNING.equals(businessStatus)) {
|
if (!STATUS_RUNNING.equals(businessStatus)) {
|
||||||
return new TransitCheckinDecision(true, true, false, false, frequencyDays, timeStart, timeEnd);
|
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.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,4 +334,66 @@ class WaybillProcessSupportTest {
|
|||||||
"TRANSPORT_JSON 的回单启用且没有 confirmMode:缺字段当没配「无需确认完成」,否则会跳过本该人工确认的回单");
|
"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