✨ 异常上报校验上报人必须是该运单的承运司机

This commit is contained in:
2026-10-07 12:18:49 +08:00
parent 7e84b670b6
commit 75cd448e2d
5 changed files with 219 additions and 0 deletions
@@ -115,4 +115,12 @@ public interface IDriverWaybillService {
*/
boolean isNodePunched(Long waybillId, String nodeCode);
/**
* 断言当前登录司机对该运单有归属:运单车牌须落在司机档案自述绑定的车牌内。
* <p>
* 无司机档案、未绑定车辆、车牌不匹配都会抛异常。管理端与调度员账号没有司机档案,
* 会停在「未找到当前登录司机档案」——这是刻意的,不提供代报。
*/
void assertCurrentDriverOwnsWaybill(Long waybillId);
}
@@ -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>
@@ -138,6 +138,11 @@ public class ExceptionDisposalServiceImpl
throw new ServiceException("请关联运单后再上报");
}
// 上报人只可能是该运单的承运司机本人,取登录态;请求体里的上报人一律忽略。
// 归属按司机绑定车牌判定,与司机打卡、司机接单同一口径。
// 整单异常同样受这条约束:没有节点不等于没有上报人。
driverWaybillService.assertCurrentDriverOwnsWaybill(disposal.getWaybillId());
// 节点可以为空:全局入口(首页快捷格、打卡页底部按钮)上报的异常不指定环节,
// 它无从定位阻断区间,由门禁按「阻断该运单全部未打卡节点」处理。
// 节点非空时才要求它确属该运单;节点名取自过程配置而非请求体,避免落进一个该节点并不叫的名字。
@@ -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;
}
}
@@ -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) {