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 4bca6317..691cb573 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 @@ -49,4 +49,7 @@ public class DriverNodePunchVO implements Serializable { @Schema(description = "数量") private String quantity; + @Schema(description = "本次打卡是否使运单自动完成") + private Boolean waybillCompleted; + } 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 72914b8b..a5aad0ef 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 @@ -360,7 +360,20 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { nodePunchMapper.insert(punch); advanceCurrentProcessNode(waybill, nodeCfg); - return toNodePunchVO(punch); + // 末位打卡节点是到货/卸货/签收、且回单无需确认完成时,本次打卡一并把运单置为完成; + // 判定与找节点共用上面已解析的那份过程配置,不重新解析。 + String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg); + String nodeName = WaybillProcessSupport.nodeName(nodeCfg); + boolean completed = false; + if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey) + && WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName) + && WaybillProcessSupport.returnNeedsNoConfirmation(processJson) + && canAutoCompleteWaybill(waybill)) { + completed = waybillService.completeWithoutDeptCheck(waybill.getId()); + } + DriverNodePunchVO vo = toNodePunchVO(punch); + vo.setWaybillCompleted(completed); + return vo; } @Override @@ -1022,6 +1035,20 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return countByType; } + /** + * 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。 + *
+ * 不满足返回 false,由调用方静默跳过,打卡不受影响。 + */ + private boolean canAutoCompleteWaybill(Waybill waybill) { + String status = waybill.getBusinessStatus(); + boolean statusAllowed = STATUS_RUNNING.equals(status) + || STATUS_PENDING.equals(status) + || STATUS_WAITING_DISPATCH.equals(status) + || STATUS_DISPATCHING.equals(status); + return statusAllowed && unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty(); + } + /** * 断言该运单没有未了结的异常。 *
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 new file mode 100644 index 00000000..d7b86e23 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java @@ -0,0 +1,248 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +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.MockedStatic; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.transport.mapper.ExceptionDisposalMapper; +import org.springblade.transport.mapper.WaybillEnroutePunchMapper; +import org.springblade.transport.mapper.WaybillNodePunchMapper; +import org.springblade.transport.pojo.dto.NodeSubmitDTO; +import org.springblade.transport.pojo.dto.PunchPhotoDTO; +import org.springblade.transport.pojo.entity.Driver; +import org.springblade.transport.pojo.entity.ExceptionDisposal; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.entity.WaybillNodePunch; +import org.springblade.transport.pojo.vo.DriverNodePunchVO; +import org.springblade.transport.service.IDriverService; +import org.springblade.transport.service.IProcessConfigService; +import org.springblade.transport.service.ITransportVehicleService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.CertificationGate; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 司机打完末位打卡节点后运单自动完成。 + *
+ * 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、
+ * 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水,
+ * 响应里的 waybillCompleted 始终取完成接口的返回值,前端据此决定是否提示「运单已完成」。
+ */
+class DriverWaybillAutoCompleteTest {
+
+ private static final Long WAYBILL_ID = 5801L;
+ private static final Long USER_ID = 7001L;
+ private static final String PLATE = "桂AAW3255";
+ private static final String CLOCK_PHOTO = "https://oss/clock.jpg";
+
+ /** 末位打卡节点是「签收」,没有回单节点 */
+ private static final String SIGN_LAST_JSON = """
+ [{"key":"sign","name":"签收","type":"cargo","enabled":true,"punch":true}]
+ """;
+
+ /** 末位打卡节点是「签收」,回单启用且需确认完成 */
+ private static final String SIGN_LAST_RETURN_CONFIRM_JSON = """
+ [{"key":"sign","name":"签收","type":"cargo","enabled":true,"punch":true},
+ {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}]
+ """;
+
+ /** 末位打卡节点是「发货」,不是到货/卸货/签收 */
+ private static final String DISPATCH_LAST_JSON = """
+ [{"key":"dispatch","name":"发货","type":"cargo","enabled":true,"punch":true}]
+ """;
+
+ private IWaybillService waybillService;
+ private IDriverService driverService;
+ private WaybillNodePunchMapper nodePunchMapper;
+ private ExceptionDisposalMapper exceptionDisposalMapper;
+ private DriverWaybillServiceImpl service;
+
+ @BeforeAll
+ static void initTableInfo() {
+ // 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
+ assistant.setCurrentNamespace("test");
+ TableInfoHelper.initTableInfo(assistant, Waybill.class);
+ TableInfoHelper.initTableInfo(assistant, Driver.class);
+ TableInfoHelper.initTableInfo(assistant, WaybillNodePunch.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ waybillService = mock(IWaybillService.class);
+ driverService = mock(IDriverService.class);
+ nodePunchMapper = mock(WaybillNodePunchMapper.class);
+ exceptionDisposalMapper = mock(ExceptionDisposalMapper.class);
+ service = new DriverWaybillServiceImpl(
+ waybillService,
+ driverService,
+ mock(IProcessConfigService.class),
+ mock(ITransportVehicleService.class),
+ mock(CertificationGate.class),
+ mock(WaybillEnroutePunchMapper.class),
+ nodePunchMapper,
+ exceptionDisposalMapper);
+
+ when(driverService.getOne(any())).thenReturn(driver());
+ when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
+ when(waybillService.updateById(any())).thenReturn(true);
+ when(nodePunchMapper.selectList(any())).thenReturn(List.of());
+ when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of());
+ when(nodePunchMapper.insert(any(WaybillNodePunch.class))).thenReturn(1);
+ }
+
+ // ------------------------------------------------------------ 命中
+
+ @Test
+ @DisplayName("末位签收打卡且回单无需确认时调完成接口,响应标记 waybillCompleted")
+ void lastDeliveryPunchCompletesWaybill() {
+ givenWaybill(SIGN_LAST_JSON, "running");
+ when(waybillService.completeWithoutDeptCheck(WAYBILL_ID)).thenReturn(true);
+
+ DriverNodePunchVO vo = submit("sign");
+
+ verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
+ assertTrue(vo.getWaybillCompleted(), "完成接口返回成功,响应要告诉前端运单已完成");
+ }
+
+ @Test
+ @DisplayName("waybillCompleted 取完成接口的返回值,不按判定命中臆断")
+ void completionFlagFollowsServiceReturn() {
+ givenWaybill(SIGN_LAST_JSON, "running");
+ when(waybillService.completeWithoutDeptCheck(WAYBILL_ID)).thenReturn(false);
+
+ DriverNodePunchVO vo = submit("sign");
+
+ verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
+ assertFalse(vo.getWaybillCompleted(), "完成接口自己说不算完成时,前端不能提示运单已完成");
+ }
+
+ // ------------------------------------------------------------ 不命中
+
+ @Test
+ @DisplayName("末位是发货时只打卡不完成")
+ void punchOnNonDeliveryLastNodeDoesNotComplete() {
+ givenWaybill(DISPATCH_LAST_JSON, "running");
+
+ DriverNodePunchVO vo = submit("dispatch");
+
+ verify(waybillService, never()).completeWithoutDeptCheck(any());
+ verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
+ assertFalse(vo.getWaybillCompleted());
+ }
+
+ @Test
+ @DisplayName("回单启用且需确认完成时只打卡不完成")
+ void returnNeedingConfirmationDoesNotComplete() {
+ 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());
+ }
+
+ // ------------------------------------------------------------ 前置:运单本身能否完成
+
+ @Test
+ @DisplayName("业务状态为 draft 时只打卡不完成,打卡照常成功")
+ void draftWaybillIsPunchedButNotCompleted() {
+ givenWaybill(SIGN_LAST_JSON, "draft");
+
+ DriverNodePunchVO vo = submit("sign");
+
+ verify(waybillService, never()).completeWithoutDeptCheck(any());
+ verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
+ assertFalse(vo.getWaybillCompleted());
+ }
+
+ @Test
+ @DisplayName("待执行与调度中间态同样允许自动完成")
+ void dispatchableStatusesStillComplete() {
+ for (String status : List.of("pending", "waiting_dispatch", "dispatching")) {
+ givenWaybill(SIGN_LAST_JSON, status);
+ when(waybillService.completeWithoutDeptCheck(WAYBILL_ID)).thenReturn(true);
+
+ DriverNodePunchVO vo = submit("sign");
+
+ assertTrue(vo.getWaybillCompleted(), status + " 属于可完成状态");
+ }
+ }
+
+ @Test
+ @DisplayName("有未了结异常时不自动完成——与司机端「完成运单」同一口径,异常不落在打卡节点上也拦")
+ void unresolvedExceptionBlocksAutoComplete() {
+ givenWaybill(SIGN_LAST_JSON, "running");
+ when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of(unresolvedException("unload")));
+
+ DriverNodePunchVO vo = submit("sign");
+
+ verify(waybillService, never()).completeWithoutDeptCheck(any());
+ verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
+ assertFalse(vo.getWaybillCompleted());
+ }
+
+ // ------------------------------------------------------------ 夹具
+
+ private DriverNodePunchVO submit(String nodeCode) {
+ NodeSubmitDTO dto = new NodeSubmitDTO();
+ dto.setWaybillId(WAYBILL_ID);
+ dto.setNodeCode(nodeCode);
+ dto.setPhotoItems(List.of(photo("打卡照片", CLOCK_PHOTO)));
+ try (MockedStatic