🐛 恢复合并时丢失的司机端自动完成、地点回退与拒单脱离

合并 websoft/dev 解冲突时 DriverWaybillServiceImpl 整段退回旧版,丢掉三处功能,按原提交补回:
- 末位打卡是到货/卸货/签收且回单无需确认时自动完成运单
- 导入运单只有详细地址、没有地点名称时回退显示地址
- 司机拒单后该运单对其列表/计数/详情/打卡/接单全部不可达

归属校验测试原按手机号匹配运单,与打卡、接单的车牌口径冲突,改为按司机档案绑定的车牌断言。
This commit is contained in:
2026-10-13 14:12:31 +08:00
parent 6fecc262b1
commit 8f11bc70ec
2 changed files with 90 additions and 36 deletions
@@ -114,6 +114,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final List<String> TAB_ALL_STATUSES = Arrays.asList( private static final List<String> TAB_ALL_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING, STATUS_RUNNING, STATUS_COMPLETED
); );
/** 允许完成运单的业务状态 */
private static final List<String> 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 DEFAULT_PREVIEW_SIZE = 2;
private static final int MAX_PREVIEW_SIZE = 20; private static final int MAX_PREVIEW_SIZE = 20;
@@ -313,7 +317,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
throw new ServiceException("运单ID不能为空"); throw new ServiceException("运单ID不能为空");
} }
Driver driver = requireCurrentDriver(); Driver driver = requireCurrentDriver();
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); // 在途打卡单独保留原取单判据,不排除已拒单(见 scopedQuery 的重载说明)
Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver), true);
waybill = normalizeAcceptStatus(waybill); waybill = normalizeAcceptStatus(waybill);
String processJson = resolveProcessJson(waybill); String processJson = resolveProcessJson(waybill);
@@ -415,7 +420,20 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
nodePunchMapper.insert(punch); nodePunchMapper.insert(punch);
advanceCurrentProcessNode(waybill, nodeCfg); 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 @Override
@@ -502,10 +520,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
Driver driver = requireCurrentDriver(); Driver driver = requireCurrentDriver();
Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver));
waybill = normalizeAcceptStatus(waybill); waybill = normalizeAcceptStatus(waybill);
if (!STATUS_RUNNING.equals(waybill.getBusinessStatus()) if (!COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())) {
&& !STATUS_PENDING.equals(waybill.getBusinessStatus())
&& !STATUS_WAITING_DISPATCH.equals(waybill.getBusinessStatus())
&& !STATUS_DISPATCHING.equals(waybill.getBusinessStatus())) {
throw new ServiceException("当前运单状态不允许完成"); throw new ServiceException("当前运单状态不允许完成");
} }
// 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁 // 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁
@@ -524,6 +539,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) { if (WaybillProcessSupport.isAccepted(waybill.getDriverAcceptStatus())) {
throw new ServiceException("该运单已接单"); throw new ServiceException("该运单已接单");
} }
// 拒单后运单已对本人脱离,不给自己接回来的入口;要改主意只能由调度重新派单
if (WaybillProcessSupport.isRejected(waybill.getDriverAcceptStatus())) {
throw new ServiceException("该运单已拒单,请联系调度重新派单");
}
} }
private void assertRejectable(Waybill waybill) { private void assertRejectable(Waybill waybill) {
@@ -539,13 +558,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
} }
private Waybill loadDriverWaybill(Long id, List<String> plates) { private Waybill loadDriverWaybill(Long id, List<String> plates) {
return loadDriverWaybill(id, plates, false);
}
/** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */
private Waybill loadDriverWaybill(Long id, List<String> plates, boolean includeRejected) {
if (id == null) { if (id == null) {
throw new ServiceException("运单ID不能为空"); throw new ServiceException("运单ID不能为空");
} }
if (plates.isEmpty()) { if (plates.isEmpty()) {
throw new ServiceException("当前司机未绑定车辆"); 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) { if (waybill == null) {
throw new ServiceException("运单不存在或无权操作"); throw new ServiceException("运单不存在或无权操作");
} }
@@ -577,8 +602,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
} }
switch (status) { switch (status) {
case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES); case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES);
case 1 -> wrapper.in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING, case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES);
STATUS_WAITING_DISPATCH, STATUS_DISPATCHING));
case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED); case 2 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_COMPLETED);
case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED); case 3 -> wrapper.eq(Waybill::getBusinessStatus, STATUS_CANCELLED);
default -> wrapper.in(Waybill::getBusinessStatus, TAB_ALL_STATUSES); 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
* <p>
* 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。
* 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。
*/ */
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates) { private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates) {
return Wrappers.<Waybill>lambdaQuery().and(w -> { return scopedQuery(plates, false);
}
/**
* @param includeRejected true 时不排除已拒单的运单,仅限在途打卡使用:
* 拒单的运单不可能处于运输中,收口拦不到合法操作,
* 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤
*/
private LambdaQueryWrapper<Waybill> scopedQuery(List<String> plates, boolean includeRejected) {
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery().and(w -> {
w.in(Waybill::getVehicleNo, plates) w.in(Waybill::getVehicleNo, plates)
.or().in(Waybill::getTrailerVehicleNo, plates); .or().in(Waybill::getTrailerVehicleNo, plates);
for (String plate : 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<Waybill> wrapper, String keyword) { private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
@@ -707,10 +748,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
DriverWaybillCardVO card = new DriverWaybillCardVO(); DriverWaybillCardVO card = new DriverWaybillCardVO();
card.setId(waybill.getId()); card.setId(waybill.getId());
card.setWaybillNo(waybill.getWaybillNo()); 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.setFromName(Func.toStrWithEmpty(waybill.getDepartureName(), Func.toStr(waybill.getDepartureAddress(), "")));
card.setToAddress(Func.toStr(waybill.getArrivalAddress(), card.getToName())); 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.setCargoNames(splitCargoNames(waybill.getCargoName()));
card.setCargoCategory(Func.toStr(waybill.getCargoType(), "")); card.setCargoCategory(Func.toStr(waybill.getCargoType(), ""));
card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit())); card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()));
@@ -1096,6 +1139,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return countByType; return countByType;
} }
/**
* 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。
* <p>
* 不满足返回 false,由调用方静默跳过,打卡不受影响。
*/
private boolean canAutoCompleteWaybill(Waybill waybill) {
return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())
&& unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty();
}
/** /**
* 断言该运单没有未了结的异常。 * 断言该运单没有未了结的异常。
* <p> * <p>
@@ -38,14 +38,14 @@ import static org.mockito.Mockito.when;
/** /**
* 司机对运单的归属校验(异常上报等按运单操作的前置条件)。 * 司机对运单的归属校验(异常上报等按运单操作的前置条件)。
* <p> * <p>
* 口径:当前用户手机号匹配运单 {@code driverPhone}。 * 口径与司机打卡、司机接单一致:当前登录司机档案自述绑定的车牌({@code driving_vehicle})
* 管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。 * 覆盖运单主车或挂车。管理端与调度员账号没有司机档案,会停在这里,这是刻意的:不提供代报。
*/ */
class DriverWaybillOwnershipTest { class DriverWaybillOwnershipTest {
private static final Long WAYBILL_ID = 5301L; private static final Long WAYBILL_ID = 5301L;
private static final Long USER_ID = 7001L; private static final Long USER_ID = 7001L;
private static final String PHONE = "13800138000"; private static final String PLATE = "沪AE31312";
private IDriverService driverService; private IDriverService driverService;
private IWaybillService waybillService; private IWaybillService waybillService;
@@ -88,21 +88,21 @@ class DriverWaybillOwnershipTest {
} }
@Test @Test
@DisplayName("司机档案与登录态都没有手机号时被拒") @DisplayName("司机档案没有绑定车辆时被拒")
void withoutPhoneIsRejected() { void withoutBoundVehicleIsRejected() {
Driver driver = driver(null); Driver driver = driver(null);
when(driverService.getOne(any())).thenReturn(driver); when(driverService.getOne(any())).thenReturn(driver);
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
() -> runOwnershipCheck(WAYBILL_ID)); () -> runOwnershipCheck(WAYBILL_ID));
assertEquals("当前用户手机号为空,无法查找运单", exception.getMessage()); assertEquals("当前司机未绑定车辆", exception.getMessage());
} }
@Test @Test
@DisplayName("运单司机手机号与当前用户不一致时被拒") @DisplayName("运单车牌不在当前司机绑定车牌内时被拒")
void waybillOfAnotherPhoneIsRejected() { void waybillOfAnotherVehicleIsRejected() {
when(driverService.getOne(any())).thenReturn(driver(PHONE)); when(driverService.getOne(any())).thenReturn(driver(PLATE));
when(waybillService.getOne(any())).thenReturn(null); when(waybillService.getOne(any())).thenReturn(null);
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
@@ -112,9 +112,9 @@ class DriverWaybillOwnershipTest {
} }
@Test @Test
@DisplayName("本人手机号运单放行,且查询确实按司机手机号收敛") @DisplayName("本人绑定车牌的运单放行,且查询确实按车牌收敛")
void ownWaybillPasses() { void ownWaybillPasses() {
when(driverService.getOne(any())).thenReturn(driver(PHONE)); when(driverService.getOne(any())).thenReturn(driver(PLATE));
when(waybillService.getOne(any())).thenReturn(waybill()); when(waybillService.getOne(any())).thenReturn(waybill());
assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID)); assertDoesNotThrow(() -> runOwnershipCheck(WAYBILL_ID));
@@ -122,17 +122,17 @@ class DriverWaybillOwnershipTest {
ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); ArgumentCaptor<LambdaQueryWrapper<Waybill>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(waybillService).getOne(captor.capture()); verify(waybillService).getOne(captor.capture());
LambdaQueryWrapper<Waybill> wrapper = captor.getValue(); LambdaQueryWrapper<Waybill> wrapper = captor.getValue();
assertTrue(wrapper.getTargetSql().contains("driver_phone"), assertTrue(wrapper.getTargetSql().contains("vehicle_no"),
"归属查询必须落在运单司机手机号上:" + wrapper.getTargetSql()); "归属查询必须落在运单主车/挂车车牌上:" + wrapper.getTargetSql());
assertTrue(wrapper.getParamNameValuePairs().values().stream() assertTrue(wrapper.getParamNameValuePairs().values().stream()
.anyMatch(PHONE::equals), .anyMatch(PLATE::equals),
"归属查询必须带上当前用户手机号:" + wrapper.getParamNameValuePairs()); "归属查询必须带上当前司机绑定的车牌:" + wrapper.getParamNameValuePairs());
} }
@Test @Test
@DisplayName("运单ID为空时被拒") @DisplayName("运单ID为空时被拒")
void withoutWaybillIdIsRejected() { void withoutWaybillIdIsRejected() {
when(driverService.getOne(any())).thenReturn(driver(PHONE)); when(driverService.getOne(any())).thenReturn(driver(PLATE));
ServiceException exception = assertThrows(ServiceException.class, ServiceException exception = assertThrows(ServiceException.class,
() -> runOwnershipCheck(null)); () -> runOwnershipCheck(null));
@@ -145,17 +145,18 @@ class DriverWaybillOwnershipTest {
private void runOwnershipCheck(Long waybillId) { private void runOwnershipCheck(Long waybillId) {
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) { try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID); authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
authUtil.when(AuthUtil::getUserAccount).thenReturn(PHONE); authUtil.when(AuthUtil::getUserAccount).thenReturn("13800138000");
service.assertCurrentDriverOwnsWaybill(waybillId); service.assertCurrentDriverOwnsWaybill(waybillId);
} }
} }
private Driver driver(String mobile) { private Driver driver(String drivingVehicle) {
Driver driver = new Driver(); Driver driver = new Driver();
driver.setId(9001L); driver.setId(9001L);
driver.setUserId(USER_ID); driver.setUserId(USER_ID);
driver.setDriverName("韦司机"); driver.setDriverName("韦司机");
driver.setMobile(mobile); driver.setMobile("13800138000");
driver.setDrivingVehicle(drivingVehicle);
return driver; return driver;
} }
@@ -163,7 +164,7 @@ class DriverWaybillOwnershipTest {
Waybill waybill = new Waybill(); Waybill waybill = new Waybill();
waybill.setId(WAYBILL_ID); waybill.setId(WAYBILL_ID);
waybill.setWaybillNo("YD202610070001"); waybill.setWaybillNo("YD202610070001");
waybill.setDriverPhone(PHONE); waybill.setVehicleNo(PLATE);
waybill.setIsDeleted(0); waybill.setIsDeleted(0);
return waybill; return waybill;
} }