diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java index a41d714a..e1735c5d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IDriverWaybillService.java @@ -115,4 +115,12 @@ public interface IDriverWaybillService { */ boolean isNodePunched(Long waybillId, String nodeCode); + /** + * 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。 + *

+ * 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案, + * 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。 + */ + void assertCurrentDriverOwnsWaybill(Long waybillId); + } 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 1f0e57af..fc1c3297 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 @@ -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(未达终态:已完成 / 已撤销)。 *

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index 1d686126..a3d97307 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -138,6 +138,11 @@ public class ExceptionDisposalServiceImpl throw new ServiceException("请关联运单后再上报"); } + // 上报人只可能是该运单的承运司机本人,取登录态;请求体里的上报人一律忽略。 + // 归属按司机绑定车牌判定,与司机打卡、司机接单同一口径。 + // 整单异常同样受这条约束:没有节点不等于没有上报人。 + driverWaybillService.assertCurrentDriverOwnsWaybill(disposal.getWaybillId()); + // 节点可以为空:全局入口(首页快捷格、打卡页底部按钮)上报的异常不指定环节, // 它无从定位阻断区间,由门禁按「阻断该运单全部未打卡节点」处理。 // 节点非空时才要求它确属该运单;节点名取自过程配置而非请求体,避免落进一个该节点并不叫的名字。 diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java new file mode 100644 index 00000000..fa84fb62 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillOwnershipTest.java @@ -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; + +/** + * 司机对运单的归属校验(异常上报等按运单操作的前置条件)。 + *

+ * 口径只有一条:司机档案里自述绑定的车牌要与运单的主车或挂车车牌匹配。 + * 它不是「运单上的承运司机」——那两件事互不推导;也不是「谁提交了这份运单材料」。 + * 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。 + */ +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> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(waybillService).getOne(captor.capture()); + LambdaQueryWrapper 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 = 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; + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java index 658612a0..900b19ac 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java @@ -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) {