✨ 异常上报校验上报人必须是该运单的承运司机
This commit is contained in:
+8
@@ -115,4 +115,12 @@ public interface IDriverWaybillService {
|
||||
*/
|
||||
boolean isNodePunched(Long waybillId, String nodeCode);
|
||||
|
||||
/**
|
||||
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
|
||||
* <p>
|
||||
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
|
||||
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
|
||||
*/
|
||||
void assertCurrentDriverOwnsWaybill(Long waybillId);
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -819,6 +819,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
.anyMatch(key::equalsIgnoreCase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertCurrentDriverOwnsWaybill(Long waybillId) {
|
||||
// 与司机打卡、司机接单共用同一套归属判定,不另立第二套规则
|
||||
Driver driver = requireCurrentDriver();
|
||||
loadDriverWaybill(waybillId, currentBoundPlates(driver));
|
||||
}
|
||||
|
||||
/**
|
||||
* 该运单「未处理完成的异常」所属节点 key(未达终态:已完成 / 已撤销)。
|
||||
* <p>
|
||||
|
||||
+5
@@ -138,6 +138,11 @@ public class ExceptionDisposalServiceImpl
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -234,6 +235,32 @@ class ExceptionDisposalReportGateTest {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user