From 93f06f8ff89576f389fc690433a129dc1db125f9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 14:03:04 +0800 Subject: [PATCH 1/3] fix: fall back to order declared volume --- .../MeasurementLocationOccupationService.java | 28 +++++++++++++++++-- .../main/resources/openapi/firstleg-v1.yaml | 3 +- ...surementLocationOccupationServiceTest.java | 28 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java index 1d7ce7f44..752ad35c4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java @@ -5,6 +5,7 @@ import jakarta.annotation.Resource; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.client.OrderCargoClient; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.config.InboundProperties; import com.baiyue.firstleg.entity.InboundOrderEntity; @@ -32,6 +33,8 @@ public class MeasurementLocationOccupationService { @Resource private InboundMeasurementRepository measurements; @Resource + private OrderCargoClient orderCargo; + @Resource private InboundProperties properties; @Value("${baiyue.security.internal-service-token}") private String internalToken; @@ -167,15 +170,34 @@ public class MeasurementLocationOccupationService { } private BigDecimal requireVolume(long tenantId, long inboundId, InboundOrderEntity order) { + if (order.getActualVolumeCbm() != null && order.getActualVolumeCbm().signum() > 0) { + return order.getActualVolumeCbm(); + } var measurement = measurements.findLatestMeasurement(tenantId, inboundId); if (measurement != null && measurement.getOriginalVolumeCbm() != null && measurement.getOriginalVolumeCbm().signum() > 0) { return measurement.getOriginalVolumeCbm(); } - if (order.getActualVolumeCbm() == null || order.getActualVolumeCbm().signum() <= 0) { - throw new BizException(ResultCode.CONFLICT, "入库单缺少有效客户申报体积"); + if (order.getOrderId() == null || order.getOrderId() <= 0) { + throw new BizException(ResultCode.CONFLICT, "入库单缺少有效实测或客户申报体积"); } - return order.getActualVolumeCbm(); + try { + ApiResponse response = orderCargo.measurementDeclaration( + internalToken, tenantId, order.getOrderId()); + if (response == null || !response.successful() || response.data() == null) { + String message = response == null || !org.springframework.util.StringUtils.hasText(response.message()) + ? "客户订单申报体积查询失败" : response.message(); + throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE + : ResultCode.of(response.code()), message); + } + BigDecimal declaredVolume = response.data().originalVolumeCbm(); + if (declaredVolume != null && declaredVolume.signum() > 0) { + return declaredVolume; + } + } catch (FeignException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法读取客户申报体积"); + } + throw new BizException(ResultCode.CONFLICT, "入库单缺少有效实测或客户申报体积"); } private static int requirePackageCount(InboundOrderEntity order) { diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 887e63226..e239e48b7 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -996,7 +996,8 @@ paths: 分别写入 CORRECT 和 CONFIRM 修订记录。输入表示含托盘毛重;服务端统一换算为 KG, 使用创建时固化的托盘皮重计算 netWeightKg,且毛重必须大于皮重。 入库单 actualGrossWeightKg 为实测毛重;actualNetWeightKg 为扣除托盘皮重后的货物净重,两者不得混用。 - 首张图片确认成功时按实测体积容量与已确认图片数建立并占用整票库位块;后续图片绑定下一个空槽。 + 首张图片确认成功时优先按墨家/人工测量形成的实测体积建立并占用整票库位块;尚无实测体积时依次使用 + 测量批次冻结的申报体积、Order 最新客户申报体积,三者均无有效值才拒绝。后续图片绑定下一个空槽。 所需槽数增长时原子扩容原块,既有 blockId、slotNo、reservationId 和 locationId 保持不变, 普通连续位不足的新增槽使用同仓共享 A0。确认响应立即返回图片对应的权威块槽绑定。 parameters: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java index b51217813..98b45698b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java @@ -2,6 +2,7 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.client.OrderCargoClient; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.config.InboundProperties; import com.baiyue.firstleg.entity.InboundOrderEntity; @@ -45,6 +46,7 @@ class MeasurementLocationOccupationServiceTest { @MockBean private InboundPalletAllocationRepository allocations; @MockBean private InboundWorkflowRepository workflow; @MockBean private InboundMeasurementRepository measurements; + @MockBean private OrderCargoClient orderCargo; @Resource private InboundProperties properties; @Resource private MeasurementLocationOccupationService service; @@ -160,6 +162,32 @@ class MeasurementLocationOccupationServiceTest { 20, new BigDecimal("2.5000"), block("OCCUPIED", 3)); } + @Test + void fallsBackToLatestOrderDeclarationWhenNoMeasurementBatchExists() { + InboundOrderEntity inbound = inbound(); + inbound.setOrderId(77L); + inbound.setActualVolumeCbm(null); + when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound); + when(measurements.findLatestMeasurement(BUSINESS_TENANT, INBOUND)).thenReturn(null); + when(orderCargo.measurementDeclaration("internal-token", BUSINESS_TENANT, inbound.getOrderId())) + .thenReturn(ApiResponse.success(new OrderCargoClient.MeasurementDeclarationSnapshot( + "REF-88", new BigDecimal("100"), new BigDecimal("6.5000"), "货物"))); + when(measurements.listWeights(BUSINESS_TENANT, INBOUND)) + .thenReturn(confirmedWeights().subList(0, 1)); + when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 6))); + when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 6))); + + service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 1); + + verify(orderCargo).measurementDeclaration("internal-token", BUSINESS_TENANT, inbound.getOrderId()); + verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.argThat(request -> request.requiredSlotCount() == 6)); + verify(allocations).replaceBlock(BUSINESS_TENANT, INBOUND, WAREHOUSE, + 20, new BigDecimal("6.5000"), block("OCCUPIED", 6)); + } + @Test void rejectsConfirmedWeightWithoutConfirmationTimeInsteadOfDroppingIt() { InboundWeightResultEntity weight = confirmedWeights().get(0); From f70d38a03425e69905f3eefe6356ba6e0a89cc2a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 14:04:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/LoadingPlanItemEntity.java | 2 + .../firstleg/service/SmartLoadingService.java | 56 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java index 6e402cecf..0f12680ca 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingPlanItemEntity.java @@ -13,6 +13,8 @@ public class LoadingPlanItemEntity { @TableField(exist=false) private Long activeInboundDetailId; @TableField(exist=false) private Integer pickedPackageCount; @TableField(exist=false) private Integer minPackageCount; + @TableField(exist=false) private Integer totalPackageCount; + @TableField(exist=false) private Integer remainingPackageCount; @TableField(exist=false) private Boolean canAdjust; @TableField(exist=false) private Boolean canRemove; private Long createdBy; private String createdByName; private LocalDateTime createdAt; 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 8f9f0fd11..3419e04ef 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -847,16 +847,29 @@ public class SmartLoadingService { ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); Candidate candidate = repository.candidate(planId, booking.getWarehouseId(), item.getInboundId(), item.getInboundDetailId()); - if (candidate == null) throw validation("调整箱数超过可用箱数"); int currentPackageCount = item.getPackageCount() == null ? 0 : item.getPackageCount(); - int remainingPackageCount = Math.max(0, candidate.availablePackageCount() == null - ? 0 : candidate.availablePackageCount()); - if (request.packageCount() > currentPackageCount - && request.packageCount() - currentPackageCount > remainingPackageCount) { + OrderCargoClient.CargoBinding cargo; + int maxPackageCount; + if (candidate == null) { + InboundOrderEntity inbound = repository.inbound(item.getAccountId(), item.getInboundId()); + if (inbound == null || !Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", + "MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING") + .contains(inbound.getProcessStatus())) { + throw validation("调整箱数超过可用箱数"); + } + cargo = loadCargo(item.getAccountId(), item.getOrderId(), item.getInboundDetailId()); + var snapshot = loadInboundSnapshot(item.getAccountId(), item.getOrderId()); + candidate = declaredCandidate(snapshot, item.getInboundId(), cargo, "WAIT_MEASURE"); + maxPackageCount = cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount(); + } else { + cargo = loadCargo(candidate); + maxPackageCount = currentPackageCount + Math.max(0, candidate.availablePackageCount() == null + ? 0 : candidate.availablePackageCount()); + } + if (request.packageCount() > maxPackageCount) { throw validation("调整箱数超过可用箱数"); } requireSameCustomerScope(booking, candidate); - OrderCargoClient.CargoBinding cargo = loadCargo(candidate); List others = new ArrayList<>(repository.items(planId)); others.removeIf(x -> Objects.equals(x.getId(), itemId)); checkRouteAndCapacity(booking, others, candidate, request.packageCount(), true); @@ -1246,9 +1259,13 @@ public class SmartLoadingService { } private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) { + return loadCargo(candidate.accountId(), candidate.orderId(), candidate.inboundDetailId()); + } + + private OrderCargoClient.CargoBinding loadCargo(long accountId, long orderId, long inboundDetailId) { try { ApiResponse response = orderCargoClient.detail(internalToken, - candidate.accountId(), candidate.orderId(), candidate.inboundDetailId()); + accountId, orderId, inboundDetailId); if (response == null) { throw unavailable(); } @@ -1256,8 +1273,8 @@ public class SmartLoadingService { throw new BizException(ResultCode.of(response.code()), response.message()); } OrderCargoClient.CargoBinding cargo = response.data(); - if (cargo == null || !Objects.equals(cargo.cargoId(), candidate.inboundDetailId()) - || !Objects.equals(cargo.orderId(), candidate.orderId())) { + if (cargo == null || !Objects.equals(cargo.cargoId(), inboundDetailId) + || !Objects.equals(cargo.orderId(), orderId)) { throw conflict("订单货物明细返回与入库单不匹配"); } return cargo; @@ -1412,6 +1429,9 @@ public class SmartLoadingService { int picked = task.pickedByPlanItem().getOrDefault(item.getId(), 0); item.setPickedPackageCount(picked); item.setMinPackageCount(Math.max(1, picked)); + int total = adjustmentTotal(p.getId(), b.getWarehouseId(), item); + item.setTotalPackageCount(total); + item.setRemainingPackageCount(Math.max(0, total - item.getPackageCount())); item.setCanAdjust(mutable); item.setCanRemove(mutable && picked == 0); } @@ -1421,6 +1441,24 @@ public class SmartLoadingService { b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus, task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),items); } + private int adjustmentTotal(long planId, long warehouseId, LoadingPlanItemEntity item) { + int current = item.getPackageCount() == null ? 0 : item.getPackageCount(); + Candidate measured = repository.candidate(planId, warehouseId, item.getInboundId(), item.getInboundDetailId()); + if (measured != null) { + return current + Math.max(0, measured.availablePackageCount() == null + ? 0 : measured.availablePackageCount()); + } + InboundOrderEntity inbound = repository.inbound(item.getAccountId(), item.getInboundId()); + if (inbound == null || !Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", + "MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING") + .contains(inbound.getProcessStatus())) return current; + try { + OrderCargoClient.CargoBinding cargo = loadCargo(item.getAccountId(), item.getOrderId(), item.getInboundDetailId()); + return Math.max(current, cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount()); + } catch (RuntimeException ignored) { + return current; + } + } private List planCoLoadGroups(long planId) { Map> grouped=repository.planColoadMembers(planId).stream() .collect(java.util.stream.Collectors.groupingBy(PlanColoadMemberRow::groupId, From b79d136925d846dc07e5fd4a740c5ed303e9ff83 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 14:04:52 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 20 +++++++++++++++---- .../resources/db/phase2-inbound-schema.sql | 2 +- .../PdaSplitSettlementSqlContractTest.java | 15 ++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 5c72c4995..5f35a3b51 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3162,7 +3162,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_pallet_allocation_no (tenant_id, inbound_id, allocation_no), - UNIQUE KEY uk_pallet_allocation_slot (tenant_id, block_id, slot_no), + UNIQUE KEY uk_pallet_allocation_slot (tenant_id, inbound_id, block_id, slot_no), UNIQUE KEY uk_pallet_allocation_weight (tenant_id, weight_result_id), KEY idx_allocation_location (tenant_id, warehouse_id, location_id, allocation_status), KEY idx_allocation_reservation (reservation_id), @@ -10248,9 +10248,16 @@ SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_inbound_pallet_allocation' AND column_name='reservation_no') > 0, 'ALTER TABLE fl_inbound_pallet_allocation DROP COLUMN reservation_no', 'SELECT 1'); PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement; -SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics - WHERE table_schema=DATABASE() AND table_name='fl_inbound_pallet_allocation' AND index_name='uk_pallet_allocation_slot') = 0, - 'ALTER TABLE fl_inbound_pallet_allocation ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,block_id,slot_no)', 'SELECT 1'); +SET @sql = IF((SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index) + FROM information_schema.statistics + WHERE table_schema=DATABASE() AND table_name='fl_inbound_pallet_allocation' + AND index_name='uk_pallet_allocation_slot') <> 'tenant_id,inbound_id,block_id,slot_no', + 'ALTER TABLE fl_inbound_pallet_allocation DROP INDEX uk_pallet_allocation_slot, ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)', + IF((SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema=DATABASE() AND table_name='fl_inbound_pallet_allocation' + AND index_name='uk_pallet_allocation_slot') = 0, + 'ALTER TABLE fl_inbound_pallet_allocation ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)', + 'SELECT 1')); PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement; SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='fl_inbound_pallet_allocation' AND index_name='uk_pallet_allocation_weight') = 0, @@ -15005,6 +15012,11 @@ BEGIN AND table_name = 'fl_inbound_pallet_allocation' AND column_name IN ('pallet_id','pallet_code','pallet_capacity_cbm') AND is_nullable = 'YES') <> 3 + OR (SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index) + FROM information_schema.statistics + WHERE table_schema = 'baiyue_firstleg' + AND table_name = 'fl_inbound_pallet_allocation' + AND index_name = 'uk_pallet_allocation_slot') <> 'tenant_id,inbound_id,block_id,slot_no' OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_pallet_allocation' diff --git a/firstleg/src/main/resources/db/phase2-inbound-schema.sql b/firstleg/src/main/resources/db/phase2-inbound-schema.sql index faed9ff13..53320af37 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-schema.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-schema.sql @@ -735,7 +735,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_pallet_allocation_no (tenant_id, inbound_id, allocation_no), - UNIQUE KEY uk_pallet_allocation_slot (tenant_id, block_id, slot_no), + UNIQUE KEY uk_pallet_allocation_slot (tenant_id, inbound_id, block_id, slot_no), UNIQUE KEY uk_pallet_allocation_weight (tenant_id, weight_result_id), KEY idx_allocation_location (tenant_id, warehouse_id, location_id, allocation_status), KEY idx_allocation_reservation (reservation_id), 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 08e2d4f21..f6b1b3f0b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java @@ -175,4 +175,19 @@ class PdaSplitSettlementSqlContractTest { .contains("block_id=#{blockId}") .contains("allocation_status='CONFIRMED'"); } + + @Test + void allowsTransferredSourceAndRemainderChildToKeepTheSameBlockSlotIdentity() throws Exception { + String moduleSql = new ClassPathResource("db/phase2-inbound-schema.sql") + .getContentAsString(StandardCharsets.UTF_8); + String unifiedSql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"), + StandardCharsets.UTF_8); + + assertThat(moduleSql) + .contains("UNIQUE KEY uk_pallet_allocation_slot (tenant_id, inbound_id, block_id, slot_no)") + .doesNotContain("UNIQUE KEY uk_pallet_allocation_slot (tenant_id, block_id, slot_no)"); + assertThat(unifiedSql) + .contains("'tenant_id,inbound_id,block_id,slot_no'") + .contains("ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)"); + } }