✨ 司机打完末位到货卸货签收节点且回单无需确认时自动完成运单
This commit is contained in:
+3
@@ -49,4 +49,7 @@ public class DriverNodePunchVO implements Serializable {
|
||||
@Schema(description = "数量")
|
||||
private String quantity;
|
||||
|
||||
@Schema(description = "本次打卡是否使运单自动完成")
|
||||
private Boolean waybillCompleted;
|
||||
|
||||
}
|
||||
|
||||
+28
-1
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。
|
||||
* <p>
|
||||
* 不满足返回 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言该运单没有未了结的异常。
|
||||
* <p>
|
||||
|
||||
+248
@@ -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;
|
||||
|
||||
/**
|
||||
* 司机打完末位打卡节点后运单自动完成。
|
||||
* <p>
|
||||
* 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、
|
||||
* 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水,
|
||||
* 响应里的 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<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
return service.submitNode(dto);
|
||||
}
|
||||
}
|
||||
|
||||
private void givenWaybill(String processJson, String businessStatus) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610080001");
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setProcessJson(processJson);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setIsDeleted(0);
|
||||
when(waybillService.getOne(any())).thenReturn(waybill);
|
||||
}
|
||||
|
||||
private PunchPhotoDTO photo(String type, String url) {
|
||||
PunchPhotoDTO photo = new PunchPhotoDTO();
|
||||
photo.setType(type);
|
||||
photo.setUrl(url);
|
||||
return photo;
|
||||
}
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9001L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
private ExceptionDisposal unresolvedException(String nodeCode) {
|
||||
ExceptionDisposal exception = new ExceptionDisposal();
|
||||
exception.setNodeCode(nodeCode);
|
||||
exception.setDisposalStatus("processing");
|
||||
return exception;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user