This commit is contained in:
2026-08-17 11:23:28 +08:00
parent d51e054def
commit 6b416621bb
19 changed files with 327 additions and 222 deletions
@@ -59,18 +59,19 @@ public interface WarehousePalletLocationClient {
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
String inboundCode, String idempotencyKey, long operatorId,
BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType,
BigDecimal totalVolumeCbm, List<PalletReference> pallets,
LocalDateTime expiresAt, boolean measurementCompleted) {}
record PalletReference(long weightResultId, long palletId) {}
record ConfirmRequest(long tenantId, long warehouseId, String sourceService, long businessId,
long operatorId, BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm,
int packageCount) {}
long operatorId, BigDecimal totalVolumeCbm, int packageCount) {}
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
String reason) {}
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
String idempotencyKey, long operatorId, String reason,
List<Long> locationIds) {}
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
long palletId, String palletCode, long locationId, String locationCode,
long palletId, String palletCode, BigDecimal palletCapacityCbm,
long locationId, String locationCode,
BigDecimal allocatedVolumeCbm, String state) {}
record BatchResult(int palletCount, List<AllocationResult> allocations) {}
record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode,
@@ -22,7 +22,7 @@ public class InboundPalletAllocationRepository {
@Transactional
public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId,
BigDecimal capacity, int totalPackageCount, BatchResult result) {
int totalPackageCount, BatchResult result) {
Map<Long, String> reservationNos = mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
.eq(InboundPalletAllocationEntity::getInboundId, inboundId))
@@ -33,9 +33,6 @@ public class InboundPalletAllocationRepository {
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
LocalDateTime now = LocalDateTime.now();
int allocationCount = result.allocations().size();
if (allocationCount > totalPackageCount) {
throw new IllegalArgumentException("托盘数量不能超过入库单总箱数");
}
int basePackageCount = allocationCount == 0 ? 0 : totalPackageCount / allocationCount;
int remainder = allocationCount == 0 ? 0 : totalPackageCount % allocationCount;
for (int index = 0; index < result.allocations().size(); index++) {
@@ -48,7 +45,7 @@ public class InboundPalletAllocationRepository {
value.setWarehouseResourceTenantId(warehouseResourceTenantId);
value.setPalletId(source.palletId());
value.setPalletCode(source.palletCode());
value.setPalletCapacityCbm(capacity);
value.setPalletCapacityCbm(source.palletCapacityCbm());
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
value.setAllocatedPackageCount(allocatedPackages);
@@ -117,6 +117,9 @@ public class InboundMeasurementService {
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
BigDecimal confirmedWeight = confirmedWeightForSubmission(weights);
int confirmedPalletCount = Math.toIntExact(weights.stream()
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
.count());
if (!alreadyAdvanced) {
requireConfirmedPackageTotal(inbound, items);
@@ -137,6 +140,7 @@ public class InboundMeasurementService {
if (confirmedWeight != null) {
inbound.setActualGrossWeightKg(confirmedWeight);
}
inbound.setPalletCount(confirmedPalletCount);
if (measurement.getTotalVolumeCbm() != null) {
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
}
@@ -8,6 +8,8 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.firstleg.config.InboundProperties;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import feign.FeignException;
@@ -29,6 +31,8 @@ public class MeasurementLocationOccupationService {
@Resource
private InboundWorkflowRepository workflow;
@Resource
private InboundMeasurementRepository measurements;
@Resource
private InboundProperties properties;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@@ -39,6 +43,7 @@ public class MeasurementLocationOccupationService {
long warehouseId = requireWarehouse(order);
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
BigDecimal volume = requireVolume(order);
var palletReferences = confirmedPalletReferences(tenantId, inboundId);
// 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键;
// 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion
@@ -46,19 +51,16 @@ public class MeasurementLocationOccupationService {
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
properties.getDefaultPalletCapacityCbm(), "STANDARD",
palletReferences,
LocalDateTime.now().plus(properties.getReservationTtl()), true))),
"测量完成后库位预占失败");
int packageCount = requirePackageCount(order);
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
properties.getDefaultPalletCapacityCbm(), packageCount, reserved);
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, reserved);
try {
WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm(
internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId,
SOURCE_SERVICE, inboundId, operatorId, volume,
properties.getDefaultPalletCapacityCbm(), packageCount))), "测量完成后库位占用失败");
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
properties.getDefaultPalletCapacityCbm(), packageCount, occupied);
SOURCE_SERVICE, inboundId, operatorId, volume, packageCount))), "测量完成后库位占用失败");
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, occupied);
} catch (RuntimeException failure) {
try {
warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest(
@@ -97,6 +99,24 @@ public class MeasurementLocationOccupationService {
return order.getTotalPackageCount();
}
private java.util.List<WarehousePalletLocationClient.PalletReference> confirmedPalletReferences(
long tenantId, long inboundId) {
java.util.List<WarehousePalletLocationClient.PalletReference> references = measurements
.listWeights(tenantId, inboundId).stream()
.filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
.filter(weight -> weight.getConfirmedAt() != null && weight.getNetWeightKg() != null)
.filter(weight -> weight.getPalletId() != null && weight.getPalletId() > 0)
.map(weight -> new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId()))
.toList();
if (references.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "至少需要一张已确认的称重图片才能提交测量");
}
if (references.size() > 200) {
throw new BizException(ResultCode.VALIDATION_FAILED, "单票最多记录200张已确认称重图片");
}
return references;
}
private static <T> ApiResponse<T> call(RemoteCall<T> call) {
try { return call.invoke(); }
catch (FeignException exception) {
@@ -58,12 +58,12 @@ class InboundPalletAllocationRepositoryTest {
when(mapper.selectList(any())).thenReturn(List.of(first, second));
var result = new WarehousePalletLocationClient.BatchResult(2, List.of(
new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", 11L, "A1",
new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", new BigDecimal("1.2"), 11L, "A1",
new BigDecimal("1.2"), "OCCUPIED"),
new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", 12L, "A2",
new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", new BigDecimal("1.2"), 12L, "A2",
new BigDecimal("1.2"), "OCCUPIED")));
repository.replace(10001L, 31L, 88L, 0L, new BigDecimal("1.2"), 2, result);
repository.replace(10001L, 31L, 88L, 0L, 2, result);
ArgumentCaptor<InboundPalletAllocationEntity> inserted = ArgumentCaptor.forClass(InboundPalletAllocationEntity.class);
verify(mapper, org.mockito.Mockito.times(2)).insert(inserted.capture());
@@ -115,21 +115,28 @@ class CartonMeasurementServiceTest {
}
@Test
void measuringInboundAllowsManualEntryBeforeMeasurementSubmission() {
void measuringInboundWithoutMeasurementAllowsManualEntryBeforeSubmission() {
InboundOrderEntity measuring = inbound("MEASURING");
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
when(repository.findInbound(10001L, 70001L)).thenReturn(measuring);
when(repository.lockInbound(10001L, 70001L)).thenReturn(measuring);
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get()));
when(repository.updateMeasurement(measurement)).thenReturn(true);
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(null);
when(repository.listItems(org.mockito.ArgumentMatchers.eq(10001L),
org.mockito.ArgumentMatchers.anyLong())).thenAnswer(invocation -> List.of(insertedItem.get()));
when(repository.updateMeasurement(any())).thenReturn(true);
CartonMeasurementResult result = service.upsertManualForInbound(
10001L, 90001L, 70001L, acceptanceCommand());
ArgumentCaptor<InboundMeasurementEntity> measurement =
ArgumentCaptor.forClass(InboundMeasurementEntity.class);
verify(repository).insertMeasurement(measurement.capture());
assertThat(result.created()).isTrue();
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL);
assertThat(result.measurementId()).isEqualTo(measurement.getValue().getId());
assertThat(measurement.getValue().getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL);
assertThat(measurement.getValue().getSourceCode()).isEqualTo("PLATFORM");
assertThat(measurement.getValue().getStatus()).isEqualTo(MeasurementBatchService.STATUS_RECEIVED);
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(measurement.getValue().getId());
verify(images).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any());
}
@@ -598,6 +598,7 @@ class InboundMeasurementServiceTest {
assertThat(inbound.getMeasureReviewStatus()).isEqualTo("PENDING");
assertThat(measurement.getStatus()).isEqualTo("SUBMITTED");
assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("20.000");
assertThat(inbound.getPalletCount()).isEqualTo(1);
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("1.2345");
}
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.firstleg.config.InboundProperties;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
@@ -39,6 +41,7 @@ class MeasurementLocationOccupationServiceTest {
@MockBean private WarehousePalletLocationClient warehouse;
@MockBean private InboundPalletAllocationRepository allocations;
@MockBean private InboundWorkflowRepository workflow;
@MockBean private InboundMeasurementRepository measurements;
@Resource private InboundProperties properties;
@Resource private MeasurementLocationOccupationService service;
@@ -46,6 +49,7 @@ class MeasurementLocationOccupationServiceTest {
void platformWarehouseResourcesUseZeroWhileSnapshotsRemainInBusinessTenant() {
properties.setWarehouseResourceTenantId(90001L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
@@ -59,6 +63,9 @@ class MeasurementLocationOccupationServiceTest {
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-");
assertThat(reserve.getValue().pallets())
.extracting(WarehousePalletLocationClient.PalletReference::palletId)
.containsExactly(601L, 601L, 601L);
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
@@ -72,7 +79,6 @@ class MeasurementLocationOccupationServiceTest {
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
org.mockito.ArgumentMatchers.eq(20),
org.mockito.ArgumentMatchers.any());
}
@@ -81,6 +87,7 @@ class MeasurementLocationOccupationServiceTest {
void defaultResourceTenantRemainsPlatformZero() {
properties.setWarehouseResourceTenantId(0L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
@@ -104,7 +111,6 @@ class MeasurementLocationOccupationServiceTest {
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
org.mockito.ArgumentMatchers.eq(20),
org.mockito.ArgumentMatchers.any());
}
@@ -113,6 +119,7 @@ class MeasurementLocationOccupationServiceTest {
void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() {
properties.setWarehouseResourceTenantId(90001L);
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
IllegalStateException failure = new IllegalStateException("confirm failed");
@@ -130,7 +137,7 @@ class MeasurementLocationOccupationServiceTest {
assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT,
new BigDecimal("1.200000"), 20, batch("RESERVED"));
20, batch("RESERVED"));
}
private static InboundProperties properties() {
@@ -159,9 +166,22 @@ class MeasurementLocationOccupationServiceTest {
}
private static WarehousePalletLocationClient.BatchResult batch(String state) {
return new WarehousePalletLocationClient.BatchResult(1, List.of(
new WarehousePalletLocationClient.AllocationResult(
501L, "RES-001", 1, 601L, "PL-001", 701L, "A-001",
new BigDecimal("1.0000"), state)));
return new WarehousePalletLocationClient.BatchResult(3, java.util.stream.IntStream.rangeClosed(1, 3)
.mapToObj(index -> new WarehousePalletLocationClient.AllocationResult(
500L + index, "RES-00" + index, index, 601L, "PL-001", new BigDecimal("1.200000"),
700L + index, "A-00" + index, new BigDecimal("0.333333"), state))
.toList());
}
private static List<InboundWeightResultEntity> confirmedWeights() {
return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> {
InboundWeightResultEntity weight = new InboundWeightResultEntity();
weight.setId(800L + index);
weight.setPalletId(601L);
weight.setRecognitionStatus("CONFIRMED");
weight.setConfirmedAt(java.time.LocalDateTime.now());
weight.setNetWeightKg(new BigDecimal("10"));
return weight;
}).toList();
}
}