Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-04 14:06:02 +08:00
8 changed files with 136 additions and 18 deletions
@@ -3216,7 +3216,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),
@@ -10525,9 +10525,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,
@@ -15289,6 +15296,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'
@@ -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;
@@ -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<OrderCargoClient.MeasurementDeclarationSnapshot> 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) {
@@ -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<LoadingPlanItemEntity> 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<OrderCargoClient.CargoBinding> 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<PlanCoLoadGroup> planCoLoadGroups(long planId) {
Map<Long,List<PlanColoadMemberRow>> grouped=repository.planColoadMembers(planId).stream()
.collect(java.util.stream.Collectors.groupingBy(PlanColoadMemberRow::groupId,
@@ -737,7 +737,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),
@@ -996,7 +996,8 @@ paths:
分别写入 CORRECT 和 CONFIRM 修订记录。输入表示含托盘毛重;服务端统一换算为 KG,
使用创建时固化的托盘皮重计算 netWeightKg,且毛重必须大于皮重。
入库单 actualGrossWeightKg 为实测毛重;actualNetWeightKg 为扣除托盘皮重后的货物净重,两者不得混用。
首张图片确认成功时按实测体积容量与已确认图片数建立并占用整票库位块;后续图片绑定下一个空槽。
首张图片确认成功时优先按墨家/人工测量形成的实测体积建立并占用整票库位块;尚无实测体积时依次使用
测量批次冻结的申报体积、Order 最新客户申报体积,三者均无有效值才拒绝。后续图片绑定下一个空槽。
所需槽数增长时原子扩容原块,既有 blockId、slotNo、reservationId 和 locationId 保持不变,
普通连续位不足的新增槽使用同仓共享 A0。确认响应立即返回图片对应的权威块槽绑定。
parameters:
@@ -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)");
}
}
@@ -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);