diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index 32956daa5..b9d5265f7 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -65,7 +65,7 @@ - 价格簿查询消费者依赖 `PriceBookQueryUseCase` 或稳定查询端口,不得反向依赖持久化 Mapper;移动旧查询类时同步更新生产调用、测试、Mock 与 import,避免同一职责在根 `repository/mapper` 和 `pricebookquery/` 双写。 - 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。 - 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。 -- 一件代发以 `fl_inbound_carton_label_sku` 作为物理箱内不可变 SKU 快照;一个稳定 `box_code` 可以对应多条 SKU,PDA 扫箱后必须一次清点全部 SKU,不得将 SKU 塞入二维码或以 SKU 替代箱码。实际打托后的查询必须能按“现场托盘 -> 客户 -> SKU -> 箱数”聚合;禁止按货名、箱规或箱号猜测 SKU。修改本链路时同步检查 `OrderCargoClient`、Order 内部货物接口及 PDA 打托实现。 +- 一件代发以 `fl_inbound_carton_label_sku` 作为物理箱内不可变 SKU 快照;一个稳定 `box_code` 可以对应多条 SKU,PDA 扫箱后必须一次清点全部 SKU,不得将 SKU 塞入二维码或以 SKU 替代箱码。实际打托后的查询必须能按“现场托盘 -> 客户 -> SKU -> 箱数”聚合;同一国内作业仓的多票允许共用一个开放托盘,单票全部归托不自动封托,只有参与该票装箱的开放托盘阻止该票完成,预生成但未使用的空托盘不得阻塞;禁止按货名、箱规或箱号猜测 SKU。修改本链路时同步检查 `OrderCargoClient`、Order 内部货物接口及 PDA 打托实现。 - 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。 - 客户箱规配置必须恰好保持四档且默认算法在四行一致;`ADD` 以实测尺寸为基准对三个维度分别增加命中箱规的尺寸值,`MULTIPLY` 使用当前客户唯一默认计费规则,并由服务端按规则中的规格档位选择系数。测量行必须保存实际尺寸、选用规则和算法快照,页面不得自行重算后覆盖服务端结果;计价使用用户确认的重量/体积选择值并纳入幂等请求哈希。 - 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。 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 75dbd1ec7..5c5715965 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/DropshipPalletizationMapper.java @@ -56,7 +56,7 @@ public interface DropshipPalletizationMapper { @Param("inboundId") long inboundId,@Param("warehouseId") long warehouseId,@Param("palletNo") String palletNo, @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 * FROM fl_dropship_pallet WHERE pallet_no=#{palletNo} FOR UPDATE") 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(""" @@ -67,11 +67,18 @@ public interface DropshipPalletizationMapper { PalletRow pallet(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("warehouseId") long warehouseId); @Select(""" SELECT p.* FROM fl_dropship_pallet p - WHERE p.task_id=#{taskId} ORDER BY p.id + WHERE p.task_id=#{taskId} + OR EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=p.id AND c.task_id=#{taskId}) + ORDER BY p.id """) List pallets(@Param("taskId") long taskId); @Select(""" SELECT p.* FROM fl_dropship_pallet p - WHERE p.tenant_id=#{tenantId} AND p.inbound_id=#{inboundId} ORDER BY p.id + WHERE ((p.tenant_id=#{tenantId} AND p.inbound_id=#{inboundId}) + AND EXISTS(SELECT 1 FROM fl_inbound_order own + WHERE own.id=#{inboundId} AND own.tenant_id=#{tenantId} AND own.process_status='WAIT_PALLETIZE')) + OR EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c + WHERE c.pallet_id=p.id AND c.tenant_id=#{tenantId} AND c.inbound_id=#{inboundId}) + ORDER BY p.id """) List palletsByInbound(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId); @Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0") @@ -162,15 +169,17 @@ public interface DropshipPalletizationMapper { int seal(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("expectedVersion") int expectedVersion, @Param("warehouseId") long warehouseId,@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName); @Select(""" - SELECT COUNT(*) FROM fl_dropship_pallet p - WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN' + SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet_carton c + JOIN fl_dropship_pallet p ON p.id=c.pallet_id + WHERE c.task_id=#{taskId} AND p.pallet_status='OPEN' """) int openPalletCount(@Param("taskId") long taskId); @Select("SELECT DISTINCT task_id FROM fl_dropship_pallet_carton WHERE pallet_id=#{palletId} ORDER BY task_id") List taskIdsByPallet(@Param("palletId") long palletId); @Update(""" UPDATE fl_dropship_pallet_task SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{taskId} AND task_status='CLAIMED' AND counted_cartons=total_cartons AND allocated_cartons=total_cartons - AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId); + AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c JOIN fl_dropship_pallet p ON p.id=c.pallet_id + WHERE c.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId); @Update(""" UPDATE fl_inbound_order o SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet_carton c JOIN fl_dropship_pallet p ON p.id=c.pallet_id 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 c3a569d5f..224386def 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java @@ -158,8 +158,8 @@ public class DropshipPalletizationService { private PalletRow requireAssignablePallet(TaskRow task,String palletNo){ PalletRow existing=mapper.palletByNo(palletNo); - if(existing==null) throw new BizException(ResultCode.NOT_FOUND,"托盘码不是本单测量审核后生成的托盘码"); - if(!Objects.equals(existing.getTaskId(),task.getId())||!Objects.equals(existing.getWarehouseId(),task.getWarehouseId())) throw conflict("托盘码不属于当前任务"); + if(existing==null) throw new BizException(ResultCode.NOT_FOUND,"托盘码不存在,请扫描测量审核后生成的 DP 托盘码"); + if(!Objects.equals(existing.getWarehouseId(),task.getWarehouseId())) throw conflict("托盘码不属于当前作业仓库"); if(!"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘已封托,不能继续绑定箱"); return existing; } @@ -173,7 +173,7 @@ 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(); + int generated=mapper.palletCount(row.getId()); 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(), diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/DropshipPalletizationSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/DropshipPalletizationSqlContractTest.java index d44557cc2..b549689c2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/DropshipPalletizationSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/DropshipPalletizationSqlContractTest.java @@ -61,4 +61,19 @@ class DropshipPalletizationSqlContractTest { .contains("DROP INDEX uk_loading_item_plan_detail") .contains("ADD UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id,active_assignment_key)"); } + + @Test + void sharedPalletsAreVisibleAndOnlyParticipatingOpenPalletsBlockCompletion() throws Exception { + String pallets = DropshipPalletizationMapper.class.getDeclaredMethod("pallets", long.class) + .getAnnotation(Select.class).value()[0]; + String open = DropshipPalletizationMapper.class.getDeclaredMethod("openPalletCount", long.class) + .getAnnotation(Select.class).value()[0]; + String complete = DropshipPalletizationMapper.class.getDeclaredMethod("completeTask", long.class) + .getAnnotation(org.apache.ibatis.annotations.Update.class).value()[0]; + + assertThat(pallets).contains("c.pallet_id=p.id AND c.task_id=#{taskId}"); + assertThat(open).contains("fl_dropship_pallet_carton c").contains("c.task_id=#{taskId}"); + assertThat(complete).contains("fl_dropship_pallet_carton c JOIN fl_dropship_pallet p") + .doesNotContain("p.task_id=#{taskId}"); + } } 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 a6b6d5683..b985a88d6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java @@ -53,6 +53,24 @@ class DropshipPalletizationServiceTest { verify(mapper).insertCartonSku(anyLong(),anyLong(),eq(40L),eq("SKU-1"),any(),eq(12),eq(12),eq("MATCH"),isNull(),eq(1)); } + @Test void cartonCanBindToAnOpenPalletGeneratedForAnotherTaskInTheSameWarehouse(){ + TaskRow current=task(); PalletRow shared=pallet(); shared.setTaskId(99L); shared.setTenantId(2L); shared.setInboundId(88L); + when(mapper.task(10L,100L)).thenReturn(current); when(mapper.palletCount(10L)).thenReturn(2); + when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(shared); + when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton()); + when(mapper.pallet(10L,20L,100L)).thenReturn(shared); when(mapper.cartons(20L)).thenReturn(java.util.List.of()); + service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三"); + verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),anyLong(),anyLong(),anyString(),any(),eq("BOX-1"),anyInt(),anyInt(),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三")); + } + + @Test void palletFromAnotherWarehouseCannotBeMixed(){ + PalletRow other=pallet(); other.setWarehouseId(101L); + 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(other); + assertThatThrownBy(()->service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三")) + .isInstanceOf(BizException.class).hasMessageContaining("当前作业仓库"); + } + @Test void unknownPalletCodeCannotCreateAnAdHocPallet(){ when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCount(10L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); var request=countRequest("DP-260911-9999");