diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java
index b0dca1a4..aa0dbfb5 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillCardVO.java
@@ -65,6 +65,9 @@ public class AdminWaybillCardVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
+ @Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
+ private String businessStatus;
+
@Schema(description = "承运方")
private String carrierName;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java
index ff601f14..e874df68 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/AdminWaybillDetailVO.java
@@ -48,6 +48,9 @@ public class AdminWaybillDetailVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
+ @Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
+ private String businessStatus;
+
@Schema(description = "起点名称")
private String fromName;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java
index 691cb573..d5d10b60 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverNodePunchVO.java
@@ -52,4 +52,7 @@ public class DriverNodePunchVO implements Serializable {
@Schema(description = "本次打卡是否使运单自动完成")
private Boolean waybillCompleted;
+ @Schema(description = "本次打卡后的运单业务状态:running / pending_return_confirm / completed")
+ private String waybillBusinessStatus;
+
}
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 23432c6a..a4be3625 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
@@ -76,6 +76,9 @@ public class DriverWaybillCardVO implements Serializable {
@Schema(description = "状态:0待接单/1运输中/2已完成/3已取消")
private Integer status;
+ @Schema(description = "原始业务状态:pending / running / pending_return_confirm / completed / cancelled")
+ private String businessStatus;
+
@Schema(description = "创建时间")
private String createTime;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
index bb9b4693..dfba67e8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
@@ -201,13 +201,6 @@ public class DriverWaybillController extends BladeController {
return R.data(driverWaybillService.submitNode(dto));
}
- @PostMapping("/complete")
- @ApiOperationSupport(order = 10)
- @Operation(summary = "司机完成运单", description = "校验司机归属后改状态为已完成,并检查生成应收应付明细(与管理端一致)")
- public R complete(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
- return R.status(driverWaybillService.complete(id));
- }
-
/** 前端可能传空字符串表示「全部」 */
private Integer parseStatus(String status) {
if (Func.isEmpty(status)) {
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 9ad5f543..0cc4d89b 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
@@ -157,11 +157,6 @@ public interface IDriverWaybillService {
*/
DriverNodePunchVO submitNode(NodeSubmitDTO dto);
- /**
- * 司机完成运单:校验归属后改状态为已完成,并走与管理端相同的应收应付明细生成逻辑。
- */
- boolean complete(Long id);
-
/**
* 按运单与节点 key 取该运单实际生效的过程节点配置;节点不存在时返回 null。
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
index d43242fd..e87bc79a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
@@ -108,7 +108,7 @@ public interface IWaybillService extends BaseService {
boolean complete(Long id);
/**
- * 司机端完成运单:跳过管理端部门校验,其余逻辑与 {@link #complete(Long)} 一致
+ * 末位打卡自动完成:跳过管理端部门校验,其余逻辑与 {@link #complete(Long)} 一致
* (改状态 + 生成应收应付明细 + 尝试完成配载单)。
*/
boolean completeWithoutDeptCheck(Long id);
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 ae825828..428c4398 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
@@ -118,13 +118,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final List PENDING_STATUSES = Arrays.asList(
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
);
- /** Tab「全部」统计口径:待接 + 运输中 + 已完成(不含已取消) */
+ /** Tab「全部」统计口径:待接 + 运输中 + 待回单确认 + 已完成(不含已取消) */
private static final List 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,
+ WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, STATUS_COMPLETED
);
- /** 允许完成运单的业务状态 */
- private static final List COMPLETABLE_STATUSES = Arrays.asList(
- STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
+ /**
+ * 进行中口径:司机手上还没走完的单,含待回单确认。
+ * 待回单确认指司机已打完末位卡、等业务员确认回单,此时业务状态还不是 completed。
+ */
+ private static final List ACTIVE_STATUSES = Arrays.asList(
+ STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING,
+ WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM
);
private static final int DEFAULT_PREVIEW_SIZE = 2;
@@ -520,19 +525,32 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
nodePunchMapper.insert(punch);
advanceCurrentProcessNode(waybill, nodeCfg);
- // 末位打卡节点是到货/卸货/签收、且回单无需确认完成时,本次打卡一并把运单置为完成;
- // isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的就是上面那份 processJson,未再调 resolveProcessJson 取过程配置。
+ // 末位打卡节点为到货/卸货/签收、且运单允许完成时:回单无需确认则本次打卡一并完成运单,
+ // 回单需业务员确认则停在待回单确认。
+ // isLastDriverPunchNode 与 returnNeedsNoConfirmation 传的是上面那份 processJson,未再调 resolveProcessJson 取过程配置。
String nodeKey = WaybillProcessSupport.nodeKey(nodeCfg);
String nodeName = WaybillProcessSupport.nodeName(nodeCfg);
+ String waybillBusinessStatus = waybill.getBusinessStatus();
boolean completed = false;
if (WaybillProcessSupport.isLastDriverPunchNode(processJson, nodeKey)
&& WaybillProcessSupport.isDeliveryNode(nodeKey, nodeName)
- && WaybillProcessSupport.returnNeedsNoConfirmation(processJson)
&& canAutoCompleteWaybill(waybill)) {
- completed = waybillService.completeWithoutDeptCheck(waybill.getId());
+ if (WaybillProcessSupport.returnNeedsNoConfirmation(processJson)) {
+ completed = waybillService.completeWithoutDeptCheck(waybill.getId());
+ if (completed) {
+ waybillBusinessStatus = STATUS_COMPLETED;
+ }
+ } else if (WaybillProcessSupport.STATUS_RUNNING.equalsIgnoreCase(waybill.getBusinessStatus())) {
+ // 当前业务状态为 running 才置为待回单确认;canAutoCompleteWaybill 的状态口径还含待执行/待调度。
+ // 待回单确认在 isPunchEnded 里与已完成、已取消同属结束态。
+ waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
+ waybillService.updateById(waybill);
+ waybillBusinessStatus = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
+ }
}
DriverNodePunchVO vo = toNodePunchVO(punch);
vo.setWaybillCompleted(completed);
+ vo.setWaybillBusinessStatus(waybillBusinessStatus);
return vo;
}
@@ -611,24 +629,6 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return waybillService.updateById(waybill);
}
- @Override
- @Transactional(rollbackFor = Exception.class)
- public boolean complete(Long id) {
- if (id == null) {
- throw new ServiceException("运单ID不能为空");
- }
- requireCurrentDriver();
- Waybill waybill = loadDriverWaybill(id);
- waybill = normalizeAcceptStatus(waybill);
- if (!COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())) {
- throw new ServiceException("当前运单状态不允许完成");
- }
- // 有未了结异常时不允许完成——完成接口不要求节点全部打卡,不挡住就能绕过打卡门禁
- assertNoUnresolvedException(waybill.getId());
- // 与管理端完成逻辑一致:改状态 + 生成应收应付明细 + 尝试完成配载单
- return waybillService.completeWithoutDeptCheck(waybill.getId());
- }
-
private void assertAcceptable(Waybill waybill) {
if (WaybillProcessSupport.isTerminalBusinessStatus(waybill.getBusinessStatus())) {
throw new ServiceException("当前运单状态不允许接单");
@@ -843,7 +843,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
switch (status) {
case 0 -> wrapper.in(Waybill::getBusinessStatus, PENDING_STATUSES);
- case 1 -> wrapper.in(Waybill::getBusinessStatus, COMPLETABLE_STATUSES);
+ case 1 -> wrapper.in(Waybill::getBusinessStatus, ACTIVE_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);
@@ -1011,6 +1011,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
WaybillCargoSupport.Quantity weighing = WaybillCargoSupport.weighing(cargos);
card.setWeight(weighing == null ? "" : formatWeight(weighing.value(), weighing.unit()));
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
+ card.setBusinessStatus(waybill.getBusinessStatus());
String createTime = formatDateTime(waybill.getCreateTime());
card.setCreateTime(createTime);
card.setPublishTime(createTime);
@@ -1112,6 +1113,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
card.setId(parseId(children.isEmpty() ? trip.getId() : children.get(0).getId()));
card.setWaybillNo(children.isEmpty() ? card.getTripNo() : Func.toStr(children.get(0).getWaybillNo(), ""));
card.setStatus(trip.getStatus());
+ // 原始业务状态只在整趟车子单状态唯一时下发:配载车混状态时一趟车没有唯一原始状态,给 null 由前端按数字态展示
+ List childBusinessStatuses = children.stream()
+ .map(TripVO.TripWaybillVO::getBusinessStatus)
+ .filter(Func::isNotBlank)
+ .distinct()
+ .collect(Collectors.toList());
+ card.setBusinessStatus(childBusinessStatuses.size() == 1 ? childBusinessStatuses.get(0) : null);
card.setRequireAccept(trip.getRequireAccept());
card.setAcceptStatus(trip.getAcceptStatus());
card.setTransportOrgType(trip.getTransportOrgType());
@@ -1519,27 +1527,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
/**
- * 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。
+ * 自动完成的前置:业务状态在进行中口径内,且没有未了结异常。
*
* 不满足返回 false,由调用方静默跳过,打卡不受影响。
*/
private boolean canAutoCompleteWaybill(Waybill waybill) {
- return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())
+ return ACTIVE_STATUSES.contains(waybill.getBusinessStatus())
&& unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty();
}
- /**
- * 断言该运单没有未了结的异常。
- *
- * 完成运单前必须校验:完成接口本身不要求节点全部打卡,不挡住它就能从
- * 「完成运单」绕过打卡门禁。
- */
- private void assertNoUnresolvedException(Long waybillId) {
- if (!unresolvedExceptionNodeCodeList(waybillId).isEmpty()) {
- throw new ServiceException("该运单存在未处理异常,处理完成后才能完成运单");
- }
- }
-
/**
* 组装过程配置 punch=是 的打卡节点列表。
*
@@ -2003,7 +1999,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
}
return switch (businessStatus) {
case STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING -> 0;
- case STATUS_RUNNING -> 1;
+ case STATUS_RUNNING, WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> 1;
case STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3;
default -> null;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
index 4b1e5c93..a0ee73f3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
@@ -744,6 +744,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
detail.setId(waybill.getId());
detail.setWaybillNo(waybill.getWaybillNo());
detail.setStatus(toAppStatus(waybill.getBusinessStatus()));
+ detail.setBusinessStatus(waybill.getBusinessStatus());
String mode = Func.toStr(waybill.getTransportType(), "");
detail.setTransportMode(mode);
@@ -937,6 +938,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
card.setPlanTime(formatLocalDate(waybill.getEstimatedStartTime()));
card.setPlanTimeEnd(formatLocalDate(waybill.getEstimatedEndTime()));
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
+ card.setBusinessStatus(waybill.getBusinessStatus());
card.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
card.setDriverName(Func.toStr(waybill.getDriverName(), ""));
card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
@@ -994,7 +996,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
return switch (businessStatus) {
case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0;
- case STATUS_RUNNING -> 1;
+ // 数字枚举与司机端 toAppStatus 同源:0 待接单 / 1 运输中 / 2 已完成 / 3 已取消;
+ // 待回单确认属进行中,落 1。给 null 时前端显示「未知」。
+ case STATUS_RUNNING, WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> 1;
case STATUS_COMPLETED -> 2;
case STATUS_CANCELLED -> 3;
default -> null;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java
index 51491538..3c3dc747 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java
@@ -362,7 +362,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl dispatchedGoods = dispatchedGoods(routeWaybills, routePlans);
route.put("dispatchedQuantity", dispatchedGoods.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("dispatchedGoods", dispatchedGoods);
- route.put("arrivedQuantity", routeWaybills.stream().filter(item -> "completed".equals(item.getBusinessStatus())).flatMap(item -> waybillGoods(item).stream()).map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add));
+ route.put("arrivedQuantity", routeWaybills.stream().filter(item -> arrived(item.getBusinessStatus())).flatMap(item -> waybillGoods(item).stream()).map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("waybills", routeWaybills);
route.put("transportPlans", routePlans);
}
@@ -541,12 +541,22 @@ public class MasterOrderServiceImpl extends BaseServiceImpl route) {
return waybillsByMasterNo(masterNo).stream()
- .filter(item -> "completed".equals(item.getBusinessStatus()))
+ .filter(item -> arrived(item.getBusinessStatus()))
.filter(item -> belongsToRoute(item, route))
.flatMap(item -> waybillGoods(item).stream())
.map(item -> decimal(item, "quantity"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
+
+ /**
+ * 已到达口径:待回单确认也算到达——货物确实到了,回单只是单据。
+ * 不计入会按「调度总量不能大于上一段已到达数量」把多联总单的下一段挡在门外。
+ */
+ static boolean arrived(String businessStatus) {
+ return "completed".equals(businessStatus)
+ || WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM.equals(businessStatus);
+ }
+
private Map availableGoods(MasterOrder masterOrder) {
Map available = new LinkedHashMap<>();
for (Map goods : parseArray(masterOrder.getGoodsJson())) available.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add);
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 8ce2f5f7..7a757025 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
@@ -61,6 +61,7 @@ import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.IWaybillService;
+import org.springblade.transport.support.WaybillProcessSupport;
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -3295,6 +3296,7 @@ public class ReceivablePayableDetailServiceImpl
case "completed" -> "已完成";
case "processing", "running", "in_progress", "inProgress" -> "进行中";
case "pending", "created" -> "待执行";
+ case WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> "待回单确认";
case "cancelled", "canceled" -> "已取消";
default -> status;
};
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripAggregation.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripAggregation.java
index d55d700a..6eb4689a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripAggregation.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripAggregation.java
@@ -45,6 +45,7 @@ final class TripAggregation {
static final String RUNNING = "running";
static final String COMPLETED = "completed";
static final String CANCELLED = "cancelled";
+ static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
/** 待接单档:与运单侧 Tab「待接单」取同一组状态 */
static final List PENDING_STATUSES = List.of(PENDING, WAITING_DISPATCH, DISPATCHING);
@@ -53,9 +54,10 @@ final class TripAggregation {
/**
* 进运次的子单状态:草稿尚未派单、不成运次;已取消要进,
* 否则「子单全已取消」会被算成运输中而不是已取消。
+ * 待回单确认要进:子单漏在运次外,剩下那几张会被判成整趟车已走完。
*/
static final List CHILD_STATUSES = List.of(
- PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, COMPLETED, CANCELLED
+ PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, PENDING_RETURN_CONFIRM, COMPLETED, CANCELLED
);
/** 待重新派单的口径:子单被拒且这趟车还没结束 */
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
index 9f40356f..a3ca5791 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
@@ -531,10 +531,33 @@ public class WaybillServiceImpl extends BaseServiceImpl
}
// 只有显式传 false 才是取消打卡,未传按已打卡处理
boolean punched = !Boolean.FALSE.equals(request.getPunched());
- if (ENROUTE_PUNCH_TYPE.equalsIgnoreCase(TransportBusinessSupport.trimToEmpty(request.getType()))) {
- return saveEnroutePunchRecord(waybill, request, punched);
+ // 待回单确认的运单司机已无待打卡节点,再补一条打卡流水与状态含义矛盾;
+ // 漏打的出路是先取消该节点的打卡退回进行中(punched=false),再重新补录。
+ if (punched && WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM
+ .equalsIgnoreCase(waybill.getBusinessStatus())) {
+ throw new ServiceException("运单待业务员确认回单,不能补录打卡");
}
- return saveNodePunchRecord(waybill, request, punched);
+ if (ENROUTE_PUNCH_TYPE.equalsIgnoreCase(TransportBusinessSupport.trimToEmpty(request.getType()))) {
+ return revertIfReturnConfirmPreconditionLost(waybill, punched,
+ saveEnroutePunchRecord(waybill, request, punched));
+ }
+ return revertIfReturnConfirmPreconditionLost(waybill, punched,
+ saveNodePunchRecord(waybill, request, punched));
+ }
+
+ /**
+ * 取消打卡撤掉了「司机已无待打卡节点」这个前提,停在待回单确认的运单退回进行中。
+ *
+ * 只认取消打卡后的待回单确认,其它状态一律不动。
+ */
+ private boolean revertIfReturnConfirmPreconditionLost(Waybill waybill, boolean punched, boolean saved) {
+ if (!saved || punched
+ || !WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM
+ .equalsIgnoreCase(waybill.getBusinessStatus())) {
+ return saved;
+ }
+ waybill.setBusinessStatus(WaybillProcessSupport.STATUS_RUNNING);
+ return updateById(waybill);
}
/**
@@ -962,13 +985,16 @@ public class WaybillServiceImpl extends BaseServiceImpl
/**
* 运单是否已越过接单阶段。
*
- * 正向列举进行中/已完成/已取消,不能用「不是终态」取反:终态里还包含草稿、待调度、
+ * 正向列举进行中/已完成/已取消/待回单确认,不能用「不是终态」取反:终态里还包含草稿、待调度、
* 调度中这些并未越过接单的阶段,取反会把它们判成已接单。
+ *
+ * 待回单确认下司机已打完末位卡,无需接单配置时接单节点不该再显示未打卡。
*/
private static boolean passedAcceptStage(String businessStatus) {
return "running".equals(businessStatus)
|| "completed".equals(businessStatus)
- || "cancelled".equals(businessStatus);
+ || "cancelled".equals(businessStatus)
+ || WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM.equals(businessStatus);
}
private String formatPunchTime(Date time) {
@@ -1062,10 +1088,11 @@ public class WaybillServiceImpl extends BaseServiceImpl
waybill.setMasterNo(null);
waybill.setMileageRemark(null);
}
- // 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致
+ // 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致;
+ // 编辑保存只改资料,business_status 以库里的值为准,请求体里的值不回写
String preservedBusinessStatus = created
? TransportBusinessSupport.trimToNull(waybill.getBusinessStatus())
- : null;
+ : editPreservedBusinessStatus(oldRecord);
if (isCompletedBusinessStatus(preservedBusinessStatus)) {
preservedBusinessStatus = "completed";
}
@@ -1087,6 +1114,17 @@ public class WaybillServiceImpl extends BaseServiceImpl
}
}
+ /**
+ * 编辑保存要回写的业务状态:取库里的值。
+ * 草稿返回 null —— submit 即正式提交,草稿须由过程校正提升为待执行 / 进行中。
+ */
+ private String editPreservedBusinessStatus(Waybill oldRecord) {
+ String currentStatus = oldRecord == null
+ ? null
+ : TransportBusinessSupport.trimToNull(oldRecord.getBusinessStatus());
+ return STATUS_DRAFT.equals(currentStatus) ? null : currentStatus;
+ }
+
private void fillCustomerName(Waybill waybill) {
if (Func.isNotEmpty(waybill.getContractId())) {
ContractManage contract = contractManageService.getById(waybill.getContractId());
@@ -1507,14 +1545,14 @@ public class WaybillServiceImpl extends BaseServiceImpl
@Transactional(rollbackFor = Exception.class)
public boolean confirmReturnReceipt(Long id) {
Waybill waybill = loadEditable(id, true);
+ // 判据只看运单当前状态,不看回单配置当前值:配置改回「无需确认」后,
+ // 已停在待回单确认的运单仍要能确认回单
+ if (!WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM.equalsIgnoreCase(waybill.getBusinessStatus())) {
+ throw new ServiceException("当前状态不允许确认回单");
+ }
String processJson = LiveProcessConfigSupport.resolve(
LiveProcessConfigSupport.byProject(processConfigService, waybill.getProjectId()),
waybill.getProcessJson());
- if (WaybillProcessSupport.returnNeedsNoConfirmation(processJson)) {
- // 不要求确认时司机打完末位卡就已经完成了,运单不会停在进行中等确认;配置对不上要说清,
- // 否则用户会以为是确认没生效
- throw new ServiceException("该运单的回单不需要业务员确认");
- }
assertPunchNodesFinished(waybill.getId(), processJson);
assertNoUnresolvedException(waybill.getId());
return doComplete(waybill);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java
index 4462c3a3..3b9d920e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/WaybillProcessSupport.java
@@ -49,6 +49,8 @@ public final class WaybillProcessSupport {
public static final String STATUS_PENDING = "pending";
public static final String STATUS_RUNNING = "running";
+ /** 司机已打完末位卡、回单需业务员确认,尚未生成应收应付 */
+ public static final String STATUS_PENDING_RETURN_CONFIRM = "pending_return_confirm";
public static final String ACCEPT_PENDING = "pending";
public static final String ACCEPT_ACCEPTED = "accepted";
public static final String ACCEPT_REJECTED = "rejected";
@@ -134,14 +136,16 @@ public final class WaybillProcessSupport {
}
/**
- * 运单已结束:已完成或已取消。此后不再接受司机打卡。
+ * 运单已结束:已完成、已取消,或已打完末位卡停在待回单确认。此后不再接受司机打卡。
*
- * 只认这两个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡,
+ * 只认这三个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡,
* 所以不能按 {@link #isTerminalBusinessStatus} 的「终态或调度中间态」口径判定。
*/
public static boolean isPunchEnded(String businessStatus) {
String status = stringVal(businessStatus);
- return "completed".equalsIgnoreCase(status) || "cancelled".equalsIgnoreCase(status);
+ return "completed".equalsIgnoreCase(status)
+ || "cancelled".equalsIgnoreCase(status)
+ || STATUS_PENDING_RETURN_CONFIRM.equalsIgnoreCase(status);
}
/**
@@ -155,6 +159,9 @@ public final class WaybillProcessSupport {
if ("cancelled".equalsIgnoreCase(status)) {
return "运单已取消";
}
+ if (STATUS_PENDING_RETURN_CONFIRM.equalsIgnoreCase(status)) {
+ return "运单待业务员确认回单";
+ }
return null;
}
@@ -195,7 +202,7 @@ public final class WaybillProcessSupport {
* 上报异常前的运单状态门禁原因;可上报时返回 null。
*
* 与打卡共用「已开始运输」判据,但文案说的是能不能报,不是能不能打。
- * 结束优先于未开始:已完成/已取消的运单先说结束,不再提「等你上路」。
+ * 结束优先于未开始:已结束的运单先说结束,不再提「等你上路」。
*/
public static String reportBlockedReason(String businessStatus) {
String endedReason = punchEndedReason(businessStatus);
@@ -213,7 +220,8 @@ public final class WaybillProcessSupport {
|| "completed".equals(businessStatus)
|| "cancelled".equals(businessStatus)
|| "waiting_dispatch".equals(businessStatus)
- || "dispatching".equals(businessStatus);
+ || "dispatching".equals(businessStatus)
+ || STATUS_PENDING_RETURN_CONFIRM.equals(businessStatus);
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java
index 1dab5a5b..d0257f86 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/WaybillWrapper.java
@@ -122,6 +122,7 @@ public class WaybillWrapper extends BaseEntityWrapper {
case "waiting_dispatch" -> "待调度";
case "dispatching" -> "调度中";
case "running", "processing", "in_progress", "inProgress" -> "进行中";
+ case WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> "待回单确认";
case "completed" -> "已完成";
case "cancelled" -> "已取消";
case "closed" -> "已关闭";
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/WaybillCompleteEndpointTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/WaybillCompleteEndpointTest.java
new file mode 100644
index 00000000..fae4e73d
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/controller/WaybillCompleteEndpointTest.java
@@ -0,0 +1,71 @@
+package org.springblade.transport.controller;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springblade.transport.service.IContractManageService;
+import org.springblade.transport.service.ICustomerArchiveService;
+import org.springblade.transport.service.IDriverWaybillService;
+import org.springblade.transport.service.IProjectApplyService;
+import org.springblade.transport.service.ITransportPlanService;
+import org.springblade.transport.service.IWaybillImportBatchService;
+import org.springblade.transport.service.IWaybillService;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * 运单「完成运单」接口的边界:司机端不再有完成入口,管理端强制完成仍可达。
+ *
+ * 走真实 HTTP 路由:司机端那条链删没删干净由 404 说话,管理端保持可用由 200 + 落到
+ * {@code waybillService.complete} 说话。
+ */
+class WaybillCompleteEndpointTest {
+
+ private IWaybillService waybillService;
+ private MockMvc driverMvc;
+ private MockMvc manageMvc;
+
+ @BeforeEach
+ void setUp() {
+ waybillService = mock(IWaybillService.class);
+ DriverWaybillController driverController = new DriverWaybillController(mock(IDriverWaybillService.class));
+ driverMvc = MockMvcBuilders.standaloneSetup(driverController).build();
+ WaybillController manageController = new WaybillController(
+ waybillService,
+ mock(IProjectApplyService.class),
+ mock(IContractManageService.class),
+ mock(ICustomerArchiveService.class),
+ mock(ITransportPlanService.class),
+ mock(IWaybillImportBatchService.class));
+ manageMvc = MockMvcBuilders.standaloneSetup(manageController).build();
+ }
+
+ @Test
+ @DisplayName("POST /blade-transport/waybill/complete:司机端完成运单路由已下线")
+ void driverCompleteRouteIsGone() throws Exception {
+ driverMvc.perform(post("/blade-transport/waybill/complete").param("id", "6001"))
+ .andExpect(status().isNotFound());
+ driverMvc.perform(post("/waybill/complete").param("id", "6001"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @DisplayName("POST /waybill-manage/complete:管理端强制完成仍可达")
+ void manageCompleteRouteIsKept() throws Exception {
+ when(waybillService.complete(6001L)).thenReturn(true);
+
+ manageMvc.perform(post("/waybill-manage/complete").param("id", "6001"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(waybillService).complete(6001L);
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
index 7bdcbba1..41189b41 100644
--- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverTripCardTest.java
@@ -30,6 +30,7 @@ import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.CertificationGate;
+import org.springblade.transport.support.WaybillProcessSupport;
import java.math.BigDecimal;
import java.util.Date;
@@ -147,6 +148,34 @@ class DriverTripCardTest {
assertEquals("桂A32523", card.getVehicleNo());
}
+ @Test
+ @DisplayName("运次卡片带唯一原始业务状态:子单都在待回单确认时下发 pending_return_confirm,数字态仍是 1")
+ void tripCardCarriesUniqueChildBusinessStatus() {
+ TripVO trip = loadingTrip();
+ trip.getWaybills().forEach(child -> child.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM));
+ stubTripPage(1, trip);
+
+ IPage page = runAsDriver(() -> service.tripPage(1, 10, null, null));
+
+ DriverWaybillCardVO card = page.getRecords().get(0);
+ assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, card.getBusinessStatus());
+ assertEquals(trip.getStatus(), card.getStatus(), "运次数字态口径不变,仍取运次状态");
+ }
+
+ @Test
+ @DisplayName("运次卡片原始业务状态:配载车混状态时给 null,由前端回退数字态")
+ void tripCardWithoutUniqueChildBusinessStatusGivesNull() {
+ TripVO trip = loadingTrip();
+ trip.getWaybills().get(0).setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
+ stubTripPage(1, trip);
+
+ IPage page = runAsDriver(() -> service.tripPage(1, 10, null, null));
+
+ DriverWaybillCardVO card = page.getRecords().get(0);
+ assertNull(card.getBusinessStatus(), "一趟车里两种原始状态,没有唯一值可下发");
+ assertEquals(1, card.getStatus(), "混状态不影响运次数字态");
+ }
+
@Test
@DisplayName("运次列表不因配载卡去回查运单,也不写库")
void tripPageDoesNotLookUpWaybillsOrWrite() {
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 f2eeac76..6a23c1ac 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
@@ -7,6 +7,7 @@ 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;
@@ -28,6 +29,7 @@ import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.support.CertificationGate;
+import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,6 +37,7 @@ 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.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -42,11 +45,13 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
- * 司机打完末位打卡节点后运单自动完成。
+ * 司机打完末位打卡节点后运单的去向。
*
- * 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、
- * 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水,
- * 响应里的 waybillCompleted 始终取完成接口的返回值,前端据此决定是否提示「运单已完成」。
+ * 末位打卡是到货/卸货/签收、运单本身允许完成时,回单是否需业务员确认决定去向:
+ * 无需确认则本次一并完成运单,需确认则把运单置为待回单确认等业务员动手。
+ * 任一条件不成立都只落打卡流水。
+ * 响应里的 waybillCompleted 始终取完成接口的返回值,前端据此决定是否提示「运单已完成」;
+ * 待回单确认不是已完成,只能靠 waybillBusinessStatus 分辨。
*/
class DriverWaybillAutoCompleteTest {
@@ -67,11 +72,24 @@ class DriverWaybillAutoCompleteTest {
{"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}]
""";
+ /** 装货 → 签收两个司机打卡节点,末位是签收,回单需确认完成 */
+ private static final String LOAD_SIGN_RETURN_CONFIRM_JSON = """
+ [{"key":"load","name":"装货","type":"cargo","enabled":true,"punch":true},
+ {"key":"sign","name":"签收","type":"cargo","enabled":true,"punch":true},
+ {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}]
+ """;
+
/** 末位打卡节点是「发货」,不是到货/卸货/签收 */
private static final String DISPATCH_LAST_JSON = """
[{"key":"dispatch","name":"发货","type":"cargo","enabled":true,"punch":true}]
""";
+ /** 末位司机打卡节点是「发货」,回单启用且需确认完成 */
+ private static final String DISPATCH_LAST_RETURN_CONFIRM_JSON = """
+ [{"key":"dispatch","name":"发货","type":"cargo","enabled":true,"punch":true},
+ {"key":"return","name":"回单","type":"confirm","enabled":true,"punch":true,"confirmMode":"yes"}]
+ """;
+
private IWaybillService waybillService;
private IDriverService driverService;
private WaybillNodePunchMapper nodePunchMapper;
@@ -118,7 +136,7 @@ class DriverWaybillAutoCompleteTest {
// ------------------------------------------------------------ 命中
@Test
- @DisplayName("末位签收打卡且回单无需确认时调完成接口,响应标记 waybillCompleted")
+ @DisplayName("末位签收打卡且回单无需确认时调完成接口,响应标记 waybillCompleted 与 completed")
void lastDeliveryPunchCompletesWaybill() {
givenWaybill(SIGN_LAST_JSON, "running");
when(waybillService.completeWithoutDeptCheck(WAYBILL_ID)).thenReturn(true);
@@ -127,6 +145,7 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
assertTrue(vo.getWaybillCompleted(), "完成接口返回成功,响应要告诉前端运单已完成");
+ assertEquals("completed", vo.getWaybillBusinessStatus(), "完成后运单业务状态要如实回报");
}
@Test
@@ -139,6 +158,25 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService).completeWithoutDeptCheck(WAYBILL_ID);
assertFalse(vo.getWaybillCompleted(), "完成接口自己说不算完成时,前端不能提示运单已完成");
+ assertEquals("running", vo.getWaybillBusinessStatus(), "完成没落库,运单仍在进行中");
+ }
+
+ // ------------------------------------------------------------ 待回单确认
+
+ @Test
+ @DisplayName("回单需业务员确认时末位打卡进入待回单确认,落库并回报,且不调完成接口")
+ void returnNeedingConfirmationEntersPendingReturnConfirm() {
+ givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
+
+ DriverNodePunchVO vo = submit("sign");
+
+ verify(waybillService, never()).completeWithoutDeptCheck(any());
+ verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
+ assertFalse(vo.getWaybillCompleted(), "待回单确认不是已完成");
+ assertEquals("pending_return_confirm", vo.getWaybillBusinessStatus(),
+ "客户端要知道打卡已提交、等业务员确认回单");
+ assertEquals("pending_return_confirm", writtenBusinessStatus(),
+ "状态要落库,否则下次查询又回到进行中");
}
// ------------------------------------------------------------ 不命中
@@ -153,18 +191,31 @@ class DriverWaybillAutoCompleteTest {
verify(waybillService, never()).completeWithoutDeptCheck(any());
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
assertFalse(vo.getWaybillCompleted());
+ assertEquals("running", vo.getWaybillBusinessStatus());
}
@Test
- @DisplayName("回单启用且需确认完成时只打卡不完成")
- void returnNeedingConfirmationDoesNotComplete() {
- givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
+ @DisplayName("回单需确认但不是末位打卡:运单留在进行中")
+ void nonLastPunchDoesNotEnterReturnConfirmation() {
+ givenWaybill(LOAD_SIGN_RETURN_CONFIRM_JSON, "running");
- DriverNodePunchVO vo = submit("sign");
+ DriverNodePunchVO vo = submit("load");
+
+ verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
+ assertEquals("running", vo.getWaybillBusinessStatus(), "后面还有签收要打,不能提前停在待回单确认");
+ assertEquals("running", writtenBusinessStatus());
+ }
+
+ @Test
+ @DisplayName("回单需确认但末位不是到货/卸货/签收:运单留在进行中")
+ void nonDeliveryLastNodeDoesNotEnterReturnConfirmation() {
+ givenWaybill(DISPATCH_LAST_RETURN_CONFIRM_JSON, "running");
+
+ DriverNodePunchVO vo = submit("dispatch");
verify(waybillService, never()).completeWithoutDeptCheck(any());
- verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
- assertFalse(vo.getWaybillCompleted());
+ assertEquals("running", vo.getWaybillBusinessStatus(), "发货不是到货类节点,打完不该停在等待回单");
+ assertEquals("running", writtenBusinessStatus());
}
// ------------------------------------------------------------ 前置:运单本身能否完成
@@ -208,8 +259,50 @@ class DriverWaybillAutoCompleteTest {
assertFalse(vo.getWaybillCompleted());
}
+ @Test
+ @DisplayName("有未了结异常时回单需确认也不进待回单确认——四条进入条件共同成立才进")
+ void unresolvedExceptionBlocksEnteringReturnConfirmation() {
+ givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
+ when(exceptionDisposalMapper.selectList(any())).thenReturn(List.of(unresolvedException("unload")));
+
+ DriverNodePunchVO vo = submit("sign");
+
+ assertEquals("running", vo.getWaybillBusinessStatus(), "异常未了结,不能把运单停在等待回单");
+ assertEquals("running", writtenBusinessStatus());
+ }
+
+ // ------------------------------------------------------------ 并发:只从进行中进入
+
+ @Test
+ @DisplayName("打卡期间运单已被推到终态时不再写待回单确认——只从进行中进入")
+ void concurrentTerminalStatusPreventsEnteringReturnConfirmation() {
+ givenWaybill(SIGN_LAST_RETURN_CONFIRM_JSON, "running");
+ List statusesWritten = new ArrayList<>();
+ // 模拟另一事务在这次打卡推进过程节点时把运单置为已完成(管理端强制完成)
+ when(waybillService.updateById(any())).thenAnswer(invocation -> {
+ Waybill entity = invocation.getArgument(0);
+ statusesWritten.add(entity.getBusinessStatus());
+ entity.setBusinessStatus("completed");
+ return true;
+ });
+
+ DriverNodePunchVO vo = submit("sign");
+
+ assertFalse(statusesWritten.contains("pending_return_confirm"),
+ "运单已被完成,不能再改写回待回单确认");
+ assertEquals("completed", vo.getWaybillBusinessStatus());
+ }
+
// ------------------------------------------------------------ 夹具
+ /** 打卡流程最后一次落库时运单的业务状态;状态写库走 updateById,各次传的是同一个实体引用 */
+ private String writtenBusinessStatus() {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Waybill.class);
+ verify(waybillService, atLeastOnce()).updateById(captor.capture());
+ List written = captor.getAllValues();
+ return written.get(written.size() - 1).getBusinessStatus();
+ }
+
private DriverNodePunchVO submit(String nodeCode) {
NodeSubmitDTO dto = new NodeSubmitDTO();
dto.setWaybillId(WAYBILL_ID);
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java
new file mode 100644
index 00000000..668dc965
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/DriverWaybillTabStatusTest.java
@@ -0,0 +1,194 @@
+package org.springblade.transport.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+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.MockedStatic;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.system.feign.IMpUserClient;
+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.pojo.entity.WaybillNodePunch;
+import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
+import org.springblade.transport.pojo.vo.DriverWaybillTabCountsVO;
+import org.springblade.transport.service.IDriverService;
+import org.springblade.transport.service.IProcessConfigService;
+import org.springblade.transport.service.ITransportVehicleService;
+import org.springblade.transport.service.ITripQueryService;
+import org.springblade.transport.service.IWaybillService;
+import org.springblade.transport.support.CertificationGate;
+import org.springblade.transport.support.WaybillProcessSupport;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.when;
+
+/**
+ * 司机端四个 Tab 的状态分档。
+ *
+ * 待回单确认是「等业务员动手」的进行中:数字态沿用 1(进行中),查询口径也必须认它。
+ * 漏掉的话司机打完末位卡,这张单从他的列表里彻底消失——不是展示问题,是单子不见了。
+ */
+class DriverWaybillTabStatusTest {
+
+ private static final Long WAYBILL_ID = 5901L;
+ private static final Long USER_ID = 7001L;
+ private static final String PLATE = "桂AAW3256";
+ private static final String PHONE = "13800138002";
+
+ private IWaybillService waybillService;
+ private IDriverService driverService;
+ private WaybillNodePunchMapper nodePunchMapper;
+ private ExceptionDisposalMapper exceptionDisposalMapper;
+ private final List> queried = new ArrayList<>();
+ private DriverWaybillServiceImpl service;
+
+ @BeforeAll
+ static void initTableInfo() {
+ // 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
+ assistant.setCurrentNamespace("test");
+ TableInfoHelper.initTableInfo(assistant, Waybill.class);
+ TableInfoHelper.initTableInfo(assistant, Driver.class);
+ TableInfoHelper.initTableInfo(assistant, WaybillNodePunch.class);
+ }
+
+ @BeforeEach
+ void setUp() {
+ waybillService = mock(IWaybillService.class);
+ driverService = mock(IDriverService.class);
+ nodePunchMapper = mock(WaybillNodePunchMapper.class);
+ exceptionDisposalMapper = mock(ExceptionDisposalMapper.class);
+ service = new DriverWaybillServiceImpl(
+ waybillService,
+ driverService,
+ mock(IMpUserClient.class),
+ mock(IProcessConfigService.class),
+ mock(ITransportVehicleService.class),
+ mock(CertificationGate.class),
+ mock(WaybillEnroutePunchMapper.class),
+ nodePunchMapper,
+ exceptionDisposalMapper,
+ mock(ITripQueryService.class));
+
+ when(driverService.getOne(any())).thenReturn(driver());
+ when(waybillService.syncDriverAcceptState(any())).thenAnswer(invocation -> invocation.getArgument(0));
+ when(waybillService.list(anyWrapper())).thenAnswer(invocation -> {
+ queried.add(invocation.getArgument(0));
+ return List.of(pendingReturnConfirmWaybill());
+ });
+ }
+
+ @Test
+ @DisplayName("待回单确认落在「进行中」档:列表查得到,数字态是 1")
+ void pendingReturnConfirmIsInProgressTier() {
+ IPage page = runAsDriver(() -> service.page(1, 10, 1, null));
+
+ assertEquals(1, page.getRecords().size(), "进行中档必须包含待回单确认,否则司机看不到这张单");
+ assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus(), "数字态沿用 1(进行中),不新增档位");
+ assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
+ page.getRecords().get(0).getBusinessStatus(),
+ "原始业务状态随卡片下发:数字态恒为 1,前端靠它区分运输中与待回单确认");
+ }
+
+ @Test
+ @DisplayName("「进行中」查询口径在库层就带上待回单确认")
+ void inProgressQueryCarriesPendingReturnConfirm() {
+ runAsDriver(() -> service.page(1, 10, 1, null));
+
+ assertTrue(queriedStatuses().contains(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM));
+ }
+
+ @Test
+ @DisplayName("「全部」tab 不漏掉待回单确认的运单")
+ void allTabKeepsPendingReturnConfirm() {
+ IPage page = runAsDriver(() -> service.page(1, 10, null, null));
+
+ assertEquals(1, page.getRecords().size(), "打完末位卡的单不能从全部 tab 消失");
+ assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus());
+ assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
+ page.getRecords().get(0).getBusinessStatus());
+ assertTrue(queriedStatuses().contains(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM));
+ }
+
+ @Test
+ @DisplayName("Tab 计数把它算进进行中,不算已完成")
+ void tabCountsCountsItAsDoing() {
+ DriverWaybillTabCountsVO counts = runAsDriver(service::tabCounts);
+
+ assertEquals(1, counts.getDoing(), "待回单确认仍是进行中,不是已完成");
+ assertEquals(0, counts.getDone());
+ assertEquals(1, counts.getAll());
+ }
+
+ // ------------------------------------------------------------ 夹具
+
+ private T runAsDriver(Supplier action) {
+ try (MockedStatic authUtil = mockStatic(AuthUtil.class)) {
+ authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
+ return action.get();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Collection