✅ 锁定末位打卡回单需确认时进入待回单确认

This commit is contained in:
2026-10-22 13:46:03 +08:00
parent 311e7ff94a
commit 3e60c0720b
2 changed files with 99 additions and 10 deletions
@@ -52,4 +52,7 @@ public class DriverNodePunchVO implements Serializable {
@Schema(description = "本次打卡是否使运单自动完成")
private Boolean waybillCompleted;
@Schema(description = "本次打卡后的运单业务状态:running / pending_return_confirm / completed")
private String waybillBusinessStatus;
}
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.secure.utils.AuthUtil;
@@ -28,6 +29,7 @@ import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.support.CertificationGate;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -42,11 +45,13 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 司机打完末位打卡节点后运单自动完成。
* 司机打完末位打卡节点后运单的去向。
* <p>
* 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、
* 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水,
* 响应里的 waybillCompleted 始终取完成接口的返回值,前端据此决定是否提示「运单已完成」。
* 末位打卡是到货/卸货/签收、运单本身允许完成时,回单是否需业务员确认决定去向:
* 无需确认则本次一并完成运单,需确认则把运单置为待回单确认等业务员动手。
* 任一条件不成立都只落打卡流水。
* 响应里的 waybillCompleted 始终取完成接口的返回值,前端据此决定是否提示「运单已完成」;
* 待回单确认不是已完成,只能靠 waybillBusinessStatus 分辨。
*/
class DriverWaybillAutoCompleteTest {
@@ -72,6 +77,12 @@ class DriverWaybillAutoCompleteTest {
[{"key":"dispatch","name":"发货","type":"cargo","enabled":true,"punch":true}]
""";
/** 末位司机打卡节点是「发货」,回单启用且需确认完成 */
private static final String DISPATCH_LAST_RETURN_CONFIRM_JSON = """
[{"key":"dispatch","name":"发货","type":"cargo","enabled":true,"punch":true},
{"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}]
""";
private IWaybillService waybillService;
private IDriverService driverService;
private WaybillNodePunchMapper nodePunchMapper;
@@ -118,7 +129,7 @@ class DriverWaybillAutoCompleteTest {
// ------------------------------------------------------------ 命中
@Test
@DisplayName("末位签收打卡且回单无需确认时调完成接口,响应标记 waybillCompleted")
@DisplayName("末位签收打卡且回单无需确认时调完成接口,响应标记 waybillCompleted 与 completed")
void lastDeliveryPunchCompletesWaybill() {
givenWaybill(SIGN_LAST_JSON, "running");
when(waybillService.completeWithoutDeptCheck(WAYBILL_ID)).thenReturn(true);
@@ -127,6 +138,7 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
assertTrue(vo.getWaybillCompleted(), "完成接口返回成功,响应要告诉前端运单已完成");
assertEquals("completed", vo.getWaybillBusinessStatus(), "完成后运单业务状态要如实回报");
}
@Test
@@ -139,6 +151,25 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
assertFalse(vo.getWaybillCompleted(), "完成接口自己说不算完成时,前端不能提示运单已完成");
assertEquals("running", vo.getWaybillBusinessStatus(), "完成没落库,运单仍在进行中");
}
// ------------------------------------------------------------ 待回单确认
@Test
@DisplayName("回单需业务员确认时末位打卡进入待回单确认,落库并回报,且不调完成接口")
void returnNeedingConfirmationEntersPendingReturnConfirm() {
givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
DriverNodePunchVO vo = submit("sign");
verify(waybillService, never()).completeWithoutDeptCheck(any());
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
assertFalse(vo.getWaybillCompleted(), "待回单确认不是已完成");
assertEquals("pending_return_confirm", vo.getWaybillBusinessStatus(),
"客户端要知道打卡已提交、等业务员确认回单");
assertEquals("pending_return_confirm", writtenBusinessStatus(),
"状态要落库,否则下次查询又回到进行中");
}
// ------------------------------------------------------------ 不命中
@@ -153,18 +184,31 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService, never()).completeWithoutDeptCheck(any());
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
assertFalse(vo.getWaybillCompleted());
assertEquals("running", vo.getWaybillBusinessStatus());
}
@Test
@DisplayName("回单启用且需确认完成时只打卡不完成")
void returnNeedingConfirmationDoesNotComplete() {
@DisplayName("回单需确认但不是末位打卡:运单留在进行中")
void nonLastPunchDoesNotEnterReturnConfirmation() {
givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
DriverNodePunchVO vo = submit("sign");
DriverNodePunchVO vo = submit("load");
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
assertEquals("running", vo.getWaybillBusinessStatus(), "后面还有签收要打,不能提前停在待回单确认");
assertEquals("running", writtenBusinessStatus());
}
@Test
@DisplayName("回单需确认但末位不是到货/卸货/签收:运单留在进行中")
void nonDeliveryLastNodeDoesNotEnterReturnConfirmation() {
givenWaybill(DISPATCH_LAST_RETURN_CONFIRM_JSON, "running");
DriverNodePunchVO vo = submit("dispatch");
verify(waybillService, never()).completeWithoutDeptCheck(any());
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
assertFalse(vo.getWaybillCompleted());
assertEquals("running", vo.getWaybillBusinessStatus(), "发货不是到货类节点,打完不该停在等待回单");
assertEquals("running", writtenBusinessStatus());
}
// ------------------------------------------------------------ 前置:运单本身能否完成
@@ -208,8 +252,50 @@ class DriverWaybillAutoCompleteTest {
assertFalse(vo.getWaybillCompleted());
}
@Test
@DisplayName("有未了结异常时回单需确认也不进待回单确认——四条进入条件共同成立才进")
void unresolvedExceptionBlocksEnteringReturnConfirmation() {
givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of(unresolvedException("unload")));
DriverNodePunchVO vo = submit("sign");
assertEquals("running", vo.getWaybillBusinessStatus(), "异常未了结,不能把运单停在等待回单");
assertEquals("running", writtenBusinessStatus());
}
// ------------------------------------------------------------ 并发:只从进行中进入
@Test
@DisplayName("打卡期间运单已被推到终态时不再写待回单确认——只从进行中进入")
void concurrentTerminalStatusPreventsEnteringReturnConfirmation() {
givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
List<String> statusesWritten = new ArrayList<>();
// 模拟另一事务在这次打卡推进过程节点时把运单置为已完成(管理端强制完成)
when(waybillService.updateById(any())).thenAnswer(invocation -> {
Waybill entity = invocation.getArgument(0);
statusesWritten.add(entity.getBusinessStatus());
entity.setBusinessStatus("completed");
return true;
});
DriverNodePunchVO vo = submit("sign");
assertFalse(statusesWritten.contains("pending_return_confirm"),
"运单已被完成,不能再改写回待回单确认");
assertEquals("completed", vo.getWaybillBusinessStatus());
}
// ------------------------------------------------------------ 夹具
/** 打卡流程最后一次落库时运单的业务状态;状态写库走 updateById,各次传的是同一个实体引用 */
private String writtenBusinessStatus() {
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
verify(waybillService, atLeastOnce()).updateById(captor.capture());
List<Waybill> written = captor.getAllValues();
return written.get(written.size() - 1).getBusinessStatus();
}
private DriverNodePunchVO submit(String nodeCode) {
NodeSubmitDTO dto = new NodeSubmitDTO();
dto.setWaybillId(WAYBILL_ID);