fix(dispatch): expose PDA departure gate

This commit is contained in:
2026-09-02 14:00:54 +08:00
parent 25ffd62e14
commit afb38f3c3f
5 changed files with 58 additions and 2 deletions
@@ -14,6 +14,8 @@ public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
OutboundOrder findGlobalByNo(@Param("outboundNo") String outboundNo); OutboundOrder findGlobalByNo(@Param("outboundNo") String outboundNo);
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key); OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
OutboundOrder findByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
OutboundOrder lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId); OutboundOrder lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1 FOR UPDATE") @Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1 FOR UPDATE")
@@ -5,7 +5,7 @@ import java.util.List;
public final class DispatchPdaResponses { public final class DispatchPdaResponses {
private DispatchPdaResponses() {} private DispatchPdaResponses() {}
public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status, public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status,
List<Item> items) {} boolean canDepart, String departBlockReason, List<Item> items) {}
public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode, public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel, String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
int scheduledPackages, int scheduledPackages,
@@ -242,7 +242,23 @@ public class DispatchPdaWorkflowService {
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()), new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()),
n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList())); n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList()));
} }
return new DispatchPdaResponses.Task(task.getId(), task.getTaskNo(), order.getDispatchNo(), order.getVehiclePlate(), task.getWarehouseId(), task.getStatus(), values); OutboundOrder outbound = outboundOrders.findByDispatch(customerId, order.getId());
String departBlockReason = departBlockReason(order, task, source, outbound);
return new DispatchPdaResponses.Task(task.getId(), task.getTaskNo(), order.getDispatchNo(), order.getVehiclePlate(),
task.getWarehouseId(), task.getStatus(), departBlockReason == null, departBlockReason, values);
}
private String departBlockReason(DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source,
OutboundOrder outbound) {
if ("DEPARTED".equals(order.getStatus())) return "该排车已发车,不可重复提交。";
if (trim(order.getVehiclePlate()) == null) return "请先在平台维护车辆信息。";
if (!"COMPLETED".equals(task.getStatus()) || !"READY_TO_DEPART".equals(order.getStatus())
|| !allPicked(order.getCustomerId(), task.getId(), source)) {
return "请先完成排车拣货并核平全部箱数。";
}
if (outbound == null) return "请先在平台完成“转派送”,生成待出库单后才能发车。";
if (!"WAIT_OUTBOUND".equals(outbound.getStatus())) return "关联出库单不是待出库状态,暂不可发车。";
return null;
} }
private boolean allPicked(String customerId, Long taskId, List<DispatchItem> source) { private boolean allPicked(String customerId, Long taskId, List<DispatchItem> source) {
@@ -15,6 +15,7 @@ paths:
/api/pda/v1/last-leg/dispatches/{dispatchNo}: /api/pda/v1/last-leg/dispatches/{dispatchNo}:
get: get:
summary: 查询绑定仓排车任务详情 summary: 查询绑定仓排车任务详情
description: 返回 canDepart 与 departBlockReason;PDA 必须以服务端门禁决定是否开放发车,不得仅按本地拣货数量推断。
parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }] parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }]
responses: { '200': { description: 排车托盘、目的仓与库存位置详情 }, '404': { description: 当前绑定仓无此排车任务 } } responses: { '200': { description: 排车托盘、目的仓与库存位置详情 }, '404': { description: 当前绑定仓无此排车任务 } }
/api/pda/v1/last-leg/dispatches/{dispatchNo}/items/{itemId}/pick: /api/pda/v1/last-leg/dispatches/{dispatchNo}/items/{itemId}/pick:
@@ -107,6 +107,43 @@ class DispatchPdaWorkflowServiceTest {
verify(tasks, never()).findByDispatch(any(), any()); verify(tasks, never()).findByDispatch(any(), any());
} }
@Test
void completedTaskExposesTransferDeliveryGateBeforeDeparture() {
DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART");
order.setVehiclePlate("CA-100");
DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED");
DispatchItem item = item(21L, "100", 6);
when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order);
when(tasks.findByDispatch("100", 1L)).thenReturn(task);
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item));
when(picks.totalItem("100", 11L, 21L)).thenReturn(6L);
DispatchPdaResponses.Task result = service.task("WH-1", "DSP-1");
assertFalse(result.canDepart());
assertEquals("请先在平台完成“转派送”,生成待出库单后才能发车。", result.departBlockReason());
}
@Test
void completedTaskExposesDepartureWhenPendingOutboundExists() {
DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART");
order.setVehiclePlate("CA-100");
DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED");
DispatchItem item = item(21L, "100", 6);
OutboundOrder outbound = new OutboundOrder();
outbound.setStatus("WAIT_OUTBOUND");
when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order);
when(tasks.findByDispatch("100", 1L)).thenReturn(task);
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item));
when(picks.totalItem("100", 11L, 21L)).thenReturn(6L);
when(outboundOrders.findByDispatch("100", 1L)).thenReturn(outbound);
DispatchPdaResponses.Task result = service.task("WH-1", "DSP-1");
assertTrue(result.canDepart());
assertNull(result.departBlockReason());
}
@Test @Test
void exactPickRetryReturnsPersistedResultAfterTaskAutomaticallyCompleted() throws Exception { void exactPickRetryReturnsPersistedResultAfterTaskAutomaticallyCompleted() throws Exception {
DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART");