From 8f11bc70ec6e98694ec8ef66fd342303d04e34bf Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Tue, 13 Oct 2026 14:12:31 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E6=81=A2=E5=A4=8D=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=97=B6=E4=B8=A2=E5=A4=B1=E7=9A=84=E5=8F=B8=E6=9C=BA=E7=AB=AF?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=AE=8C=E6=88=90=E3=80=81=E5=9C=B0=E7=82=B9?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E4=B8=8E=E6=8B=92=E5=8D=95=E8=84=B1=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并 websoft/dev 解冲突时 DriverWaybillServiceImpl 整段退回旧版,丢掉三处功能,按原提交补回: - 末位打卡是到货/卸货/签收且回单无需确认时自动完成运单 - 导入运单只有详细地址、没有地点名称时回退显示地址 - 司机拒单后该运单对其列表/计数/详情/打卡/接单全部不可达 归属校验测试原按手机号匹配运单,与打卡、接单的车牌口径冲突,改为按司机档案绑定的车牌断言。 --- .../impl/DriverWaybillServiceImpl.java | 85 +++++++++++++++---- .../impl/DriverWaybillOwnershipTest.java | 41 ++++----- 2 files changed, 90 insertions(+), 36 deletions(-) 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 0a06a6b0..967738bd 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 @@ -114,6 +114,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { private static final List TAB_ALL_STATUSES = Arrays.asList( STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED ); + /** 允许完成运单的业务状态 */ + private static final List COMPLETABLE_STATUSES = Arrays.asList( + STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING + ); private static final int DEFAULT_PREVIEW_SIZE = 2; private static final int MAX_PREVIEW_SIZE = 20; @@ -313,7 +317,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { throw new ServiceException("运单ID不能为空"); } Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); + // 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明) + Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver), true); waybill = normalizeAcceptStatus(waybill); String processJson = resolveProcessJson(waybill); @@ -415,7 +420,20 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { nodePunchMapper.insert(punch); advanceCurrentProcessNode(waybill, nodeCfg); - return toNodePunchVO(punch); + // 末位打卡节点是到货/卸货/签收、且回单无需确认完成时,本次打卡一并把运单置为完成; + // isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的就是上面那份 processJson,未再调 resolveProcessJson 取过程配置。 + String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg); + String nodeName = WaybillProcessSupport.nodeName(nodeCfg); + boolean completed = false; + if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey) + && WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName) + && WaybillProcessSupport.returnNeedsNoConfirmation(processJson) + && canAutoCompleteWaybill(waybill)) { + completed = waybillService.completeWithoutDeptCheck(waybill.getId()); + } + DriverNodePunchVO vo = toNodePunchVO(punch); + vo.setWaybillCompleted(completed); + return vo; } @Override @@ -502,10 +520,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { Driver driver = requireCurrentDriver(); Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); waybill = normalizeAcceptStatus(waybill); - if (!STATUS_RUNNING.equals(waybill.getBusinessStatus()) - && !STATUS_PENDING.equals(waybill.getBusinessStatus()) - && !STATUS_WAITING_DISPATCH.equals(waybill.getBusinessStatus()) - && !STATUS_DISPATCHING.equals(waybill.getBusinessStatus())) { + if (!COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())) { throw new ServiceException("当前运单状态不允许完成"); } // 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁 @@ -524,6 +539,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) { throw new ServiceException("该运单已接单"); } + // 拒单后运单已对本人脱离,不给自己接回来的入口;要改主意只能由调度重新派单 + if (WaybillProcessSupport.isRejected(waybill.getDriverAcceptStatus())) { + throw new ServiceException("该运单已拒单,请联系调度重新派单"); + } } private void assertRejectable(Waybill waybill) { @@ -539,13 +558,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } private Waybill loadDriverWaybill(Long id, List plates) { + return loadDriverWaybill(id, plates, false); + } + + /** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */ + private Waybill loadDriverWaybill(Long id, List plates, boolean includeRejected) { if (id == null) { throw new ServiceException("运单ID不能为空"); } if (plates.isEmpty()) { throw new ServiceException("当前司机未绑定车辆"); } - Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1")); + Waybill waybill = waybillService.getOne(scopedQuery(plates, includeRejected) + .eq(Waybill::getId, id).last("LIMIT 1")); if (waybill == null) { throw new ServiceException("运单不存在或无权操作"); } @@ -577,8 +602,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } switch (status) { case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES); - case 1 -> wrapper.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING, - STATUS_WAITING_DISPATCH, STATUS_DISPATCHING)); + case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES); case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED); case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED); default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES); @@ -602,11 +626,23 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } /** - * 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。 - * 绑定来源:blade_transport_driver.driving_vehicle + * 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内, + * 且该运单未被这个司机拒单。绑定来源:blade_transport_driver.driving_vehicle + *

