🔀 合并待回单确认功能
This commit is contained in:
+3
@@ -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;
|
||||
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
|
||||
+3
@@ -52,4 +52,7 @@ public class DriverNodePunchVO implements Serializable {
|
||||
@Schema(description = "本次打卡是否使运单自动完成")
|
||||
private Boolean waybillCompleted;
|
||||
|
||||
@Schema(description = "本次打卡后的运单业务状态:running / pending_return_confirm / completed")
|
||||
private String waybillBusinessStatus;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
|
||||
-7
@@ -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)) {
|
||||
|
||||
-5
@@ -157,11 +157,6 @@ public interface IDriverWaybillService {
|
||||
*/
|
||||
DriverNodePunchVO submitNode(NodeSubmitDTO dto);
|
||||
|
||||
/**
|
||||
* 司机完成运单:校验归属后改状态为已完成,并走与管理端相同的应收应付明细生成逻辑。
|
||||
*/
|
||||
boolean complete(Long id);
|
||||
|
||||
/**
|
||||
* 按运单与节点 key 取该运单实际生效的过程节点配置;节点不存在时返回 null。
|
||||
* <p>
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
boolean complete(Long id);
|
||||
|
||||
/**
|
||||
* 司机端完成运单:跳过管理端部门校验,其余逻辑与 {@link #complete(Long)} 一致
|
||||
* 末位打卡自动完成:跳过管理端部门校验,其余逻辑与 {@link #complete(Long)} 一致
|
||||
* (改状态 + 生成应收应付明细 + 尝试完成配载单)。
|
||||
*/
|
||||
boolean completeWithoutDeptCheck(Long id);
|
||||
|
||||
+39
-43
@@ -118,13 +118,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
private static final List<String> PENDING_STATUSES = Arrays.asList(
|
||||
STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
|
||||
);
|
||||
/** Tab「全部」统计口径:待接 + 运输中 + 已完成(不含已取消) */
|
||||
/** Tab「全部」统计口径:待接 + 运输中 + 待回单确认 + 已完成(不含已取消) */
|
||||
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,
|
||||
WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, STATUS_COMPLETED
|
||||
);
|
||||
/** 允许完成运单的业务状态 */
|
||||
private static final List<String> COMPLETABLE_STATUSES = Arrays.asList(
|
||||
STATUS_RUNNING, STATUS_PENDING, STATUS_WAITING_DISPATCH, STATUS_DISPATCHING
|
||||
/**
|
||||
* 进行中口径:司机手上还没走完的单,含待回单确认。
|
||||
* 待回单确认指司机已打完末位卡、等业务员确认回单,此时业务状态还不是 completed。
|
||||
*/
|
||||
private static final List<String> 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<String> 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动完成的前置:与司机端「完成运单」同一套——业务状态允许完成,且没有未了结异常。
|
||||
* 自动完成的前置:业务状态在进行中口径内,且没有未了结异常。
|
||||
* <p>
|
||||
* 不满足返回 false,由调用方静默跳过,打卡不受影响。
|
||||
*/
|
||||
private boolean canAutoCompleteWaybill(Waybill waybill) {
|
||||
return COMPLETABLE_STATUSES.contains(waybill.getBusinessStatus())
|
||||
return ACTIVE_STATUSES.contains(waybill.getBusinessStatus())
|
||||
&& unresolvedExceptionNodeCodeList(waybill.getId()).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言该运单没有未了结的异常。
|
||||
* <p>
|
||||
* 完成运单前必须校验:完成接口本身不要求节点全部打卡,不挡住它就能从
|
||||
* 「完成运单」绕过打卡门禁。
|
||||
*/
|
||||
private void assertNoUnresolvedException(Long waybillId) {
|
||||
if (!unresolvedExceptionNodeCodeList(waybillId).isEmpty()) {
|
||||
throw new ServiceException("该运单存在未处理异常,处理完成后才能完成运单");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装过程配置 punch=是 的打卡节点列表。
|
||||
* <p>
|
||||
@@ -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;
|
||||
|
||||
+5
-1
@@ -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;
|
||||
|
||||
+12
-2
@@ -362,7 +362,7 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
Map<String, BigDecimal> 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<MasterOrderMapper, M
|
||||
}
|
||||
private BigDecimal completedQuantity(String masterNo, Map<String, Object> 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<String, BigDecimal> availableGoods(MasterOrder masterOrder) {
|
||||
Map<String, BigDecimal> available = new LinkedHashMap<>();
|
||||
for (Map<String, Object> goods : parseArray(masterOrder.getGoodsJson())) available.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add);
|
||||
|
||||
+2
@@ -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;
|
||||
};
|
||||
|
||||
+3
-1
@@ -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<String> PENDING_STATUSES = List.of(PENDING, WAITING_DISPATCH, DISPATCHING);
|
||||
@@ -53,9 +54,10 @@ final class TripAggregation {
|
||||
/**
|
||||
* 进运次的子单状态:草稿尚未派单、不成运次;已取消要进,
|
||||
* 否则「子单全已取消」会被算成运输中而不是已取消。
|
||||
* 待回单确认要进:子单漏在运次外,剩下那几张会被判成整趟车已走完。
|
||||
*/
|
||||
static final List<String> CHILD_STATUSES = List.of(
|
||||
PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, COMPLETED, CANCELLED
|
||||
PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, PENDING_RETURN_CONFIRM, COMPLETED, CANCELLED
|
||||
);
|
||||
|
||||
/** 待重新派单的口径:子单被拒且这趟车还没结束 */
|
||||
|
||||
+50
-12
@@ -531,10 +531,33 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
}
|
||||
// 只有显式传 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消打卡撤掉了「司机已无待打卡节点」这个前提,停在待回单确认的运单退回进行中。
|
||||
* <p>
|
||||
* 只认取消打卡后的待回单确认,其它状态一律不动。
|
||||
*/
|
||||
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<WaybillMapper, Waybill>
|
||||
/**
|
||||
* 运单是否已越过接单阶段。
|
||||
* <p>
|
||||
* 正向列举进行中/已完成/已取消,不能用「不是终态」取反:终态里还包含草稿、待调度、
|
||||
* 正向列举进行中/已完成/已取消/待回单确认,不能用「不是终态」取反:终态里还包含草稿、待调度、
|
||||
* 调度中这些并未越过接单的阶段,取反会把它们判成已接单。
|
||||
* <p>
|
||||
* 待回单确认下司机已打完末位卡,无需接单配置时接单节点不该再显示未打卡。
|
||||
*/
|
||||
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<WaybillMapper, Waybill>
|
||||
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<WaybillMapper, Waybill>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑保存要回写的业务状态:取库里的值。
|
||||
* 草稿返回 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<WaybillMapper, Waybill>
|
||||
@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);
|
||||
|
||||
+13
-5
@@ -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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 运单已结束:已完成或已取消。此后不再接受司机打卡。
|
||||
* 运单已结束:已完成、已取消,或已打完末位卡停在待回单确认。此后不再接受司机打卡。
|
||||
* <p>
|
||||
* 只认这两个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡,
|
||||
* 只认这三个状态,大小写不敏感。草稿、待执行、待调度、调度中的运单司机仍要能打卡,
|
||||
* 所以不能按 {@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。
|
||||
* <p>
|
||||
* 与打卡共用「已开始运输」判据,但文案说的是能不能报,不是能不能打。
|
||||
* 结束优先于未开始:已完成/已取消的运单先说结束,不再提「等你上路」。
|
||||
* 结束优先于未开始:已结束的运单先说结束,不再提「等你上路」。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -122,6 +122,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
||||
case "waiting_dispatch" -> "待调度";
|
||||
case "dispatching" -> "调度中";
|
||||
case "running", "processing", "in_progress", "inProgress" -> "进行中";
|
||||
case WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM -> "待回单确认";
|
||||
case "completed" -> "已完成";
|
||||
case "cancelled" -> "已取消";
|
||||
case "closed" -> "已关闭";
|
||||
|
||||
+71
@@ -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;
|
||||
|
||||
/**
|
||||
* 运单「完成运单」接口的边界:司机端不再有完成入口,管理端强制完成仍可达。
|
||||
* <p>
|
||||
* 走真实 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);
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -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<DriverWaybillCardVO> 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<DriverWaybillCardVO> 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() {
|
||||
|
||||
+104
-11
@@ -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;
|
||||
|
||||
/**
|
||||
* 司机打完末位打卡节点后运单自动完成。
|
||||
* 司机打完末位打卡节点后运单的去向。
|
||||
* <p>
|
||||
* 命中要四个条件同时成立:本次打卡的是「司机可打卡节点」的末位、该节点是到货/卸货/签收、
|
||||
* 回单无需确认完成、运单本身允许完成。任一不成立都只落打卡流水,
|
||||
* 响应里的 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<String> 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<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
|
||||
verify(waybillService, atLeastOnce()).updateById(captor.capture());
|
||||
List<Waybill> written = captor.getAllValues();
|
||||
return written.get(written.size() - 1).getBusinessStatus();
|
||||
}
|
||||
|
||||
private DriverNodePunchVO submit(String nodeCode) {
|
||||
NodeSubmitDTO dto = new NodeSubmitDTO();
|
||||
dto.setWaybillId(WAYBILL_ID);
|
||||
|
||||
+194
@@ -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 的状态分档。
|
||||
* <p>
|
||||
* 待回单确认是「等业务员动手」的进行中:数字态沿用 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<Wrapper<Waybill>> 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<DriverWaybillCardVO> 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<DriverWaybillCardVO> 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> T runAsDriver(Supplier<T> action) {
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getUserId).thenReturn(USER_ID);
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Collection<Object> queriedStatuses() {
|
||||
assertEquals(1, queried.size(), "一次列表查询");
|
||||
LambdaQueryWrapper<Waybill> wrapper = (LambdaQueryWrapper<Waybill>) queried.get(0);
|
||||
// 参数槽由 SQL 片段构建时填充:不先取一次片段,参数表是空的
|
||||
wrapper.getSqlSegment();
|
||||
Collection<Object> statuses = new ArrayList<>();
|
||||
wrapper.getParamNameValuePairs().values().forEach(value -> {
|
||||
if (value instanceof Collection<?> items) {
|
||||
statuses.addAll(items);
|
||||
} else {
|
||||
statuses.add(value);
|
||||
}
|
||||
});
|
||||
return statuses;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Wrapper<Waybill> anyWrapper() {
|
||||
return org.mockito.ArgumentMatchers.<Wrapper<Waybill>>any();
|
||||
}
|
||||
|
||||
private Waybill pendingReturnConfirmWaybill() {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610080002");
|
||||
waybill.setVehicleNo(PLATE);
|
||||
waybill.setDriverPhone(PHONE);
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private Driver driver() {
|
||||
Driver driver = new Driver();
|
||||
driver.setId(9001L);
|
||||
driver.setUserId(USER_ID);
|
||||
driver.setDrivingVehicle(PLATE);
|
||||
driver.setMobile(PHONE);
|
||||
return driver;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 调度端运单详情的原始业务状态。
|
||||
* <p>
|
||||
* 数字态只有 0/1/2/3:待回单确认落进行中(1),与运输中同档。
|
||||
* 调度端要区分这两者只能看原始状态码,所以详情与卡片都得带上它。
|
||||
*/
|
||||
class ManageWaybillBusinessStatusTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 6201L;
|
||||
private static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IWaybillService waybillService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class);
|
||||
service = new ManageWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
exceptionDisposalService,
|
||||
mock(IRiskDisposalService.class),
|
||||
mock(IDriverWaybillService.class),
|
||||
mock(ITripQueryService.class),
|
||||
mock(ILoadingManageService.class));
|
||||
when(exceptionDisposalService.list(anyWrapper())).thenReturn(new ArrayList<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运单详情:数字态 1,原始业务状态是待回单确认")
|
||||
void detailCarriesRawBusinessStatus() {
|
||||
when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill());
|
||||
|
||||
AdminWaybillDetailVO detail = service.detail(String.valueOf(WAYBILL_ID));
|
||||
|
||||
assertEquals(Integer.valueOf(1), detail.getStatus(), "数字态仍落进行中,未新增档位");
|
||||
assertEquals(PENDING_RETURN_CONFIRM, detail.getBusinessStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待重新派单卡片:原始业务状态随卡片下发")
|
||||
void reassignCardCarriesRawBusinessStatus() {
|
||||
when(waybillService.page(any(), any())).thenAnswer(invocation -> {
|
||||
Page<Waybill> rows = invocation.getArgument(0);
|
||||
rows.setRecords(List.of(waybill()));
|
||||
rows.setTotal(1);
|
||||
return rows;
|
||||
});
|
||||
|
||||
IPage<AdminWaybillCardVO> page = service.pendingList(1, 10, null, true);
|
||||
|
||||
assertEquals(1, page.getRecords().size());
|
||||
assertEquals(Integer.valueOf(1), page.getRecords().get(0).getStatus(),
|
||||
"数字态落进行中:null 到前端会掉进「非 0/1/2 即已取消」的展示分支");
|
||||
assertEquals(PENDING_RETURN_CONFIRM, page.getRecords().get(0).getBusinessStatus());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private Waybill waybill() {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610080003");
|
||||
waybill.setBusinessStatus(PENDING_RETURN_CONFIRM);
|
||||
waybill.setDriverAcceptStatus("rejected");
|
||||
waybill.setIsDeleted(0);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> Wrapper<T> anyWrapper() {
|
||||
return org.mockito.ArgumentMatchers.<Wrapper<T>>any();
|
||||
}
|
||||
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
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.springblade.transport.mapper.MasterOrderMapper;
|
||||
import org.springblade.transport.pojo.entity.MasterOrder;
|
||||
import org.springblade.transport.pojo.entity.TransportPlan;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.MasterOrderVO;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.ITransportPlanService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 多联总单分段进度的「已到达数量」口径。
|
||||
* <p>
|
||||
* 到货但回单待确认的运单算到达:货物确实到了,回单只是单据。
|
||||
* 不算会让下一段被「调度总量不能大于上一段已到达数量」挡住,整条线路卡在第二段。
|
||||
*/
|
||||
class MasterOrderProgressTest {
|
||||
|
||||
private static final Long MASTER_ID = 6101L;
|
||||
private static final String MASTER_NO = "ZD20261008001";
|
||||
private static final String ROUTE_JSON = """
|
||||
[{"segmentNo":"1","transportType":"公路运输","departureName":"南宁","arrivalName":"北海"},
|
||||
{"segmentNo":"2","transportType":"铁路运输","departureName":"北海","arrivalName":"昆明"}]
|
||||
""";
|
||||
|
||||
private MasterOrderMapper masterOrderMapper;
|
||||
private IWaybillService waybillService;
|
||||
private ITransportPlanService transportPlanService;
|
||||
private MasterOrderServiceImpl 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, TransportPlan.class);
|
||||
TableInfoHelper.initTableInfo(assistant, MasterOrder.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
masterOrderMapper = mock(MasterOrderMapper.class);
|
||||
waybillService = mock(IWaybillService.class);
|
||||
transportPlanService = mock(ITransportPlanService.class);
|
||||
service = new MasterOrderServiceImpl(
|
||||
waybillService,
|
||||
transportPlanService,
|
||||
mock(IProjectApplyService.class),
|
||||
mock(IContractManageService.class));
|
||||
inject("baseMapper", masterOrderMapper);
|
||||
|
||||
when(transportPlanService.list(anyWrapper())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("到货但回单待确认的运单计入「已到达数量」")
|
||||
void arrivedQuantityCountsPendingReturnConfirm() {
|
||||
when(masterOrderMapper.selectById(MASTER_ID)).thenReturn(masterOrder());
|
||||
when(waybillService.list(anyWrapper())).thenReturn(List.of(
|
||||
child(1L, "1", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, "8")));
|
||||
|
||||
MasterOrderVO vo = service.detail(MASTER_ID);
|
||||
|
||||
assertEquals(0, decimalOf(vo.getRouteProgress().get(0).get("arrivedQuantity"))
|
||||
.compareTo(new BigDecimal("8")), "回单待确认不等于没到货");
|
||||
assertEquals(0, decimalOf(vo.getRouteProgress().get(1).get("arrivedQuantity")).signum(),
|
||||
"第二段还没有到货的运单");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已到达口径:已完成 + 待回单确认,进行中不算")
|
||||
void arrivedQuantityIgnoresRunning() {
|
||||
when(masterOrderMapper.selectById(MASTER_ID)).thenReturn(masterOrder());
|
||||
when(waybillService.list(anyWrapper())).thenReturn(List.of(
|
||||
child(1L, "1", "running", "8"),
|
||||
child(2L, "1", "completed", "5"),
|
||||
child(3L, "1", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, "3")));
|
||||
|
||||
MasterOrderVO vo = service.detail(MASTER_ID);
|
||||
|
||||
assertEquals(0, decimalOf(vo.getRouteProgress().get(0).get("arrivedQuantity"))
|
||||
.compareTo(new BigDecimal("8")), "5 + 3:进行中的 8 吨还没到,不能算到达");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private MasterOrder masterOrder() {
|
||||
MasterOrder masterOrder = new MasterOrder();
|
||||
masterOrder.setId(MASTER_ID);
|
||||
masterOrder.setMasterNo(MASTER_NO);
|
||||
masterOrder.setRouteJson(ROUTE_JSON);
|
||||
return masterOrder;
|
||||
}
|
||||
|
||||
private Waybill child(Long id, String segmentNo, String businessStatus, String quantity) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(id);
|
||||
waybill.setMasterNo(MASTER_NO);
|
||||
waybill.setRelationNo(segmentNo);
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setGoodsJson("[{\"cargoName\":\"钢材\",\"quantity\":" + quantity + "}]");
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private BigDecimal decimalOf(Object value) {
|
||||
return value == null ? BigDecimal.ZERO : (BigDecimal) value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> Wrapper<T> anyWrapper() {
|
||||
return org.mockito.ArgumentMatchers.<Wrapper<T>>any();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -193,6 +193,23 @@ class TripAggregationTest {
|
||||
assertTrue(TripAggregation.CHILD_STATUSES.containsAll(TripAggregation.TERMINAL_STATUSES));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认的子单仍进运次,且不会把运次判成已完成")
|
||||
void pendingReturnConfirmChildKeepsTripRunning() {
|
||||
String pendingReturnConfirm = org.springblade.transport.support.WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
|
||||
|
||||
assertTrue(TripAggregation.CHILD_STATUSES.contains(pendingReturnConfirm),
|
||||
"漏掉这个状态,子单会从运次读服务消失,配载单被当成全部完成");
|
||||
|
||||
// 与一张已完成的子单拼载:还有子单没走完,运次只能是运输中
|
||||
List<Waybill> children = List.of(
|
||||
waybill("completed", null, null),
|
||||
waybill(pendingReturnConfirm, null, null));
|
||||
String status = TripAggregation.statusOf(children, false, false);
|
||||
assertEquals("running", status);
|
||||
assertEquals(1, TripAggregation.appStatus(status), "运次不进已完成档");
|
||||
}
|
||||
|
||||
private Waybill waybill(String businessStatus, String acceptStatus, String processJson) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.transport.mapper.MasterOrderMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
|
||||
import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.ICommonAddressService;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springblade.transport.wrapper.WaybillWrapper;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* 运单业务状态的中文名,两套表并存(历史结构,不合并):运单管理一套、应收应付一套。
|
||||
* <p>
|
||||
* 两套都要认待回单确认:漏一套就在页面上露出原始英文码。
|
||||
*/
|
||||
class WaybillBusinessStatusNameTest {
|
||||
|
||||
private static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
|
||||
|
||||
@Test
|
||||
@DisplayName("运单管理名称表给出「待回单确认」——导出与详情共用这张表")
|
||||
void waybillWrapperNamesPendingReturnConfirm() {
|
||||
assertEquals("待回单确认", WaybillWrapper.businessStatusName(PENDING_RETURN_CONFIRM));
|
||||
// 相邻档位不受影响
|
||||
assertEquals("进行中", WaybillWrapper.businessStatusName("running"));
|
||||
assertEquals("已完成", WaybillWrapper.businessStatusName("completed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应收应付的第二套名称表同步给出「待回单确认」")
|
||||
void receivablePayableNamesPendingReturnConfirm() throws Exception {
|
||||
assertEquals("待回单确认", receivableStatusName(PENDING_RETURN_CONFIRM));
|
||||
}
|
||||
|
||||
/**
|
||||
* 应收应付那张表只被 waybillMap 用,而 waybillMap 当前没有调用方,
|
||||
* 没有公共接缝;它是纯函数,按既有做法直接取私有方法(同 LoadingManageChildStatusWriteTest)。
|
||||
*/
|
||||
private String receivableStatusName(String status) throws Exception {
|
||||
ReceivablePayableDetailServiceImpl service = new ReceivablePayableDetailServiceImpl(
|
||||
mock(ReceivablePayableCargoFeeMapper.class),
|
||||
mock(ReceivablePayableChangeRecordMapper.class),
|
||||
mock(MasterOrderMapper.class),
|
||||
mock(IWaybillService.class),
|
||||
mock(IContractManageService.class),
|
||||
mock(ICommonAddressService.class),
|
||||
mock(IPreSettlementService.class),
|
||||
mock(IFormalSettlementService.class));
|
||||
Method method = ReceivablePayableDetailServiceImpl.class
|
||||
.getDeclaredMethod("waybillStatusName", String.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, status);
|
||||
}
|
||||
|
||||
}
|
||||
+86
-14
@@ -23,6 +23,7 @@ import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
@@ -37,15 +38,17 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 确认回单:回单节点要求业务员确认时,内部人员确认后把运单置为已完成。
|
||||
* 确认回单:停在待回单确认的运单,内部人员确认后把运单置为已完成。
|
||||
* <p>
|
||||
* 完整入口不校验节点打卡,故这里必须自己挡住两件事——该运单的司机打卡节点(在途除外)全部已打卡、
|
||||
* 无未了结异常;不挡住就成了绕过打卡门禁与异常门禁的通道。回单不要求确认时不能走这个入口。
|
||||
* 完整入口不校验节点打卡,故这里必须自己挡住三件事——运单确实停在待回单确认、该运单的司机打卡节点
|
||||
* (在途除外)全部已打卡、无未了结异常;不挡住就成了绕过打卡门禁与异常门禁的通道。
|
||||
* 判据只看运单状态,不看回单配置当前值。
|
||||
*/
|
||||
class WaybillConfirmReturnTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 5701L;
|
||||
private static final String DEPT_ID = "1123598813738675201";
|
||||
private static final String PENDING_RETURN_CONFIRM = WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM;
|
||||
|
||||
private static final String RETURN_NEED_CONFIRM_JSON = """
|
||||
[{"key":"load","name":"装货","type":"cargo","enabled":true,"punch":true},
|
||||
@@ -118,9 +121,9 @@ class WaybillConfirmReturnTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("回单要求确认且节点都打完:置为已完成,并照常生成应收应付、尝试完成配载单")
|
||||
@DisplayName("待回单确认且节点都打完:置为已完成,并照常生成应收应付、尝试完成配载单")
|
||||
void confirmsReturnAndCompletesWaybill() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON);
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, PENDING_RETURN_CONFIRM);
|
||||
stubPunches("load", "sign");
|
||||
|
||||
boolean result = service.confirmReturnReceipt(WAYBILL_ID);
|
||||
@@ -135,16 +138,57 @@ class WaybillConfirmReturnTest {
|
||||
@Test
|
||||
@DisplayName("在途节点没打卡也放行:它不是必经一步")
|
||||
void transitNodeDoesNotBlockConfirmation() {
|
||||
stubWaybill(WITH_TRANSIT_JSON);
|
||||
stubWaybill(WITH_TRANSIT_JSON, PENDING_RETURN_CONFIRM);
|
||||
stubPunches("load", "sign");
|
||||
|
||||
assertEquals(true, service.confirmReturnReceipt(WAYBILL_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("进行中的运单不能确认回单:状态校验先于打卡校验")
|
||||
void runningWaybillCannotConfirmReturn() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, "running");
|
||||
// 故意一条打卡都不补:若状态校验排在打卡校验之后,这里报的会是缺哪个节点
|
||||
stubPunches();
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.confirmReturnReceipt(WAYBILL_ID));
|
||||
|
||||
assertEquals("当前状态不允许确认回单", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已完成的运单不能确认回单")
|
||||
void completedWaybillCannotConfirmReturn() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, "completed");
|
||||
stubPunches("load", "sign");
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.confirmReturnReceipt(WAYBILL_ID));
|
||||
|
||||
assertEquals("当前状态不允许确认回单", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待执行的运单不能确认回单")
|
||||
void pendingWaybillCannotConfirmReturn() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, "pending");
|
||||
stubPunches("load", "sign");
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.confirmReturnReceipt(WAYBILL_ID));
|
||||
|
||||
assertEquals("当前状态不允许确认回单", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("还有节点没打卡:拒绝并点名缺哪个节点,不改状态")
|
||||
void unfinishedPunchNodeBlocksConfirmation() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON);
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, PENDING_RETURN_CONFIRM);
|
||||
stubPunches("load");
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
@@ -157,7 +201,7 @@ class WaybillConfirmReturnTest {
|
||||
@Test
|
||||
@DisplayName("有未了结异常:拒绝,不改状态")
|
||||
void unresolvedExceptionBlocksConfirmation() {
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON);
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, PENDING_RETURN_CONFIRM);
|
||||
stubPunches("load", "sign");
|
||||
when(exceptionDisposalMapper.selectCount(any())).thenReturn(1L);
|
||||
|
||||
@@ -169,31 +213,59 @@ class WaybillConfirmReturnTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("回单不要求业务员确认:这个入口不适用,说清而不是静默完成")
|
||||
void returnWithoutConfirmationIsRejected() {
|
||||
stubWaybill(RETURN_NO_CONFIRM_JSON);
|
||||
@DisplayName("回单配置已改成无需确认、运单停在待回单确认:仍能确认回单")
|
||||
void confirmStillWorksAfterConfigSwitchedToNoConfirm() {
|
||||
stubWaybill(RETURN_NO_CONFIRM_JSON, PENDING_RETURN_CONFIRM);
|
||||
stubPunches("load", "sign");
|
||||
|
||||
assertEquals(true, service.confirmReturnReceipt(WAYBILL_ID));
|
||||
|
||||
verify(waybillMapper).updateById(org.mockito.ArgumentMatchers.<Waybill>argThat(waybill ->
|
||||
"completed".equals(waybill.getBusinessStatus())));
|
||||
verify(receivablePayableDetailService).generateForCompletedWaybills(List.of(WAYBILL_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("回单配置无需确认且运单还在进行中:这个入口不适用,报状态不符")
|
||||
void noConfirmConfigRunningWaybillCannotConfirmReturn() {
|
||||
stubWaybill(RETURN_NO_CONFIRM_JSON, "running");
|
||||
stubPunches("load", "sign");
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.confirmReturnReceipt(WAYBILL_ID));
|
||||
|
||||
assertEquals("该运单的回单不需要业务员确认", exception.getMessage());
|
||||
assertEquals("当前状态不允许确认回单", exception.getMessage());
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认可以取消:置为已取消,不生成应收应付")
|
||||
void cancelPendingReturnConfirmWaybill() {
|
||||
// 取消接口不允许已配载的运单
|
||||
stubWaybill(RETURN_NEED_CONFIRM_JSON, PENDING_RETURN_CONFIRM).setLoadingNo(null);
|
||||
|
||||
assertEquals(true, service.cancel(WAYBILL_ID));
|
||||
|
||||
verify(waybillMapper).updateById(org.mockito.ArgumentMatchers.<Waybill>argThat(waybill ->
|
||||
"cancelled".equals(waybill.getBusinessStatus()) && WAYBILL_ID.equals(waybill.getId())));
|
||||
verify(receivablePayableDetailService, never()).generateForCompletedWaybills(any());
|
||||
verify(loadingManageService, never()).completeIfAllWaybillsCompleted(any());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private void stubWaybill(String processJson) {
|
||||
private Waybill stubWaybill(String processJson, String businessStatus) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setWaybillNo("YD202610180002");
|
||||
waybill.setDeptId(Long.valueOf(DEPT_ID));
|
||||
waybill.setProjectId(7001L);
|
||||
waybill.setProcessJson(processJson);
|
||||
waybill.setBusinessStatus("running");
|
||||
waybill.setBusinessStatus(businessStatus);
|
||||
waybill.setLoadingNo("PZ-20261018-001");
|
||||
waybill.setIsDeleted(0);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private void stubPunches(String... nodeCodes) {
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
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.MockedStatic;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 运单编辑保存的业务状态来源测试。
|
||||
* <p>
|
||||
* 编辑保存只改运单资料,business_status 一律以库里的值为准:请求体不回传(null / 空串)
|
||||
* 不得把库里的待执行、进行中、已完成改写成别的状态,回传陈旧值也不得覆盖。
|
||||
* 唯一例外是草稿——submit 即正式提交,草稿须按过程配置提升为待执行 / 进行中。
|
||||
* <p>
|
||||
* 过程配置用「无需确认接单」的快照({@code []}):无过程校正时待执行会被提升为进行中,
|
||||
* 正是编辑保存最容易把库里状态写坏的场景。
|
||||
*/
|
||||
class WaybillEditSaveBusinessStatusTest {
|
||||
|
||||
private static final Long WAYBILL_ID = 7001L;
|
||||
private static final Long DEPT_ID = 100L;
|
||||
/** 无接单节点的过程配置:normalizeBusinessStatus 会把空 / pending 提升为 running */
|
||||
private static final String PROCESS_JSON_NO_ACCEPT = "[]";
|
||||
|
||||
private WaybillMapper waybillMapper;
|
||||
private WaybillServiceImpl service;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
// saveOrUpdate 判主键需要 MyBatis-Plus 的实体元数据缓存
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
waybillMapper = mock(WaybillMapper.class);
|
||||
service = new WaybillServiceImpl();
|
||||
inject("baseMapper", waybillMapper);
|
||||
when(waybillMapper.updateById(any(Waybill.class))).thenReturn(1);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 编辑保存:以库里的状态为准
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:请求体不回传业务状态,库里的待执行不得被提升为进行中")
|
||||
void editSaveKeepsPendingWhenRequestBodyOmitsStatus() {
|
||||
Waybill request = editableRequest();
|
||||
|
||||
submit(request, "pending");
|
||||
|
||||
assertEquals("pending", request.getBusinessStatus(),
|
||||
"编辑保存时请求体没有 business_status,不得按过程配置把库里的待执行改写成进行中");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:请求体不回传业务状态,库里的进行中不得被改写")
|
||||
void editSaveKeepsRunningWhenRequestBodyOmitsStatus() {
|
||||
Waybill request = editableRequest();
|
||||
|
||||
submit(request, "running");
|
||||
|
||||
assertEquals("running", request.getBusinessStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:请求体不回传业务状态,库里的已完成不得被改写")
|
||||
void editSaveKeepsCompletedWhenRequestBodyOmitsStatus() {
|
||||
Waybill request = editableRequest();
|
||||
|
||||
submit(request, "completed");
|
||||
|
||||
assertEquals("completed", request.getBusinessStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:请求体回传陈旧状态,仍以库里的值为准")
|
||||
void editSaveKeepsDatabaseStatusWhenRequestBodyCarriesStaleValue() {
|
||||
Waybill request = editableRequest();
|
||||
request.setBusinessStatus("running");
|
||||
|
||||
submit(request, "completed");
|
||||
|
||||
assertEquals("completed", request.getBusinessStatus(),
|
||||
"库里已完成,请求体带的是打开表单时的旧值,不得据此回退状态");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:请求体回传空串,库里的状态不得被清空")
|
||||
void editSaveKeepsDatabaseStatusWhenRequestBodyIsBlank() {
|
||||
Waybill request = editableRequest();
|
||||
request.setBusinessStatus("");
|
||||
|
||||
submit(request, "running");
|
||||
|
||||
assertEquals("running", request.getBusinessStatus());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 新增状态
|
||||
|
||||
@Test
|
||||
@DisplayName("编辑保存:待回单确认的运单不得被编辑保存改成进行中")
|
||||
void editSaveKeepsPendingReturnConfirm() {
|
||||
Waybill request = editableRequest();
|
||||
|
||||
submit(request, WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
|
||||
assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, request.getBusinessStatus(),
|
||||
"司机已打完末位卡,编辑一次运单资料不得把它退回进行中");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 草稿例外
|
||||
|
||||
@Test
|
||||
@DisplayName("提交草稿:草稿仍按过程配置提升为进行中")
|
||||
void submitDraftStillPromotesToRunning() {
|
||||
Waybill request = editableRequest();
|
||||
|
||||
submit(request, "draft");
|
||||
|
||||
assertEquals("running", request.getBusinessStatus(),
|
||||
"submit 即正式提交,草稿必须脱离 draft,不能被「以库里的值为准」锁死");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private Waybill editableRequest() {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setId(WAYBILL_ID);
|
||||
waybill.setProjectName("项目A");
|
||||
waybill.setContractName("客户合同A");
|
||||
waybill.setCustomerName("客户A");
|
||||
waybill.setTransportType("公路运输");
|
||||
waybill.setCargoName("钢材");
|
||||
waybill.setCargoType("普货");
|
||||
waybill.setQuantityUnit("吨");
|
||||
waybill.setCarrierType("自运");
|
||||
waybill.setVehicleNo("桂A12345");
|
||||
waybill.setDriverName("张三");
|
||||
waybill.setCarrierJson("{}");
|
||||
waybill.setQuantity(BigDecimal.TEN);
|
||||
waybill.setProcessJson(PROCESS_JSON_NO_ACCEPT);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 让库里的同一条运单处于 {@code databaseStatus},再走一次编辑保存(submit)。
|
||||
* 司机与车牌保持一致,避免接单记录被判定为「换了司机」而清空。
|
||||
*/
|
||||
private void submit(Waybill request, String databaseStatus) {
|
||||
Waybill oldRecord = editableRequest();
|
||||
oldRecord.setBusinessStatus(databaseStatus);
|
||||
oldRecord.setDeptId(DEPT_ID);
|
||||
oldRecord.setDeptName("测试组织");
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(oldRecord);
|
||||
|
||||
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class)) {
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(String.valueOf(DEPT_ID));
|
||||
assertTrue(service.submit(request));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -14,6 +14,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
@@ -126,6 +127,19 @@ class WaybillPunchRecordListTest {
|
||||
assertEquals("", accept.getPunchTime(), "没有接单动作就没有完成时刻,不得拿别的字段编一个");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认的无需确认接单运单:接单节点已打卡,但没有完成时刻")
|
||||
void acceptNodePunchedWhenPendingReturnConfirm() {
|
||||
stubWaybill(waybill(NO_CONFIRM_ACCEPT_JSON,
|
||||
WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, null));
|
||||
|
||||
WaybillPunchRecordItemVO accept = acceptRecord();
|
||||
|
||||
assertTrue(accept.getPunched());
|
||||
assertEquals("已打卡", accept.getStatusName());
|
||||
assertEquals("", accept.getPunchTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无需确认接单但仍是草稿:接单节点未打卡")
|
||||
void acceptNodeUnpunchedWhenDraft() {
|
||||
|
||||
+111
@@ -18,6 +18,7 @@ import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
@@ -67,6 +68,7 @@ class WaybillPunchRecordSaveTest {
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, WaybillNodePunch.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WaybillEnroutePunch.class);
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -78,6 +80,7 @@ class WaybillPunchRecordSaveTest {
|
||||
inject("baseMapper", waybillMapper);
|
||||
inject("waybillNodePunchMapper", nodePunchMapper);
|
||||
inject("waybillEnroutePunchMapper", enroutePunchMapper);
|
||||
when(waybillMapper.updateById(any(Waybill.class))).thenReturn(1);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 过程节点打卡
|
||||
@@ -165,6 +168,114 @@ class WaybillPunchRecordSaveTest {
|
||||
verify(nodePunchMapper, never()).updateById(any(WaybillNodePunch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认:补录打卡被拒——司机已无待打卡,漏打应先取消打卡退回进行中")
|
||||
void rejectBackfillOnPendingReturnConfirm() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("node", "load", "装货");
|
||||
request.setPunched(true);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.savePunchRecord(request));
|
||||
|
||||
assertEquals("运单待业务员确认回单,不能补录打卡", exception.getMessage());
|
||||
verify(nodePunchMapper, never()).insert(any(WaybillNodePunch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认:取消打卡仍放行——那是退回进行中的唯一路径")
|
||||
void keepCancelPunchOnPendingReturnConfirm() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of(nodePunch(7001L, "load", "装货")));
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("node", "load", "装货");
|
||||
request.setPunched(false);
|
||||
|
||||
assertTrue(service.savePunchRecord(request));
|
||||
|
||||
ArgumentCaptor<Collection<Long>> captor = ArgumentCaptor.forClass(Collection.class);
|
||||
verify(nodePunchMapper).deleteBatchIds(captor.capture());
|
||||
assertEquals(Set.of(7001L), new HashSet<>(captor.getValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认:取消打卡后退回进行中——进入该状态的前提已被撤掉")
|
||||
void revertToRunningWhenCancelPunchOnPendingReturnConfirm() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of(nodePunch(7001L, "load", "装货")));
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("node", "load", "装货");
|
||||
request.setPunched(false);
|
||||
|
||||
assertTrue(service.savePunchRecord(request));
|
||||
|
||||
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
|
||||
verify(waybillMapper).updateById(captor.capture());
|
||||
assertEquals(WaybillProcessSupport.STATUS_RUNNING, captor.getValue().getBusinessStatus());
|
||||
assertEquals(WAYBILL_ID, captor.getValue().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("进行中:取消打卡不动运单状态")
|
||||
void keepStatusWhenCancelPunchOnRunningWaybill() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_RUNNING);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of(nodePunch(7001L, "load", "装货")));
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("node", "load", "装货");
|
||||
request.setPunched(false);
|
||||
|
||||
assertTrue(service.savePunchRecord(request));
|
||||
|
||||
verify(waybillMapper, never()).updateById(any(Waybill.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认:取消在途打卡同样退回进行中")
|
||||
void revertToRunningWhenCancelEnroutePunchOnPendingReturnConfirm() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM);
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
WaybillEnroutePunch existed = new WaybillEnroutePunch();
|
||||
existed.setId(8001L);
|
||||
when(enroutePunchMapper.selectList(any())).thenReturn(List.of(existed));
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("enroute", "transit", "在途");
|
||||
request.setPunched(false);
|
||||
|
||||
assertTrue(service.savePunchRecord(request));
|
||||
|
||||
ArgumentCaptor<Waybill> captor = ArgumentCaptor.forClass(Waybill.class);
|
||||
verify(waybillMapper).updateById(captor.capture());
|
||||
assertEquals(WaybillProcessSupport.STATUS_RUNNING, captor.getValue().getBusinessStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已完成运单仍可补录——门禁只对待回单确认生效")
|
||||
void keepBackfillOnCompletedWaybill() {
|
||||
Waybill waybill = waybill();
|
||||
waybill.setBusinessStatus("completed");
|
||||
when(waybillMapper.selectById(WAYBILL_ID)).thenReturn(waybill);
|
||||
when(nodePunchMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
WaybillPunchRecordSaveRequest request = request("node", "load", "装货");
|
||||
request.setPunched(true);
|
||||
request.setPunchTime("2026-10-06 09:30:00");
|
||||
|
||||
assertTrue(service.savePunchRecord(request));
|
||||
|
||||
verify(nodePunchMapper).insert(any(WaybillNodePunch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("接单节点:补录与取消打卡都拒绝——它的完成由运单的接单事实决定")
|
||||
void rejectAcceptNodePunchEdit() {
|
||||
|
||||
+39
-4
@@ -334,15 +334,31 @@ class WaybillProcessSupportTest {
|
||||
"TRANSPORT_JSON 的回单启用且没有 confirmMode:缺字段当没配「无需确认完成」,否则会跳过本该人工确认的回单");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 待回单确认
|
||||
|
||||
@Test
|
||||
@DisplayName("待回单确认落库字面量为 pending_return_confirm,且接单归一不改写它")
|
||||
void pendingReturnConfirmLiteralSurvivesAcceptanceNormalization() {
|
||||
assertEquals("pending_return_confirm", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
|
||||
"库里存的是这个字面量,web 与小程序也按它判断,不能改名");
|
||||
assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
|
||||
WaybillProcessSupport.normalizeBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, TRANSPORT_JSON),
|
||||
"TRANSPORT_JSON 要求确认接单,未接单时归一结果本是 pending;运单已跑完全程,不能被改回待执行");
|
||||
assertEquals(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM,
|
||||
WaybillProcessSupport.normalizeBusinessStatus(WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, TRANSPORT_JSON, "accepted"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 结束门禁
|
||||
|
||||
@Test
|
||||
@DisplayName("结束门禁:只认已完成与已取消,大小写不敏感")
|
||||
void punchEndedOnlyForCompletedAndCancelled() {
|
||||
@DisplayName("结束门禁:已完成、已取消与待回单确认都算结束,大小写不敏感")
|
||||
void punchEndedForCompletedCancelledAndPendingReturnConfirm() {
|
||||
assertTrue(WaybillProcessSupport.isPunchEnded("completed"));
|
||||
assertTrue(WaybillProcessSupport.isPunchEnded("COMPLETED"));
|
||||
assertTrue(WaybillProcessSupport.isPunchEnded("cancelled"));
|
||||
assertTrue(WaybillProcessSupport.isPunchEnded("Cancelled"));
|
||||
assertTrue(WaybillProcessSupport.isPunchEnded("pending_return_confirm"),
|
||||
"司机已打完末位卡,不能再打卡");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -358,11 +374,12 @@ class WaybillProcessSupportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("结束门禁文案:完成与取消各一句,非结束态没有文案")
|
||||
@DisplayName("结束门禁文案:完成、取消与待回单确认各一句,非结束态没有文案")
|
||||
void punchEndedReasonOnlyForEndedStatuses() {
|
||||
assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason("completed"));
|
||||
assertEquals("运单已完成", WaybillProcessSupport.punchEndedReason(" COMPLETED "));
|
||||
assertEquals("运单已取消", WaybillProcessSupport.punchEndedReason("cancelled"));
|
||||
assertEquals("运单待业务员确认回单", WaybillProcessSupport.punchEndedReason("pending_return_confirm"));
|
||||
assertNull(WaybillProcessSupport.punchEndedReason(null));
|
||||
assertNull(WaybillProcessSupport.punchEndedReason(""));
|
||||
assertNull(WaybillProcessSupport.punchEndedReason(" "));
|
||||
@@ -421,6 +438,22 @@ class WaybillProcessSupportTest {
|
||||
// 结束态由结束文案负责,两句话不能抢
|
||||
assertNull(WaybillProcessSupport.punchNotStartedReason("completed"));
|
||||
assertNull(WaybillProcessSupport.punchNotStartedReason("cancelled"));
|
||||
assertNull(WaybillProcessSupport.punchNotStartedReason("pending_return_confirm"),
|
||||
"司机已打完末位卡,不能再被说成「尚未开始运输」");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("在途卡:待回单确认的运单与已完成同样不可打卡")
|
||||
void transitCardOnPendingReturnConfirmWaybillIsVisibleButNotPunchable() {
|
||||
LocalDateTime now = LocalDateTime.of(2026, 10, 20, 12, 0);
|
||||
|
||||
WaybillProcessSupport.TransitCheckinDecision decision =
|
||||
WaybillProcessSupport.evaluateTransitCheckin(TRANSPORT_JSON, "pending_return_confirm", null, now);
|
||||
|
||||
assertTrue(decision.visible(), "卡片仍要下发,司机能回看");
|
||||
assertFalse(decision.punchEnabled(), "末位卡已打完,在途卡不能再提交");
|
||||
assertFalse(decision.doneToday());
|
||||
assertFalse(decision.dueToday());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -455,9 +488,11 @@ class WaybillProcessSupportTest {
|
||||
assertNull(WaybillProcessSupport.reportBlockedReason("running"));
|
||||
assertEquals("运单尚未开始运输,暂不可上报异常", WaybillProcessSupport.reportBlockedReason("pending"));
|
||||
assertEquals("运单尚未开始运输,暂不可上报异常", WaybillProcessSupport.reportBlockedReason("dispatching"));
|
||||
// 结束优先:已完成/已取消先说结束,不提「等你上路」
|
||||
// 结束优先:已结束态先说结束,不提「等你上路」
|
||||
assertEquals("运单已完成,不能上报异常", WaybillProcessSupport.reportBlockedReason("completed"));
|
||||
assertEquals("运单已取消,不能上报异常", WaybillProcessSupport.reportBlockedReason("cancelled"));
|
||||
assertEquals("运单待业务员确认回单,不能上报异常",
|
||||
WaybillProcessSupport.reportBlockedReason("pending_return_confirm"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user