diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java index 57e394d1..323db157 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillCardVO.java @@ -150,6 +150,9 @@ public class DriverWaybillCardVO implements Serializable { @Schema(description = "运输方式文案:公路运输等") private String transportType; + @Schema(description = "运输组织:common普通 / load配载") + private String transportOrgType; + @Schema(description = "计划发货时间") private String planShipTime; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java index 0892b50e..e337b9a1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java @@ -94,6 +94,13 @@ public class PreSettlementController extends BladeController { return R.data(preSettlementService.contractOptions(keyword)); } + @GetMapping("/next-no") + @ApiOperationSupport(order = 4) + @Operation(summary = "获取最新预结算单号") + public R nextNo() { + return R.data(preSettlementService.nextNo()); + } + @GetMapping("/fee-options") @ApiOperationSupport(order = 4) @Operation(summary = "费用类型及费用项") 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 0d9d3f15..8f9cd52d 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 @@ -130,9 +130,9 @@ 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/IPreSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java index 28af8ada..c24b7240 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java @@ -33,6 +33,11 @@ public interface IPreSettlementService extends BaseService { PreSettlementVO detail(Long id); + /** + * 预览下一条预结算单号。仅供新增页面展示,真正落库的单号仍由保存时生成。 + */ + String nextNo(); + default List> contractOptions(String keyword) { return contractOptions(keyword, null); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java index 97e7186b..825c3bc0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java @@ -113,6 +113,8 @@ public class DriverServiceImpl extends BaseServiceImpl imp Driver driver = new Driver(); driver.setId(id); driver.setStatus(status); + // 只改 status 也必须带上 driving_vehicle 原值,理由见 carryDrivingVehicle + carryDrivingVehicle(id, driver); return updateById(driver); } @@ -446,9 +448,26 @@ public class DriverServiceImpl extends BaseServiceImpl imp Driver patch = new Driver(); patch.setId(driver.getId()); patch.setUserId(userId); + // 同上:这次只改 user_id,不带 driving_vehicle 原值同样会把车辆绑定清掉 + carryDrivingVehicle(driver.getId(), patch); updateById(patch); } + /** + * 把库里的 driving_vehicle 原值带进 patch。 + *

+ * 该列标了 {@code FieldStrategy.ALWAYS},更新时忽略 null 直接写:patch 里不带这一列, + * 一次只改别的列的更新就会把司机的车辆绑定清成 NULL,而司机端车辆列表正是按这一列 + * 的车牌匹配出来的(清空即「暂无绑定车辆」)。读不到该司机时保持原样, + * 让更新自己去失败。 + */ + private void carryDrivingVehicle(Long driverId, Driver patch) { + Driver current = getById(driverId); + if (current != null) { + patch.setDrivingVehicle(current.getDrivingVehicle()); + } + } + private Long defaultZero(Long value) { return value == null ? 0L : value; } 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 9158ce0d..57c8ceb4 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 @@ -102,6 +102,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { private static final String STATUS_COMPLETED = "completed"; private static final String STATUS_CANCELLED = "cancelled"; + /** 运输组织类型:运单有配载单号即配载(load),否则普通(common) */ + private static final String TRANSPORT_COMMON = "common"; + private static final String TRANSPORT_LOAD = "load"; + /** 小程序「待接单」对应的后端业务状态 */ private static final List PENDING_STATUSES = Arrays.asList( STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING @@ -110,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; @@ -134,12 +142,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override public DriverWaybillCardVO currentTask() { - List plates = currentBoundPlates(); - if (plates.isEmpty()) { + String phone = currentUserPhone(); + if (Func.isEmpty(phone)) { return null; } // 进行中:running,或无需确认接单但仍为 pending 的历史数据 - List candidates = waybillService.list(scopedQuery(plates) + List candidates = waybillService.list(scopedQuery(phone) .in(Waybill::getBusinessStatus, List.of(STATUS_RUNNING, STATUS_PENDING)) .orderByDesc(Waybill::getUpdateTime) .last("LIMIT 20")); @@ -155,12 +163,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override public DriverWaybillPreviewVO pendingPreview(Integer size) { DriverWaybillPreviewVO preview = new DriverWaybillPreviewVO(); - List plates = currentBoundPlates(); - if (plates.isEmpty()) { + String phone = currentUserPhone(); + if (Func.isEmpty(phone)) { return preview; } int limit = normalizePreviewSize(size); - List pendingList = waybillService.list(scopedQuery(plates) + List pendingList = waybillService.list(scopedQuery(phone) .in(Waybill::getBusinessStatus, PENDING_STATUSES) .orderByDesc(Waybill::getCreateTime)); List needAccept = pendingList.stream() @@ -175,11 +183,11 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override public DriverWaybillTabCountsVO tabCounts() { DriverWaybillTabCountsVO vo = new DriverWaybillTabCountsVO(); - List plates = currentBoundPlates(); - if (plates.isEmpty()) { + String phone = currentUserPhone(); + if (Func.isEmpty(phone)) { return vo; } - List waybills = waybillService.list(scopedQuery(plates) + List waybills = waybillService.list(scopedQuery(phone) .in(Waybill::getBusinessStatus, TAB_ALL_STATUSES)); long pending = 0; long doing = 0; @@ -211,12 +219,12 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE); Page empty = new Page<>(pageNo, pageSize); - List plates = currentBoundPlates(); - if (plates.isEmpty()) { + String phone = currentUserPhone(); + if (Func.isEmpty(phone)) { return empty; } - LambdaQueryWrapper wrapper = scopedQuery(plates); + LambdaQueryWrapper wrapper = scopedQuery(phone); // 先按 Tab 口径拉候选,再按过程配置校正 pending→running 后内存分页 applyStatusFilterForQuery(wrapper, status); applyKeyword(wrapper, keyword); @@ -239,15 +247,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override public DriverWaybillCardVO detail(Long id) { - Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id); return toCard(normalizeAcceptStatus(waybill), true); } @Override public WaybillTrackVO track(Long id) { - Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id); LocalDate[] range = resolveTrackDateRange(waybill); return waybillService.trackVehicle(waybill.getId(), range[0].toString(), range[1].toString()); } @@ -309,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(), true); waybill = normalizeAcceptStatus(waybill); String processJson = resolveProcessJson(waybill); @@ -358,7 +367,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { throw new ServiceException("节点编码不能为空"); } Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(dto.getWaybillId(), currentBoundPlates(driver)); + Waybill waybill = loadDriverWaybill(dto.getWaybillId()); waybill = normalizeAcceptStatus(waybill); String processJson = resolveProcessJson(waybill); @@ -411,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 @@ -421,7 +443,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { // 认证门槛:司机本人须已认证,且承运车辆须已认证。 // 判据是各自持有的生效快照指针,故「已认证的车重新提交认证」不会拦下接单。 assertCertified(driver); - Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + Waybill waybill = loadDriverWaybill(id); waybillService.syncDriverAcceptState(waybill); assertAcceptable(waybill); Date now = new Date(); @@ -471,8 +493,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override @Transactional(rollbackFor = Exception.class) public boolean reject(Long id, String reason) { - Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id); waybillService.syncDriverAcceptState(waybill); assertRejectable(waybill); String rejectReason = Func.isEmpty(reason) ? null : reason.trim(); @@ -495,13 +517,10 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { if (id == null) { throw new ServiceException("运单ID不能为空"); } - Driver driver = requireCurrentDriver(); - Waybill waybill = loadDriverWaybill(id, currentBoundPlates(driver)); + requireCurrentDriver(); + Waybill waybill = loadDriverWaybill(id); 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("当前运单状态不允许完成"); } // 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁 @@ -520,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) { @@ -534,14 +557,21 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } } - private Waybill loadDriverWaybill(Long id, List plates) { + private Waybill loadDriverWaybill(Long id) { + return loadDriverWaybill(id, false); + } + + /** @param includeRejected 仅 submitEnroute 传 true,见 scopedQuery 的重载说明 */ + private Waybill loadDriverWaybill(Long id, boolean includeRejected) { if (id == null) { throw new ServiceException("运单ID不能为空"); } - if (plates.isEmpty()) { - throw new ServiceException("当前司机未绑定车辆"); + String phone = currentUserPhone(); + if (Func.isEmpty(phone)) { + throw new ServiceException("当前用户手机号为空,无法查找运单"); } - Waybill waybill = waybillService.getOne(scopedQuery(plates).eq(Waybill::getId, id).last("LIMIT 1")); + Waybill waybill = waybillService.getOne(scopedQuery(phone, includeRejected) + .eq(Waybill::getId, id).last("LIMIT 1")); if (waybill == null) { throw new ServiceException("运单不存在或无权操作"); } @@ -573,8 +603,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); @@ -598,24 +627,31 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { } /** - * 司机可见运单范围:运单车牌(主车/挂车)落在当前司机绑定车牌内。 - * 绑定来源:blade_transport_driver.driving_vehicle + * 司机可见运单范围:运单司机手机号(driver_phone)等于当前用户手机号, + * 且该运单未被这个司机拒单。 + *

+ * 拒单后运单对司机彻底脱离——列表、计数、详情、打卡、接单一律不可达,等管理员重新派单。 + * 运单上的司机姓名/手机/车牌不清空,调度重新派单时要知道原来派给谁。 */ - private LambdaQueryWrapper scopedQuery(List plates) { - return Wrappers.lambdaQuery().and(w -> { - w.in(Waybill::getVehicleNo, plates) - .or().in(Waybill::getTrailerVehicleNo, plates); - for (String plate : plates) { - w.or().apply( - "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}", - plate - ); - w.or().apply( - "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(UPPER(IFNULL(trailer_vehicle_no,'')),'·',''),'•',''),'.',''),'-',''),' ','') = {0}", - plate - ); - } - }); + private LambdaQueryWrapper scopedQuery(String phone) { + return scopedQuery(phone, false); + } + + /** + * @param includeRejected true 时不排除已拒单的运单,仅限在途打卡使用: + * 拒单的运单不可能处于运输中,收口拦不到合法操作, + * 而保留原判据可避免「已拒单 + 进行中」的历史脏数据被误伤 + */ + private LambdaQueryWrapper scopedQuery(String phone, boolean includeRejected) { + String normalizedPhone = Func.toStr(phone, "").trim(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().and(w -> w + .eq(Waybill::getDriverPhone, normalizedPhone) + .or().apply("REPLACE(IFNULL(driver_phone,''),' ','') = {0}", normalizedPhone)); + 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) { @@ -631,13 +667,26 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { .or().like(Waybill::getVehicleNo, kw)); } - /** 当前登录司机绑定的规范化车牌列表;无司机或无绑定车牌则空 */ - private List currentBoundPlates() { + /** + * 当前用户手机号:司机档案 mobile 优先,其次小程序用户账号。 + *

+ * 派单时运单 driver_phone 取自司机档案 mobile,故档案有手机号就以它为准。 + */ + private String currentUserPhone() { Driver driver = currentDriver(); - if (driver == null || Func.isEmpty(driver.getDrivingVehicle())) { - return List.of(); + if (driver != null && isMobileLike(driver.getMobile())) { + return driver.getMobile().trim(); } - return splitPlates(driver.getDrivingVehicle()); + Long userId = AuthUtil.getUserId(); + if (userId == null || userId <= 0) { + return null; + } + R result = mpUserClient.userById(userId); + if (result != null && R.isSuccess(result) && result.getData() != null + && isMobileLike(result.getData().getAccount())) { + return result.getData().getAccount().trim(); + } + return null; } /** @@ -703,10 +752,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())); @@ -734,6 +785,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { card.setCargoQuantity(weightText); card.setTotalWeight(weightText); card.setTransportType(toTransportTypeLabel(waybill.getTransportType())); + card.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON); card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime())); card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime())); card.setRemark(Func.toStr(waybill.getRemark(), "")); @@ -880,9 +932,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService { @Override public void assertCurrentDriverOwnsWaybill(Long waybillId) { - // 归属口径与司机打卡、司机接单一致:都按司机档案自述绑定的车牌判定 - Driver driver = requireCurrentDriver(); - loadDriverWaybill(waybillId, currentBoundPlates(driver)); + // 归属口径与司机打卡、司机接单一致:按当前用户手机号匹配运单司机手机号 + requireCurrentDriver(); + loadDriverWaybill(waybillId); } /** @@ -1091,6 +1143,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/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 6cc4310a..de0b160e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -211,6 +211,11 @@ public class PreSettlementServiceImpl extends BaseServiceImpl> contractOptions(String keyword, Long projectId) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index 137b273e..e5cbabb4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -3104,6 +3104,7 @@ public class ReceivablePayableDetailServiceImpl map.put("contractId", detail.getContractId()); map.put("contractNo", detail.getContractNo()); map.put("contractName", detail.getContractName()); + map.put("sourceType", detail.getSourceType()); map.put("preSettlementNo", detail.getPreSettlementNo()); map.put("formalSettlementNo", detail.getFormalSettlementNo()); map.put("waybillNo", detail.getWaybillNo()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java index 86b9a3e7..12d00939 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverNodePunchPhotoTest.java @@ -58,6 +58,7 @@ class DriverNodePunchPhotoTest { private static final Long WAYBILL_ID = 5601L; private static final Long USER_ID = 7001L; private static final String PLATE = "桂AAW3255"; + private static final String PHONE = "13800138003"; private static final String CLOCK_PHOTO = "https://oss/clock.jpg"; private static final String WAYBILL_PHOTO = "https://oss/waybill.jpg"; private static final String RECEIPT_PHOTO = "https://oss/receipt.jpg"; @@ -308,6 +309,7 @@ class DriverNodePunchPhotoTest { driver.setId(9001L); driver.setUserId(USER_ID); driver.setDrivingVehicle(PLATE); + driver.setMobile(PHONE); return driver; } @@ -316,6 +318,7 @@ class DriverNodePunchPhotoTest { waybill.setId(WAYBILL_ID); waybill.setWaybillNo("YD202610070001"); waybill.setVehicleNo(PLATE); + waybill.setDriverPhone(PHONE); waybill.setProcessJson(PROCESS_JSON); waybill.setBusinessStatus("running"); waybill.setIsDeleted(0); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java new file mode 100644 index 00000000..878fa815 --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverServiceImplTest.java @@ -0,0 +1,189 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +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.core.tool.api.R; +import org.springblade.system.feign.IMpUserClient; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.pojo.entity.MpUser; +import org.springblade.transport.mapper.DriverMapper; +import org.springblade.transport.pojo.entity.Driver; + +import java.lang.reflect.Field; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 司机管理保存与状态变更测试。 + *

+ * 守住的是一条不变式:司机档案的行更新一律不得把「驾驶车辆」绑定写掉。 + * 该列标了 {@code FieldStrategy.ALWAYS},patch 里不带它就写成 NULL, + * 司机端车辆列表随即变成「暂无绑定车辆」,而司机在界面上无法自行恢复。 + */ +class DriverServiceImplTest { + + private static final Long DRIVER_ID = 9001L; + private static final Long USER_ID = 7001L; + private static final String TENANT_ID = "000000"; + private static final String PLATE = "桂A32523"; + private static final String MOBILE = "15975348524"; + + private DriverMapper driverMapper; + private IMpUserClient mpUserClient; + private ISysClient sysClient; + private DriverServiceImpl service; + + @BeforeAll + static void initTableInfo() { + // saveOrUpdate 需要 MyBatis-Plus 的实体元数据缓存 + MybatisConfiguration configuration = new MybatisConfiguration(); + MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, ""); + assistant.setCurrentNamespace("test"); + TableInfoHelper.initTableInfo(assistant, Driver.class); + } + + @BeforeEach + void setUp() throws Exception { + driverMapper = mock(DriverMapper.class); + mpUserClient = mock(IMpUserClient.class); + sysClient = mock(ISysClient.class); + service = new DriverServiceImpl(mpUserClient, sysClient); + inject("baseMapper", driverMapper); + } + + // ------------------------------------------------------------ 启用 / 停用 + + @Test + @DisplayName("启用/停用司机:patch 必须带上驾驶车辆原值,否则这次更新就把车辆绑定清成 NULL") + void changeStatusKeepsDrivingVehicle() { + when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver(PLATE)); + when(driverMapper.updateById(any(Driver.class))).thenReturn(1); + + assertTrue(service.changeStatus(DRIVER_ID, 2)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper).updateById(captor.capture()); + assertEquals(2, captor.getValue().getStatus()); + assertEquals(PLATE, captor.getValue().getDrivingVehicle(), + "只改 status 的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉"); + } + + @Test + @DisplayName("启用/停用司机:状态取值不合法时拒绝,且不写库") + void changeStatusRejectsInvalidStatus() { + assertThrows(ServiceException.class, () -> service.changeStatus(DRIVER_ID, 9)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 保存司机 + + @Test + @DisplayName("保存司机:关联小程序账号的那次 patch 也必须带上驾驶车辆原值") + void submitKeepsDrivingVehicleWhenLinkingMpUser() { + Driver driver = driver(PLATE); + // 提交前校验按身份证号与主键各查一次 + when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(driver); + when(driverMapper.selectByIdCardNoIncludingDeleted(driver.getIdCardNo())).thenReturn(null); + // 完整对象更新:saveOrUpdate 判存在走更新 + when(driverMapper.selectById(DRIVER_ID)).thenReturn(driver); + when(driverMapper.updateById(any(Driver.class))).thenReturn(1); + // 先查不到小程序用户 → 建号 → 回查拿到新建用户 + MpUser created = new MpUser(); + created.setId(USER_ID); + when(mpUserClient.userByPhone(TENANT_ID, MOBILE)).thenReturn(null, R.data(created)); + when(mpUserClient.saveUser(any(MpUser.class))).thenReturn(R.data(true)); + + try (MockedStatic authUtil = mockStatic(AuthUtil.class)) { + authUtil.when(AuthUtil::getTenantId).thenReturn(TENANT_ID); + assertTrue(service.submit(driver)); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(Driver.class); + verify(driverMapper, times(2)).updateById(captor.capture()); + // captor 存的是引用:完整对象更新带走的是 driver 本身,而它随后会被就地写上 userId, + // 所以只能用对象身份把「只带少数几列的 patch」摘出来,不能按 userId 认 + Driver linkPatch = captor.getAllValues().stream() + .filter(patch -> patch != driver) + .findFirst() + .orElseThrow(() -> new AssertionError("没有发出关联小程序账号的那次 patch")); + assertEquals(USER_ID, linkPatch.getUserId()); + assertEquals(PLATE, linkPatch.getDrivingVehicle(), + "关联账号的 patch 不带 driving_vehicle 会把司机的车辆绑定写掉"); + } + + @Test + @DisplayName("保存司机:司机不存在时拒绝,不写库") + void submitRejectsUnknownDriver() { + Driver driver = driver(PLATE); + when(driverMapper.selectByIdIncludingDeleted(DRIVER_ID)).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.submit(driver)); + verify(driverMapper, never()).updateById(any(Driver.class)); + } + + // ------------------------------------------------------------ 夹具 + + private Driver driver(String drivingVehicle) { + Driver driver = new Driver(); + driver.setId(DRIVER_ID); + driver.setIsDeleted(0); + driver.setDriverName("张三"); + driver.setIdCardNo("110101199001011234"); + driver.setGender("男"); + driver.setPosts("司机"); + driver.setDrivingType("A2"); + driver.setDrivingLicenseNo("110101199001011234"); + driver.setDrivingLicenseStartDate(LocalDate.of(2020, 1, 1)); + driver.setDrivingLicenseLongTerm(1); + driver.setQualificationType("道路运输"); + driver.setQualificationNo("Q123456"); + driver.setQualificationLongTerm(1); + driver.setDriverType("自有"); + driver.setMobile(MOBILE); + driver.setOrganizationName("测试组织"); + driver.setEmergencyContactName("李四"); + driver.setEmergencyContactMobile("13800000000"); + driver.setIdCardFront("https://oss/id-front.jpg"); + driver.setIdCardBack("https://oss/id-back.jpg"); + driver.setDrivingLicenseFront("https://oss/dl-front.jpg"); + driver.setDrivingLicenseBack("https://oss/dl-back.jpg"); + driver.setDrivingVehicle(drivingVehicle); + return driver; + } + + private void inject(String field, Object value) throws Exception { + Class type = service.getClass(); + while (type != null) { + try { + Field declared = type.getDeclaredField(field); + declared.setAccessible(true); + declared.set(service, value); + return; + } catch (NoSuchFieldException ignored) { + type = type.getSuperclass(); + } + } + throw new IllegalStateException("未找到字段:" + field); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java index 1aef3f7c..91ecf020 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillAutoCompleteTest.java @@ -49,6 +49,7 @@ class DriverWaybillAutoCompleteTest { private static final Long WAYBILL_ID = 5801L; private static final Long USER_ID = 7001L; private static final String PLATE = "桂AAW3255"; + private static final String PHONE = "13800138001"; private static final String CLOCK_PHOTO = "https://oss/clock.jpg"; /** 末位打卡节点是「签收」,没有回单节点 */ @@ -219,6 +220,7 @@ class DriverWaybillAutoCompleteTest { waybill.setId(WAYBILL_ID); waybill.setWaybillNo("YD202610080001"); waybill.setVehicleNo(PLATE); + waybill.setDriverPhone(PHONE); waybill.setProcessJson(processJson); waybill.setBusinessStatus(businessStatus); waybill.setIsDeleted(0); @@ -237,6 +239,7 @@ class DriverWaybillAutoCompleteTest { driver.setId(9001L); driver.setUserId(USER_ID); driver.setDrivingVehicle(PLATE); + driver.setMobile(PHONE); return driver; } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java index 3553c9c3..10f6ccbb 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillRejectDetachTest.java @@ -57,6 +57,7 @@ class DriverWaybillRejectDetachTest { private static final Long USER_ID = 7401L; private static final Long DRIVER_ID = 9001L; private static final String PLATE = "沪AE31312"; + private static final String PHONE = "13800138002"; private static final Long REJECTED_ID = 7201L; private IDriverService driverService; @@ -97,7 +98,7 @@ class DriverWaybillRejectDetachTest { } @Test - @DisplayName("首页待接预览的取单条件排除已拒单,且排除子句与车牌条件并列") + @DisplayName("首页待接预览的取单条件排除已拒单,且排除子句与取单条件并列") void pendingPreviewQueryExcludesRejected() { when(waybillService.list(ArgumentMatchers.>any())).thenReturn(List.of()); @@ -107,7 +108,7 @@ class DriverWaybillRejectDetachTest { assertTrue(sql.contains("driver_accept_status"), "取单条件必须排除已拒单:" + sql); assertTrue(sql.toUpperCase().contains("AND"), - "排除子句不能与车牌条件「或」在一起,否则等于没排除:" + sql); + "排除子句不能与取单条件「或」在一起,否则等于没排除:" + sql); } @Test @@ -212,6 +213,7 @@ class DriverWaybillRejectDetachTest { driver.setUserId(USER_ID); driver.setDriverName("黄大富"); driver.setDrivingVehicle(PLATE); + driver.setMobile(PHONE); return driver; } @@ -224,6 +226,7 @@ class DriverWaybillRejectDetachTest { waybill.setId(id); waybill.setWaybillNo(no); waybill.setVehicleNo(PLATE); + waybill.setDriverPhone(PHONE); waybill.setBusinessStatus(businessStatus); waybill.setDriverAcceptStatus(acceptStatus); waybill.setIsDeleted(0); diff --git a/doc/sql/transport/blade_invoice_receipt_kingdee_fields_20261015.sql b/doc/sql/transport/blade_invoice_receipt_kingdee_fields_20261015.sql new file mode 100644 index 00000000..a38feff0 --- /dev/null +++ b/doc/sql/transport/blade_invoice_receipt_kingdee_fields_20261015.sql @@ -0,0 +1,42 @@ +-- 收票登记:补金蝶单据字段 +-- 已有环境里的 blade_invoice_receipt 由更早一版的建表语句创建,缺 kingdee_bill_no / +-- kingdee_status;建表语句用的是 CREATE TABLE IF NOT EXISTS,对已存在的表是空操作, +-- 所以只跑建表脚本补不上这两列,必须走 ALTER。 +-- 以下语句可重复执行。 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据编号'' AFTER `current_processor`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND COLUMN_NAME = 'kingdee_bill_no' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 存量行取默认值 unsynced;收票记录只有 synced / failed / unsynced 三种取值 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''unsynced'' COMMENT ''金蝶同步状态:unsynced未同步、synced已同步、failed同步失败'' AFTER `kingdee_bill_no`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND COLUMN_NAME = 'kingdee_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_invoice_receipt` ADD KEY `idx_invoice_receipt_kingdee` (`kingdee_status`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_invoice_receipt' AND INDEX_NAME = 'idx_invoice_receipt_kingdee' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 旧列 kingdee_received_invoice_no / kingdee_received_invoice_id / kingdee_push_status / +-- kingdee_push_msg / kingdee_push_time 在代码里已无任何引用,且取值集合与新模型对不上 +-- (旧值 pushed / assigned 在新模型里不存在),故不搬运数据,保持原样不动。