+ * 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。 + * 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。 */ private LambdaQueryWrapper scopedQuery(List plates) { - return Wrappers.lambdaQuery().and(w -> { + return scopedQuery(plates, false); + } + + /** + * @param includeRejected true 时不排除已拒单的运单,仅限在途打卡使用: + * 拒单的运单不可能处于运输中,收口拦不到合法操作, + * 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤 + */ + private LambdaQueryWrapper scopedQuery(List plates, boolean includeRejected) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().and(w -> { w.in(Waybill::getVehicleNo, plates) .or().in(Waybill::getTrailerVehicleNo, plates); for (String plate : plates) { @@ -620,6 +656,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { ); } }); + if (!includeRejected) { + wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus) + .or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); + } + return wrapper; } private void applyKeyword(LambdaQueryWrapper wrapper, String keyword) { @@ -707,10 +748,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { DriverWaybillCardVO card = new DriverWaybillCardVO(); card.setId(waybill.getId()); card.setWaybillNo(waybill.getWaybillNo()); - card.setFromName(Func.toStr(waybill.getDepartureName(), "")); - card.setToName(Func.toStr(waybill.getArrivalName(), "")); - card.setFromAddress(Func.toStr(waybill.getDepartureAddress(), card.getFromName())); - card.setToAddress(Func.toStr(waybill.getArrivalAddress(), card.getToName())); + // 批量导入的运单只有发货/到货地址、没有地点名称,名称为空时回退到地址, + // 否则司机端首页、运单列表与打卡页的地点位置都是空的 + card.setFromName(Func.toStrWithEmpty(waybill.getDepartureName(), Func.toStr(waybill.getDepartureAddress(), ""))); + card.setToName(Func.toStrWithEmpty(waybill.getArrivalName(), Func.toStr(waybill.getArrivalAddress(), ""))); + card.setFromAddress(Func.toStrWithEmpty(waybill.getDepartureAddress(), card.getFromName())); + card.setToAddress(Func.toStrWithEmpty(waybill.getArrivalAddress(), card.getToName())); card.setCargoNames(splitCargoNames(waybill.getCargoName())); card.setCargoCategory(Func.toStr(waybill.getCargoType(), "")); card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); @@ -1096,6 +1139,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { return countByType; } + /** + * 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。 + *

+ * 不满足返回 false,由调用方静默跳过,打卡不受影响。 + */ + private boolean canAutoCompleteWaybill(Waybill waybill) { + return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus()) + && unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty(); + } + /** * 断言该运单没有未了结的异常。 *

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 index 035a2db1..0a1a5df6 100644 --- 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 @@ -38,14 +38,14 @@ import static org.mockito.Mockito.when; /** * 司机对运单的归属校验(异常上报等按运单操作的前置条件)。 *

- * 口径:当前用户手机号匹配运单 {@code driverPhone}。 - * 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。 + * 口径与司机打卡、司机接单一致:当前登录司机档案自述绑定的车牌({@code driving_vehicle}) + * 覆盖运单主车或挂车。管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。 */ class DriverWaybillOwnershipTest { private static final Long WAYBILL_ID = 5301L; private static final Long USER_ID = 7001L; - private static final String PHONE = "13800138000"; + private static final String PLATE = "沪AE31312"; private IDriverService driverService; private IWaybillService waybillService; @@ -88,21 +88,21 @@ class DriverWaybillOwnershipTest { } @Test - @DisplayName("司机档案与登录态都没有手机号时被拒") - void withoutPhoneIsRejected() { + @DisplayName("司机档案没有绑定车辆时被拒") + void withoutBoundVehicleIsRejected() { Driver driver = driver(null); when(driverService.getOne(any())).thenReturn(driver); ServiceException exception = assertThrows(ServiceException.class, () -> runOwnershipCheck(WAYBILL_ID)); - assertEquals("当前用户手机号为空,无法查找运单", exception.getMessage()); + assertEquals("当前司机未绑定车辆", exception.getMessage()); } @Test - @DisplayName("运单司机手机号与当前用户不一致时被拒") - void waybillOfAnotherPhoneIsRejected() { - when(driverService.getOne(any())).thenReturn(driver(PHONE)); + @DisplayName("运单车牌不在当前司机绑定车牌内时被拒") + void waybillOfAnotherVehicleIsRejected() { + when(driverService.getOne(any())).thenReturn(driver(PLATE)); when(waybillService.getOne(any())).thenReturn(null); ServiceException exception = assertThrows(ServiceException.class, @@ -112,9 +112,9 @@ class DriverWaybillOwnershipTest { } @Test - @DisplayName("本人手机号运单放行,且查询确实按司机手机号收敛") + @DisplayName("本人绑定车牌的运单放行,且查询确实按车牌收敛") void ownWaybillPasses() { - when(driverService.getOne(any())).thenReturn(driver(PHONE)); + when(driverService.getOne(any())).thenReturn(driver(PLATE)); when(waybillService.getOne(any())).thenReturn(waybill()); assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID)); @@ -122,17 +122,17 @@ class DriverWaybillOwnershipTest { ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(waybillService).getOne(captor.capture()); LambdaQueryWrapper wrapper = captor.getValue(); - assertTrue(wrapper.getTargetSql().contains("driver_phone"), - "归属查询必须落在运单司机手机号上:" + wrapper.getTargetSql()); + assertTrue(wrapper.getTargetSql().contains("vehicle_no"), + "归属查询必须落在运单主车/挂车车牌上:" + wrapper.getTargetSql()); assertTrue(wrapper.getParamNameValuePairs().values().stream() - .anyMatch(PHONE::equals), - "归属查询必须带上当前用户手机号:" + wrapper.getParamNameValuePairs()); + .anyMatch(PLATE::equals), + "归属查询必须带上当前司机绑定的车牌:" + wrapper.getParamNameValuePairs()); } @Test @DisplayName("运单ID为空时被拒") void withoutWaybillIdIsRejected() { - when(driverService.getOne(any())).thenReturn(driver(PHONE)); + when(driverService.getOne(any())).thenReturn(driver(PLATE)); ServiceException exception = assertThrows(ServiceException.class, () -> runOwnershipCheck(null)); @@ -145,17 +145,18 @@ class DriverWaybillOwnershipTest { private void runOwnershipCheck(Long waybillId) { try (MockedStatic authUtil = mockStatic(AuthUtil.class)) { authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID); - authUtil.when(AuthUtil::getUserAccount).thenReturn(PHONE); + authUtil.when(AuthUtil::getUserAccount).thenReturn("13800138000"); service.assertCurrentDriverOwnsWaybill(waybillId); } } - private Driver driver(String mobile) { + private Driver driver(String drivingVehicle) { Driver driver = new Driver(); driver.setId(9001L); driver.setUserId(USER_ID); driver.setDriverName("韦司机"); - driver.setMobile(mobile); + driver.setMobile("13800138000"); + driver.setDrivingVehicle(drivingVehicle); return driver; } @@ -163,7 +164,7 @@ class DriverWaybillOwnershipTest { Waybill waybill = new Waybill(); waybill.setId(WAYBILL_ID); waybill.setWaybillNo("YD202610070001"); - waybill.setDriverPhone(PHONE); + waybill.setVehicleNo(PLATE); waybill.setIsDeleted(0); return waybill; }