diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java index 0bd6595ff..647c9c080 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java @@ -246,7 +246,9 @@ public interface InboundSplitSettlementMapper { @Update(""" UPDATE fl_loading_plan_item plan_item JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id = plan_item.id - SET plan_item.package_count = pick_item.picked_package_count, + SET plan_item.pallet_count = CEIL(COALESCE(plan_item.pallet_count,0) + * pick_item.picked_package_count / NULLIF(plan_item.package_count,0)), + plan_item.package_count = pick_item.picked_package_count, plan_item.updated_by = #{operatorId}, plan_item.updated_by_name = #{operatorName}, plan_item.updated_at = CURRENT_TIMESTAMP(3), plan_item.version = plan_item.version + 1 WHERE plan_item.id = #{planItemId} AND plan_item.deleted = 0 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java index c93a7c73d..2306dafaa 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java @@ -174,7 +174,13 @@ public interface LoadingListQueryMapper { SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no, i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no, i.sku_code, i.destination_warehouse_name, i.product_name_zh, - product_name_en, product_image_url, package_count, pallet_count, + product_name_en, product_image_url, i.package_count, + COALESCE(NULLIF(i.pallet_count,0), + CEIL(COALESCE(NULLIF(inbound.pallet_count,0), + (SELECT COUNT(*) FROM fl_inbound_weight_result weight + WHERE weight.tenant_id=i.account_id AND weight.inbound_id=i.inbound_id + AND weight.deleted=0 AND weight.recognition_status='CONFIRMED'),0) + * i.package_count / NULLIF(inbound.total_package_count,0)),0) pallet_count, gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg, product_quantity, units_per_carton, fba_tracking_no, shipment_no, shipping_mark, internal_remark, customer_remark, warehouse_remark, @@ -182,6 +188,7 @@ public interface LoadingListQueryMapper { FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE' + LEFT JOIN fl_inbound_order inbound ON inbound.id=i.inbound_id AND inbound.tenant_id=i.account_id WHERE i.loading_plan_id = #{planId} AND i.deleted = 0 ORDER BY i.load_sequence, i.id """) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java index 5d48d045d..ab28e444c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java @@ -6,11 +6,13 @@ import com.baiyue.firstleg.entity.ContainerBookingEntity; import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; import com.baiyue.firstleg.entity.LoadingPlanItemEntity; +import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.mapper.ContainerBookingMapper; import com.baiyue.firstleg.mapper.LoadingPlanHistoryMapper; import com.baiyue.firstleg.mapper.LoadingPlanItemMapper; import com.baiyue.firstleg.mapper.LoadingPlanMapper; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper; +import com.baiyue.firstleg.mapper.InboundOrderMapper; import org.springframework.stereotype.Repository; import jakarta.annotation.Resource; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -26,11 +28,17 @@ public class SmartLoadingRepository { @Resource private LoadingPlanHistoryMapper histories; @Resource private ContainerBookingMapper bookings; @Resource private SmartLoadingCandidateMapper candidates; + @Resource private InboundOrderMapper inbounds; public List candidates(long warehouseId) { return candidates.candidates(warehouseId); } public Candidate candidate(long warehouseId, long inboundId, long detailId) { return candidates.candidate(warehouseId, inboundId, detailId); } + public InboundOrderEntity inbound(long accountId, long inboundId) { + return inbounds.selectOne(new LambdaQueryWrapper() + .eq(InboundOrderEntity::getTenantId, accountId) + .eq(InboundOrderEntity::getId, inboundId)); + } public List activeItemsByInbound(long planId, long accountId, long inboundId) { return items.selectList(new LambdaQueryWrapper() .eq(LoadingPlanItemEntity::getLoadingPlanId, planId) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index f60bca467..a3ac10ebb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -136,7 +136,8 @@ public class SmartLoadingService { if (candidate == null) throw conflict("入库货物不存在或尚未完成入库"); requireSameChannel(booking, candidate); requireSameCustomerScope(booking, candidate); - if ("LOADING".equals(plan.getPlanStatus())) { + var task = loadingPickService.mutationSnapshot(planId); + if (task.taskId() != null && "LOADING".equals(task.taskStatus())) { splitPreloadService.ensureForRunningLoadingAllocation(candidate.accountId(), candidate.inboundId(), operatorId, operatorName); } else { @@ -161,7 +162,8 @@ public class SmartLoadingService { } else { existing.setPackageCount(total); existing.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(total))); existing.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(total))); - existing.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(total))); applyCargo(existing, cargo, total); + existing.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(total))); + existing.setPalletCount(estimatedPalletCount(candidate, total)); applyCargo(existing, cargo, total); existing.setUpdatedBy(operatorId); existing.setUpdatedByName(operatorName); if (repository.update(existing) != 1) throw conflict("装柜草稿已被并发修改"); changed = existing; @@ -317,7 +319,8 @@ public class SmartLoadingService { item.setPackageCount(request.packageCount()); item.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount()))); item.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount()))); - item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount()))); applyCargo(item, cargo, request.packageCount()); + item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount()))); + item.setPalletCount(estimatedPalletCount(candidate, request.packageCount())); applyCargo(item, cargo, request.packageCount()); item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName); if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改"); history(planId, itemId, "ADJUST_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation, @@ -448,8 +451,16 @@ public class SmartLoadingService { i.setGrossWeightKg(unit(c.unitGrossWeightKg()).multiply(BigDecimal.valueOf(count))); i.setNetWeightKg(unit(c.unitNetWeightKg()).multiply(BigDecimal.valueOf(count))); i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count))); + i.setPalletCount(estimatedPalletCount(c, count)); applyCargo(i, cargo, count); i.setLoadSequence(seq); i.setDeleted(false); i.setCreatedBy(op); i.setCreatedByName(name); i.setUpdatedBy(op); i.setUpdatedByName(name); i.setVersion(0); return i; } + private int estimatedPalletCount(Candidate candidate, int packageCount) { + InboundOrderEntity inbound = repository.inbound(candidate.accountId(), candidate.inboundId()); + if (inbound == null || inbound.getPalletCount() == null || inbound.getPalletCount() <= 0 + || inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() <= 0) return 0; + return BigDecimal.valueOf(inbound.getPalletCount()).multiply(BigDecimal.valueOf(packageCount)) + .divide(BigDecimal.valueOf(inbound.getTotalPackageCount()), 0, RoundingMode.CEILING).intValueExact(); + } private void applyCargo(LoadingPlanItemEntity item, OrderCargoClient.CargoBinding cargo, int boxes) { item.setSkuCode(cargo.skuNo()); item.setProductNameEn(cargo.cargoNameEn()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java index 3a5314063..778d1ce6d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java @@ -68,21 +68,22 @@ public class SplitPreloadService { @Transactional public void ensurePendingForLoadingAllocation(long tenantId, long inboundId, long operatorId, String operatorName) { - ensureForLoadingAllocation(tenantId, inboundId, operatorId, operatorName, false); + ensureLoadingAllocationLifecycle(tenantId, inboundId, operatorId, operatorName, false); } /** - * A running loading task may re-add an inbound whose preload was confirmed by an earlier - * incremental allocation. Draft plans must still use the stricter pending-only entry point. + * A running PDA task may restore or add an unpicked allocation after the plan-level preload + * confirmation. CONFIRMING remains immutable; CONFIRMED is accepted because the caller owns + * the active PDA task lock and will synchronise the restored loading item immediately. */ @Transactional public void ensureForRunningLoadingAllocation(long tenantId, long inboundId, long operatorId, String operatorName) { - ensureForLoadingAllocation(tenantId, inboundId, operatorId, operatorName, true); + ensureLoadingAllocationLifecycle(tenantId, inboundId, operatorId, operatorName, true); } - private void ensureForLoadingAllocation(long tenantId, long inboundId, long operatorId, - String operatorName, boolean allowConfirmed) { + private void ensureLoadingAllocationLifecycle(long tenantId, long inboundId, long operatorId, + String operatorName, boolean allowConfirmed) { InboundSnapshot inbound = requireInbound(tenantId, inboundId); requireWaitingSplit(inbound, "仅待分柜订单可以建立智能装柜预装计划"); PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java index 76018775b..11e77ed09 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java @@ -60,6 +60,9 @@ class LoadingListQueryMapperContractTest { assertThat(items) .contains("i.order_id", "i.order_no", "b.container_no") .contains("b.record_status='ACTIVE'") + .contains("NULLIF(i.pallet_count,0)") + .contains("fl_inbound_weight_result weight") + .contains("weight.recognition_status='CONFIRMED'") .contains("i.loading_plan_id = #{planId}") .contains("i.deleted = 0"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java index fde74c866..5d0c3b22a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java @@ -124,6 +124,17 @@ class PdaSplitSettlementSqlContractTest { .contains("split_status='FULLY_SPLIT'"); } + @Test + void partialPickingConvergesPalletSnapshotWithActualCartons() throws Exception { + String converge = InboundSplitSettlementMapper.class.getMethod("convergePartialPlanItem", + long.class, long.class, String.class) + .getAnnotation(Update.class).value()[0]; + assertThat(converge) + .contains("plan_item.pallet_count = CEIL") + .contains("pick_item.picked_package_count / NULLIF(plan_item.package_count,0)") + .contains("plan_item.package_count = pick_item.picked_package_count"); + } + @Test void keepsAllocationConfirmedUntilReleaseStep() throws Exception { String consume = InboundPalletAllocationMapper.class.getMethod("consume", diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java index 2f42a8329..9b5cba633 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java @@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.ContainerBookingEntity; import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; import com.baiyue.firstleg.entity.LoadingPlanItemEntity; +import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.repository.SmartLoadingRepository; import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService; import com.baiyue.firstleg.split.service.SplitPreloadService; @@ -300,6 +301,9 @@ class SmartLoadingServiceTest { @Test void snapshotsCargoFields() { stubAssignment(); + InboundOrderEntity inbound = new InboundOrderEntity(); + inbound.setId(10L); inbound.setTenantId(9L); inbound.setTotalPackageCount(10); inbound.setPalletCount(4); + when(repository.inbound(9L, 10L)).thenReturn(inbound); when(cargoClient.detail(anyString(), eq(9L), eq(30L), eq(20L))).thenReturn(ApiResponse.success( new OrderCargoClient.CargoBinding(20L, 30L, 1, "中文品名", 10, new BigDecimal("20"), new BigDecimal("2"), "SKU-1", "English name", @@ -320,6 +324,7 @@ class SmartLoadingServiceTest { assertThat(saved.getProductNameEn()).isEqualTo("English name"); assertThat(saved.getUnitsPerCarton()).isEqualTo(4); assertThat(saved.getProductQuantity()).isEqualTo(20); + assertThat(saved.getPalletCount()).isEqualTo(2); assertThat(saved.getNetWeightKg()).isEqualByComparingTo("7.5"); assertThat(saved.getCustomerRemark()).isEqualTo("客户备注"); assertThat(saved.getInternalRemark()).isEqualTo("最新内部备注"); @@ -354,6 +359,8 @@ class SmartLoadingServiceTest { when(repository.items(1)).thenReturn(List.of()); when(repository.candidate(100, 10, 20)).thenReturn(candidate(10)); when(repository.activeItem(1L, 9L, 20L)).thenReturn(null); + when(loadingPickService.mutationSnapshot(1L)).thenReturn( + new LoadingPickService.MutationSnapshot(77L, "LOADING", 6, Map.of())); when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(ApiResponse.success( new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 1, BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, null))); @@ -361,14 +368,15 @@ class SmartLoadingServiceTest { Plan result = service.assign(1, new AssignRequest(10L, 20L, 1, 5, "assign-loading"), 7, "u"); assertThat(result.status()).isEqualTo("LOADING"); - verify(splitPreloadService).ensureForRunningLoadingAllocation(9L, 10L, 7L, "u"); - verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(anyLong(), anyLong(), anyLong(), anyString()); - var order = inOrder(loadingPickService, repository, loadingModelBridge); + var order = inOrder(loadingPickService, splitPreloadService, repository, loadingModelBridge); order.verify(loadingPickService).lockMutable(1L); + order.verify(splitPreloadService).ensureForRunningLoadingAllocation(9L, 10L, 7L, "u"); order.verify(repository).touch(1, 5, 7, "u"); order.verify(loadingModelBridge).bindIncrementalAllocation(1L, 9L, 10L, 7L, "u", "assign-loading"); order.verify(loadingPickService).sync(eq(1L), any(LoadingPlanItemEntity.class)); + verify(splitPreloadService, never()).ensurePendingForLoadingAllocation( + anyLong(), anyLong(), anyLong(), anyString()); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java index 08ea4d1bb..9050c7dd4 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/split/service/SplitPreloadServiceTest.java @@ -92,4 +92,14 @@ class SplitPreloadServiceTest { verify(repository, never()).createPlan(any()); } + @Test + void runningLoadingAllocationStillRejectsConfirmingPreload() { + when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 3)); + PreloadPlanEntity plan = new PreloadPlanEntity(); + plan.setStatus("CONFIRMING"); + when(repository.findPlan(10, 20)).thenReturn(plan); + + assertThatThrownBy(() -> service.ensureForRunningLoadingAllocation(10, 20, 7, "admin")) + .hasMessageContaining("已进入确认流程"); + } }