✨ 异常上报校验上报人必须是该运单的承运司机
This commit is contained in:
+8
@@ -115,4 +115,12 @@ public interface IDriverWaybillService {
|
|||||||
*/
|
*/
|
||||||
boolean isNodePunched(Long waybillId, String nodeCode);
|
boolean isNodePunched(Long waybillId, String nodeCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
|
||||||
|
* <p>
|
||||||
|
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
|
||||||
|
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
|
||||||
|
*/
|
||||||
|
void assertCurrentDriverOwnsWaybill(Long waybillId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -819,6 +819,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
|||||||
.anyMatch(key::equalsIgnoreCase);
|
.anyMatch(key::equalsIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
|
||||||
|
// 与司机打卡、司机接单共用同一套归属判定,不另立第二套规则
|
||||||
|
Driver driver = requireCurrentDriver();
|
||||||
|
loadDriverWaybill(waybillId, currentBoundPlates(driver));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 该运单「未处理完成的异常」所属节点 key(未达终态:已完成 / 已撤销)。
|
* 该运单「未处理完成的异常」所属节点 key(未达终态:已完成 / 已撤销)。
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
+5
@@ -138,6 +138,11 @@ public class ExceptionDisposalServiceImpl
|
|||||||
throw new ServiceException("请关联运单后再上报");
|
throw new ServiceException("请关联运单后再上报");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上报人只可能是该运单的承运司机本人,取登录态;请求体里的上报人一律忽略。
|
||||||
|
// 归属按司机绑定车牌判定,与司机打卡、司机接单同一口径。
|
||||||
|
// 整单异常同样受这条约束:没有节点不等于没有上报人。
|
||||||
|
driverWaybillService.assertCurrentDriverOwnsWaybill(disposal.getWaybillId());
|
||||||
|
|
||||||
// 节点可以为空:全局入口(首页快捷格、打卡页底部按钮)上报的异常不指定环节,
|
// 节点可以为空:全局入口(首页快捷格、打卡页底部按钮)上报的异常不指定环节,
|
||||||
// 它无从定位阻断区间,由门禁按「阻断该运单全部未打卡节点」处理。
|
// 它无从定位阻断区间,由门禁按「阻断该运单全部未打卡节点」处理。
|
||||||
// 节点非空时才要求它确属该运单;节点名取自过程配置而非请求体,避免落进一个该节点并不叫的名字。
|
// 节点非空时才要求它确属该运单;节点名取自过程配置而非请求体,避免落进一个该节点并不叫的名字。
|
||||||
|
|||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
package org.springblade.transport.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
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.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
|
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.entity.Driver;
|
||||||
|
import org.springblade.transport.pojo.entity.Waybill;
|
||||||
|
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.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
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.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>
|
||||||
|
* 口径只有一条:司机档案里自述绑定的车牌要与运单的主车或挂车车牌匹配。
|
||||||
|
* 它不是「运单上的承运司机」——那两件事互不推导;也不是「谁提交了这份运单材料」。
|
||||||
|
* 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。
|
||||||
|
*/
|
||||||
|
class DriverWaybillOwnershipTest {
|
||||||
|
|
||||||
|
private static final Long WAYBILL_ID = 5301L;
|
||||||
|
private static final Long USER_ID = 7001L;
|
||||||
|
private static final String PLATE = "桂AAW3255";
|
||||||
|
|
||||||
|
private IDriverService driverService;
|
||||||
|
private IWaybillService waybillService;
|
||||||
|
private DriverWaybillServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initTableInfo() {
|
||||||
|
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||||
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||||
|
assistant.setCurrentNamespace("test");
|
||||||
|
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
driverService = mock(IDriverService.class);
|
||||||
|
waybillService = mock(IWaybillService.class);
|
||||||
|
service = new DriverWaybillServiceImpl(
|
||||||
|
waybillService,
|
||||||
|
driverService,
|
||||||
|
mock(IProcessConfigService.class),
|
||||||
|
mock(ITransportVehicleService.class),
|
||||||
|
mock(CertificationGate.class),
|
||||||
|
mock(WaybillEnroutePunchMapper.class),
|
||||||
|
mock(WaybillNodePunchMapper.class),
|
||||||
|
mock(ExceptionDisposalMapper.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("登录账号没有司机档案时被拒:管理端与调度员不提供代报")
|
||||||
|
void withoutDriverProfileIsRejected() {
|
||||||
|
when(driverService.getOne(any())).thenReturn(null);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runOwnershipCheck(WAYBILL_ID));
|
||||||
|
|
||||||
|
assertEquals("未找到当前登录司机档案", exception.getMessage());
|
||||||
|
verify(waybillService, never()).getOne(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("司机档案里没有绑定车辆时被拒")
|
||||||
|
void withoutBoundPlateIsRejected() {
|
||||||
|
when(driverService.getOne(any())).thenReturn(driver(null));
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runOwnershipCheck(WAYBILL_ID));
|
||||||
|
|
||||||
|
assertEquals("当前司机未绑定车辆", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("运单不在该司机绑定的车牌下时被拒")
|
||||||
|
void waybillOfAnotherPlateIsRejected() {
|
||||||
|
when(driverService.getOne(any())).thenReturn(driver(PLATE));
|
||||||
|
when(waybillService.getOne(any())).thenReturn(null);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runOwnershipCheck(WAYBILL_ID));
|
||||||
|
|
||||||
|
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("本车运单放行,且查询确实按司机绑定的车牌收敛")
|
||||||
|
void ownWaybillPasses() {
|
||||||
|
when(driverService.getOne(any())).thenReturn(driver(PLATE));
|
||||||
|
when(waybillService.getOne(any())).thenReturn(waybill());
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
|
||||||
|
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(waybillService).getOne(captor.capture());
|
||||||
|
LambdaQueryWrapper<Waybill> wrapper = captor.getValue();
|
||||||
|
assertTrue(wrapper.getTargetSql().contains("vehicle_no"),
|
||||||
|
"归属查询必须落在运单车牌上:" + wrapper.getTargetSql());
|
||||||
|
assertTrue(wrapper.getParamNameValuePairs().values().stream()
|
||||||
|
.anyMatch(value -> PLATE.equals(value)
|
||||||
|
|| (value instanceof Collection<?> values && values.contains(PLATE))),
|
||||||
|
"归属查询必须带上司机绑定的车牌:" + wrapper.getParamNameValuePairs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("运单ID为空时被拒")
|
||||||
|
void withoutWaybillIdIsRejected() {
|
||||||
|
when(driverService.getOne(any())).thenReturn(driver(PLATE));
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runOwnershipCheck(null));
|
||||||
|
|
||||||
|
assertEquals("运单ID不能为空", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 夹具
|
||||||
|
|
||||||
|
private void runOwnershipCheck(Long waybillId) {
|
||||||
|
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||||
|
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||||
|
service.assertCurrentDriverOwnsWaybill(waybillId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Driver driver(String drivingVehicle) {
|
||||||
|
Driver driver = new Driver();
|
||||||
|
driver.setId(9001L);
|
||||||
|
driver.setUserId(USER_ID);
|
||||||
|
driver.setDriverName("韦司机");
|
||||||
|
driver.setDrivingVehicle(drivingVehicle);
|
||||||
|
return driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Waybill waybill() {
|
||||||
|
Waybill waybill = new Waybill();
|
||||||
|
waybill.setId(WAYBILL_ID);
|
||||||
|
waybill.setWaybillNo("YD202610070001");
|
||||||
|
waybill.setVehicleNo(PLATE);
|
||||||
|
waybill.setIsDeleted(0);
|
||||||
|
return waybill;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+27
@@ -34,6 +34,7 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -234,6 +235,32 @@ class ExceptionDisposalReportGateTest {
|
|||||||
assertEquals("韦司机", savedDisposal().getReporterName());
|
assertEquals("韦司机", savedDisposal().getReporterName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 上报人归属
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("上报人不属于该运单时在落库前就被拒")
|
||||||
|
void reportIsRejectedWhenReporterDoesNotOwnWaybill() {
|
||||||
|
stubNode("load", "装货", "cargo");
|
||||||
|
when(driverWaybillService.isNodePunched(WAYBILL_ID, "load")).thenReturn(false);
|
||||||
|
doThrow(new ServiceException("运单不存在或无权操作"))
|
||||||
|
.when(driverWaybillService).assertCurrentDriverOwnsWaybill(WAYBILL_ID);
|
||||||
|
|
||||||
|
ServiceException exception = submitExpectRejected("load");
|
||||||
|
|
||||||
|
assertEquals("运单不存在或无权操作", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("整单异常同样校验上报人:没有节点不等于没有上报人")
|
||||||
|
void waybillLevelReportAlsoChecksReporterOwnership() {
|
||||||
|
doThrow(new ServiceException("未找到当前登录司机档案"))
|
||||||
|
.when(driverWaybillService).assertCurrentDriverOwnsWaybill(WAYBILL_ID);
|
||||||
|
|
||||||
|
ServiceException exception = submitExpectRejected(null);
|
||||||
|
|
||||||
|
assertEquals("未找到当前登录司机档案", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------ 夹具
|
// ------------------------------------------------------------ 夹具
|
||||||
|
|
||||||
private void stubNode(String key, String name, String type) {
|
private void stubNode(String key, String name, String type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user