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