From 3b97eedc3d979e9ada251e3019304b0c6f29487d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 13:54:06 +0800 Subject: [PATCH] fix: block dropship work until pallets are ready --- firstleg/AGENTS.md | 2 +- .../firstleg/dto/DropshipPalletizationDtos.java | 4 +++- .../mapper/DropshipPalletizationMapper.java | 6 ++++-- .../service/DropshipPalletizationService.java | 15 +++++++++++++-- .../openapi/dropship-palletization-v1.yaml | 16 ++++++++++++++++ .../DropshipPalletizationServiceTest.java | 16 ++++++++++++---- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index e3a0f9bc4..d40a3a231 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -20,7 +20,7 @@ | Order 申报同步与渠道变更履约门禁 | `controller/InternalInboundOrderController.java` 的 `/order-channel-changes/validate`、`/order-declaration-updated`,`service/OrderDeclarationSyncService.java`、`mapper/OrderDeclarationSyncMapper.java`、`dto/OrderDeclarationSync.java`;契约 `src/main/resources/openapi/firstleg-v1.yaml`,只接受内部服务令牌 | | 平台测量/OCR 与箱规计费快照 | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`、`service/CustomerDimensionWeightingService.java`;手工单箱测量保存实际尺寸、可选外箱修正、`chargeRuleId` 与 `coefficientMode`,计算结果以服务端返回值为准 | | 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。`fl_inbound_carton_label.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛,打印面显示 SKU;`box_code` 与二维码内容仍只携带稳定箱码。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` | -| 一件代发测量后 PDA 合托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量通过进入 `WAIT_PALLETIZE` 并下发独立任务,不创建国内库存,封托生成 `DP` 托盘号,全部箱数完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` | +| 一件代发测量后 PDA 清点组托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量审核通过进入 `WAIT_PALLETIZE`,按预计托数预生成可打印 `DP` 托盘码并下发独立任务,不创建国内库存;PDA 逐箱清点,支持先扫箱或先扫托、归托和逐托封托,预计托数与已生成托盘数不一致时禁止作业,全部箱和全部预生成托盘完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` | | 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` | | 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java` 的 `ensureForConfirmedWeight/ensureForSubmission`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` | | 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` | diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/DropshipPalletizationDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/DropshipPalletizationDtos.java index 75027404b..6d204acfe 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/DropshipPalletizationDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/DropshipPalletizationDtos.java @@ -46,7 +46,9 @@ public final class DropshipPalletizationDtos { public record Task(Long taskId, Long inboundId, String inboundCode, Long tenantId, String customerName, Long warehouseId, String warehouseName, Integer totalCartons, Integer countedCartons, Integer boundCartons, - Integer unboundCartons, Integer discrepancyCartons, String taskStatus, + Integer unboundCartons, Integer discrepancyCartons, + Integer expectedPalletCount, Integer generatedPalletCount, + Boolean palletSetupReady, String palletSetupMessage, String taskStatus, Long assigneeId, String assigneeName, Integer version, List pallets, List unbound) {} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java index 9abb08f91..affe179ed 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java @@ -33,7 +33,8 @@ public interface DropshipPalletizationMapper { @Param("keyword") String keyword); @Select(""" - SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type + SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type, + o.pallet_count expected_pallet_count FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}""") TaskRow task(@Param("taskId") long taskId,@Param("warehouseId") long warehouseId); @@ -56,6 +57,7 @@ public interface DropshipPalletizationMapper { @Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName); @Select("SELECT * FROM fl_dropship_pallet WHERE pallet_no=#{palletNo}") PalletRow palletByNo(@Param("palletNo") String palletNo); + @Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId}") int palletCount(@Param("taskId") long taskId); @Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}") PalletRow palletBySealOperation(@Param("operationId") String operationId); @Select(""" SELECT p.* FROM fl_dropship_pallet p @@ -197,7 +199,7 @@ public interface DropshipPalletizationMapper { ORDER BY c.inbound_detail_id """) List sealedPalletLines(@Param("palletId") long palletId); - @Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,version; } + @Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,expectedPalletCount,version; } @Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; } @Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; } @Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java index 924cbc16c..5d3c63759 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java @@ -40,6 +40,7 @@ public class DropshipPalletizationService { @Transactional public Task claim(long taskId,long warehouseId,long operatorId,String operatorName){ + requirePalletSetup(requireTask(taskId,warehouseId)); if(mapper.claim(taskId,warehouseId,operatorId,operatorName)!=1) throw conflict("任务已被其他人员领取或状态已变化"); return task(taskId,warehouseId,operatorId); } @@ -60,6 +61,7 @@ public class DropshipPalletizationService { if(!"CLAIMED".equals(task.getTaskStatus())) block="请先领取任务"; else if(!Objects.equals(task.getAssigneeId(),operatorId)) block="任务不属于当前操作员"; else if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) block="入仓单已不在待清点组托状态"; + else if(!palletSetupReady(task)) block=palletSetupMessage(task); return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),label.getInboundDetailId(), label.getSkuCode(),label.getCargoName(),label.getUnitsPerCarton(),null,null,null,"UNCOUNTED",null,null, block==null,block); @@ -167,9 +169,14 @@ public class DropshipPalletizationService { } private Task view(TaskRow row){ + List pallets=mapper.pallets(row.getId()).stream().map(p->palletView(p,row.getInboundCode())).toList(); + int expected=row.getExpectedPalletCount()==null?0:row.getExpectedPalletCount(); + int generated=pallets.size(); + boolean palletSetupReady=expected>0 && generated==expected; return new Task(row.getId(),row.getInboundId(),row.getInboundCode(),row.getTenantId(),row.getCustomerName(),row.getWarehouseId(),row.getWarehouseName(), row.getTotalCartons(),row.getCountedCartons(),row.getAllocatedCartons(),row.getCountedCartons()-row.getAllocatedCartons(),row.getDiscrepancyCartons(), - row.getTaskStatus(),row.getAssigneeId(),row.getAssigneeName(),row.getVersion(),mapper.pallets(row.getId()).stream().map(p->palletView(p,row.getInboundCode())).toList(), + expected,generated,palletSetupReady,palletSetupReady?null:palletSetupMessage(expected,generated), + row.getTaskStatus(),row.getAssigneeId(),row.getAssigneeName(),row.getVersion(),pallets, mapper.unboundCartons(row.getId()).stream().map(c->cartonView(c,row.getInboundCode(),false,null)).toList()); } @@ -193,7 +200,11 @@ public class DropshipPalletizationService { } private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(!StringUtils.hasText(label.getSkuCode())) throw conflict("箱码缺少SKU快照,不能清点"); requireExpectedQuantity(label); return label; } private int requireExpectedQuantity(LabelRow label){ if(label.getUnitsPerCarton()==null||label.getUnitsPerCarton()<1) throw conflict("箱码缺少每箱SKU数量快照,不能清点"); return label.getUnitsPerCarton(); } - private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); return task; } + private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); requirePalletSetup(task); return task; } + private void requirePalletSetup(TaskRow task){ if(!palletSetupReady(task)) throw conflict(palletSetupMessage(task)); } + private boolean palletSetupReady(TaskRow task){ return task.getExpectedPalletCount()!=null && task.getExpectedPalletCount()>0 && mapper.palletCount(task.getId())==task.getExpectedPalletCount(); } + private String palletSetupMessage(TaskRow task){ int expected=task.getExpectedPalletCount()==null?0:task.getExpectedPalletCount(); return palletSetupMessage(expected,mapper.palletCount(task.getId())); } + private static String palletSetupMessage(int expected,int generated){ return "预计 "+expected+" 托,当前仅生成 "+generated+" 个托盘码,请联系后台处理后刷新"; } private TaskRow requireTask(long taskId,long warehouseId){ TaskRow task=mapper.task(taskId,warehouseId); if(task==null) throw new BizException(ResultCode.NOT_FOUND,"未找到当前仓库的一件代发清点组托任务"); return task; } private PalletRow requireOpenPallet(long taskId,long palletId,long warehouseId){ PalletRow p=mapper.pallet(taskId,palletId,warehouseId); if(p==null) throw new BizException(ResultCode.NOT_FOUND,"托盘不存在"); if(!"OPEN".equals(p.getPalletStatus())) throw conflict("托盘已封托,不能修改"); return p; } private static String operation(String v){String n=normalize(v);if(!StringUtils.hasText(n))throw invalid("操作幂等号不能为空");return n;} diff --git a/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml b/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml index c5b6880e9..c94c5d668 100644 --- a/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml +++ b/firstleg/src/main/resources/openapi/dropship-palletization-v1.yaml @@ -91,6 +91,22 @@ components: TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64}} WarehouseId: {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64}} schemas: + Task: + type: object + description: 一件代发清点组托任务;palletSetupReady=false 时禁止领取、解析箱码和提交清点。 + required: [taskId, totalCartons, countedCartons, boundCartons, unboundCartons, expectedPalletCount, generatedPalletCount, palletSetupReady, taskStatus, pallets] + properties: + taskId: {type: integer, format: int64} + totalCartons: {type: integer, minimum: 1} + countedCartons: {type: integer, minimum: 0} + boundCartons: {type: integer, minimum: 0} + unboundCartons: {type: integer, minimum: 0} + expectedPalletCount: {type: integer, minimum: 1} + generatedPalletCount: {type: integer, minimum: 0} + palletSetupReady: {type: boolean} + palletSetupMessage: {type: string, nullable: true} + taskStatus: {type: string, enum: [PENDING, CLAIMED, COMPLETED]} + pallets: {type: array, items: {$ref: '#/components/schemas/Pallet'}} Carton: type: object properties: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java index 010bb6496..bd2c3ff8e 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java @@ -36,13 +36,13 @@ class DropshipPalletizationServiceTest { } @Test void resolvesUncountedCartonBeforeQuantityConfirmation(){ - when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.label(10L,"BOX-1")).thenReturn(label()); + when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); var result=service.resolveCarton(10L,100L,"BOX-1",7L); assertThat(result.inboundCode()).isEqualTo("IN-1"); assertThat(result.expectedSkuQuantity()).isEqualTo(12); assertThat(result.canCount()).isTrue(); } @Test void cartonCanBindDirectlyToAPreGeneratedPallet(){ - when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(pallet()); + when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(pallet()); when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton()); when(mapper.pallet(10L,20L,100L)).thenReturn(pallet()); when(mapper.cartons(20L)).thenReturn(java.util.List.of()); service.countCarton(10L,new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260911-0001","op-1"),7L,"张三"); @@ -50,14 +50,22 @@ class DropshipPalletizationServiceTest { } @Test void unknownPalletCodeCannotCreateAnAdHocPallet(){ - when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.label(10L,"BOX-1")).thenReturn(label()); + when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); var request=new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260911-9999","op-1"); assertThatThrownBy(()->service.countCarton(10L,request,7L,"张三")).isInstanceOf(BizException.class).hasMessageContaining("测量审核后生成"); verify(mapper,never()).insertPallet(anyLong(),anyLong(),anyLong(),anyLong(),anyLong(),anyString(),anyString(),anyLong(),anyString()); } private static InboundOrderEntity dropshipInbound(){InboundOrderEntity i=new InboundOrderEntity();i.setId(30L);i.setTenantId(1L);i.setWarehouseId(100L);i.setFulfillmentType("ONE_CLICK_DROPSHIP");i.setTotalPackageCount(8);i.setPalletCount(2);return i;} - private static TaskRow task(){TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setVersion(1);return t;} + @Test void incompletePreGeneratedPalletsBlockCounting(){ + when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(0); when(mapper.label(10L,"BOX-1")).thenReturn(label()); + var result=service.resolveCarton(10L,100L,"BOX-1",7L); + assertThat(result.canCount()).isFalse(); assertThat(result.blockReason()).contains("预计 2 托").contains("当前仅生成 0 个托盘码"); + assertThatThrownBy(()->service.countCarton(10L,new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,null,"op-1"),7L,"张三")) + .isInstanceOf(BizException.class).hasMessageContaining("预计 2 托"); + } + + private static TaskRow task(){TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setExpectedPalletCount(2);t.setVersion(1);return t;} private static LabelRow label(){LabelRow l=new LabelRow();l.setCartonLabelId(50L);l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setUnitsPerCarton(12);l.setInboundCode("IN-1");l.setBoxCode("BOX-1");return l;} private static PalletRow pallet(){PalletRow p=new PalletRow();p.setId(20L);p.setTaskId(10L);p.setTenantId(1L);p.setInboundId(30L);p.setWarehouseId(100L);p.setPalletNo("DP-260911-0001");p.setPalletStatus("OPEN");p.setVersion(1);return p;} private static CartonRow carton(){CartonRow c=new CartonRow();c.setId(60L);c.setTaskId(10L);c.setPalletId(20L);c.setInboundId(30L);c.setInboundDetailId(40L);c.setPalletNo("DP-260911-0001");c.setSkuCode("SKU-1");c.setCargoName("货物");c.setBoxCode("BOX-1");c.setExpectedSkuQuantity(12);c.setActualSkuQuantity(12);c.setDiscrepancyStatus("MATCH");return c;}