diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java index 691cb573..d5d10b60 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java @@ -52,4 +52,7 @@ public class DriverNodePunchVO implements Serializable { @Schema(description = "本次打卡是否使运单自动完成") private Boolean waybillCompleted; + @Schema(description = "本次打卡后的运单业务状态:running / pending_return_confirm / completed") + private String waybillBusinessStatus; + } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java index f2eeac76..3c1f83fe 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java @@ -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; /** - * 司机打完末位打卡节点后运单自动完成。 + * 司机打完末位打卡节点后运单的去向。 *

- * 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、 - * 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水, - * 响应里的 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 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 captor = ArgumentCaptor.forClass(Waybill.class); + verify(waybillService, atLeastOnce()).updateById(captor.capture()); + List written = captor.getAllValues(); + return written.get(written.size() - 1).getBusinessStatus(); + } + private DriverNodePunchVO submit(String nodeCode) { NodeSubmitDTO dto = new NodeSubmitDTO(); dto.setWaybillId(WAYBILL_ID);